Merge branch 'main' of https://github.com/langgenius/dify into feat/support-image-generate-for-gemini

pull/17372/head
QuantumGhost 1 year ago
commit 435537287e

@ -6,7 +6,7 @@
本指南和 Dify 一样在不断完善中。如果有任何滞后于项目实际情况的地方,恳请谅解,我们也欢迎任何改进建议。
关于许可证,请花一分钟阅读我们简短的[许可和贡献者协议](./LICENSE)。社区同时也遵循[行为准则](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md)。
关于许可证,请花一分钟阅读我们简短的[许可和贡献者协议](./LICENSE)。同时也遵循社区[行为准则](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md)。
## 开始之前

@ -54,7 +54,7 @@
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
</p>
Dify is an open-source LLM app development platform. Its intuitive interface combines agentic AI workflow, RAG pipeline, agent capabilities, model management, observability features and more, letting you quickly go from prototype to production.
Dify is an open-source LLM app development platform. Its intuitive interface combines agentic AI workflow, RAG pipeline, agent capabilities, model management, observability features, and more, allowing you to quickly move from prototype to production.
## Quick start
@ -188,7 +188,7 @@ All of Dify's offerings come with corresponding APIs, so you could effortlessly
- **Dify for enterprise / organizations</br>**
We provide additional enterprise-centric features. [Log your questions for us through this chatbot](https://udify.app/chat/22L1zSxg6yW1cWQg) or [send us an email](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry) to discuss enterprise needs. </br>
> For startups and small businesses using AWS, check out [Dify Premium on AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) and deploy it to your own AWS VPC with one-click. It's an affordable AMI offering with the option to create apps with custom logo and branding.
> For startups and small businesses using AWS, check out [Dify Premium on AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) and deploy it to your own AWS VPC with one click. It's an affordable AMI offering with the option to create apps with custom logo and branding.
## Staying ahead
@ -233,7 +233,7 @@ Deploy Dify to AWS with [CDK](https://aws.amazon.com/cdk/)
For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
At the same time, please consider supporting Dify by sharing it on social media and at events and conferences.
> We are looking for contributors to help with translating Dify to languages other than Mandarin or English. If you are interested in helping, please see the [i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) for more information, and leave us a comment in the `global-users` channel of our [Discord Community Server](https://discord.gg/8Tpq4AcN9c).
> We are looking for contributors to help translate Dify into languages other than Mandarin or English. If you are interested in helping, please see the [i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) for more information, and leave us a comment in the `global-users` channel of our [Discord Community Server](https://discord.gg/8Tpq4AcN9c).
## Community & contact

@ -297,6 +297,7 @@ LINDORM_URL=http://ld-*******************-proxy-search-pub.lindorm.aliyuncs.com:
LINDORM_USERNAME=admin
LINDORM_PASSWORD=admin
USING_UGC_INDEX=False
LINDORM_QUERY_TIMEOUT=1
# OceanBase Vector configuration
OCEANBASE_VECTOR_HOST=127.0.0.1

@ -271,6 +271,7 @@ def migrate_knowledge_vector_database():
upper_collection_vector_types = {
VectorType.MILVUS,
VectorType.PGVECTOR,
VectorType.VASTBASE,
VectorType.RELYT,
VectorType.WEAVIATE,
VectorType.ORACLE,

@ -39,6 +39,7 @@ from .vdb.tencent_vector_config import TencentVectorDBConfig
from .vdb.tidb_on_qdrant_config import TidbOnQdrantConfig
from .vdb.tidb_vector_config import TiDBVectorConfig
from .vdb.upstash_config import UpstashConfig
from .vdb.vastbase_vector_config import VastbaseVectorConfig
from .vdb.vikingdb_config import VikingDBConfig
from .vdb.weaviate_config import WeaviateConfig
@ -270,6 +271,7 @@ class MiddlewareConfig(
OpenSearchConfig,
OracleConfig,
PGVectorConfig,
VastbaseVectorConfig,
PGVectoRSConfig,
QdrantConfig,
RelytConfig,

@ -32,3 +32,4 @@ class LindormConfig(BaseSettings):
description="Using UGC index will store the same type of Index in a single index but can retrieve separately.",
default=False,
)
LINDORM_QUERY_TIMEOUT: Optional[float] = Field(description="The lindorm search request timeout (s)", default=2.0)

@ -0,0 +1,45 @@
from typing import Optional
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class VastbaseVectorConfig(BaseSettings):
"""
Configuration settings for Vector (Vastbase with vector extension)
"""
VASTBASE_HOST: Optional[str] = Field(
description="Hostname or IP address of the Vastbase server with Vector extension (e.g., 'localhost')",
default=None,
)
VASTBASE_PORT: PositiveInt = Field(
description="Port number on which the Vastbase server is listening (default is 5432)",
default=5432,
)
VASTBASE_USER: Optional[str] = Field(
description="Username for authenticating with the Vastbase database",
default=None,
)
VASTBASE_PASSWORD: Optional[str] = Field(
description="Password for authenticating with the Vastbase database",
default=None,
)
VASTBASE_DATABASE: Optional[str] = Field(
description="Name of the Vastbase database to connect to",
default=None,
)
VASTBASE_MIN_CONNECTION: PositiveInt = Field(
description="Min connection of the Vastbase database",
default=1,
)
VASTBASE_MAX_CONNECTION: PositiveInt = Field(
description="Max connection of the Vastbase database",
default=5,
)

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

@ -80,8 +80,6 @@ class ChatMessageTextApi(Resource):
@account_initialization_required
@get_app_model
def post(self, app_model: App):
from werkzeug.exceptions import InternalServerError
try:
parser = reqparse.RequestParser()
parser.add_argument("message_id", type=str, location="json")

@ -657,6 +657,7 @@ class DatasetRetrievalSettingApi(Resource):
| VectorType.ELASTICSEARCH
| VectorType.ELASTICSEARCH_JA
| VectorType.PGVECTOR
| VectorType.VASTBASE
| VectorType.TIDB_ON_QDRANT
| VectorType.LINDORM
| VectorType.COUCHBASE
@ -706,6 +707,7 @@ class DatasetRetrievalSettingMockApi(Resource):
| VectorType.ELASTICSEARCH_JA
| VectorType.COUCHBASE
| VectorType.PGVECTOR
| VectorType.VASTBASE
| VectorType.LINDORM
| VectorType.OPENGAUSS
| VectorType.OCEANBASE

@ -5,6 +5,7 @@ from werkzeug.exceptions import Forbidden
from controllers.console import api
from controllers.console.wraps import account_initialization_required, setup_required
from core.model_runtime.utils.encoders import jsonable_encoder
from core.plugin.manager.exc import PluginPermissionDeniedError
from libs.login import login_required
from services.plugin.endpoint_service import EndpointService
@ -28,15 +29,18 @@ class EndpointCreateApi(Resource):
settings = args["settings"]
name = args["name"]
return {
"success": EndpointService.create_endpoint(
tenant_id=user.current_tenant_id,
user_id=user.id,
plugin_unique_identifier=plugin_unique_identifier,
name=name,
settings=settings,
)
}
try:
return {
"success": EndpointService.create_endpoint(
tenant_id=user.current_tenant_id,
user_id=user.id,
plugin_unique_identifier=plugin_unique_identifier,
name=name,
settings=settings,
)
}
except PluginPermissionDeniedError as e:
raise ValueError(e.description) from e
class EndpointListApi(Resource):

@ -10,6 +10,7 @@ from configs import dify_config
from controllers.console.workspace.error import AccountNotInitializedError
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from models.account import AccountStatus
from models.dataset import RateLimitLog
from models.model import DifySetup
from services.feature_service import FeatureService, LicenseStatus
@ -24,7 +25,7 @@ def account_initialization_required(view):
# check account initialization
account = current_user
if account.status == "uninitialized":
if account.status == AccountStatus.UNINITIALIZED:
raise AccountNotInitializedError()
return view(*args, **kwargs)

@ -14,6 +14,9 @@ from fields.conversation_fields import (
conversation_infinite_scroll_pagination_fields,
simple_conversation_fields,
)
from fields.conversation_variable_fields import (
conversation_variable_infinite_scroll_pagination_fields,
)
from libs.helper import uuid_value
from models.model import App, AppMode, EndUser
from services.conversation_service import ConversationService
@ -93,6 +96,31 @@ class ConversationRenameApi(Resource):
raise NotFound("Conversation Not Exists.")
class ConversationVariablesApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
@marshal_with(conversation_variable_infinite_scroll_pagination_fields)
def get(self, app_model: App, end_user: EndUser, c_id):
# conversational variable only for chat app
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError()
conversation_id = str(c_id)
parser = reqparse.RequestParser()
parser.add_argument("last_id", type=uuid_value, location="args")
parser.add_argument("limit", type=int_range(1, 100), required=False, default=20, location="args")
args = parser.parse_args()
try:
return ConversationService.get_conversational_variable(
app_model, conversation_id, end_user, args["limit"], args["last_id"]
)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
api.add_resource(ConversationRenameApi, "/conversations/<uuid:c_id>/name", endpoint="conversation_name")
api.add_resource(ConversationApi, "/conversations")
api.add_resource(ConversationDetailApi, "/conversations/<uuid:c_id>", endpoint="conversation_detail")
api.add_resource(ConversationVariablesApi, "/conversations/<uuid:c_id>/variables", endpoint="conversation_variables")

@ -59,7 +59,7 @@ class WorkflowRunDetailApi(Resource):
Get a workflow task running detail
"""
app_mode = AppMode.value_of(app_model.mode)
if app_mode != AppMode.WORKFLOW:
if app_mode not in [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]:
raise NotWorkflowAppError()
workflow_run = db.session.query(WorkflowRun).filter(WorkflowRun.id == workflow_run_id).first()

@ -7,6 +7,7 @@ from typing import Any, Literal, Optional, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
from sqlalchemy.orm import sessionmaker
import contexts
from configs import dify_config
@ -24,6 +25,8 @@ from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotA
from core.model_runtime.errors.invoke import InvokeAuthorizationError
from core.ops.ops_trace_manager import TraceQueueManager
from core.prompt.utils.get_thread_messages_length import get_thread_messages_length
from core.repository import RepositoryFactory
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from extensions.ext_database import db
from factories import file_factory
from models.account import Account
@ -158,11 +161,22 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock())
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
params={
"tenant_id": application_generate_entity.app_config.tenant_id,
"app_id": application_generate_entity.app_config.app_id,
"session_factory": session_factory,
}
)
return self._generate(
workflow=workflow,
user=user,
invoke_from=invoke_from,
application_generate_entity=application_generate_entity,
workflow_node_execution_repository=workflow_node_execution_repository,
conversation=conversation,
stream=streaming,
)
@ -215,11 +229,22 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock())
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
params={
"tenant_id": application_generate_entity.app_config.tenant_id,
"app_id": application_generate_entity.app_config.app_id,
"session_factory": session_factory,
}
)
return self._generate(
workflow=workflow,
user=user,
invoke_from=InvokeFrom.DEBUGGER,
application_generate_entity=application_generate_entity,
workflow_node_execution_repository=workflow_node_execution_repository,
conversation=None,
stream=streaming,
)
@ -270,11 +295,22 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock())
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
params={
"tenant_id": application_generate_entity.app_config.tenant_id,
"app_id": application_generate_entity.app_config.app_id,
"session_factory": session_factory,
}
)
return self._generate(
workflow=workflow,
user=user,
invoke_from=InvokeFrom.DEBUGGER,
application_generate_entity=application_generate_entity,
workflow_node_execution_repository=workflow_node_execution_repository,
conversation=None,
stream=streaming,
)
@ -286,6 +322,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
user: Union[Account, EndUser],
invoke_from: InvokeFrom,
application_generate_entity: AdvancedChatAppGenerateEntity,
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
conversation: Optional[Conversation] = None,
stream: bool = True,
) -> Mapping[str, Any] | Generator[str | Mapping[str, Any], Any, None]:
@ -296,6 +333,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
:param user: account or end user
:param invoke_from: invoke from source
:param application_generate_entity: application generate entity
:param workflow_node_execution_repository: repository for workflow node execution
:param conversation: conversation
:param stream: is stream
"""
@ -348,6 +386,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
conversation=conversation,
message=message,
user=user,
workflow_node_execution_repository=workflow_node_execution_repository,
stream=stream,
)
@ -419,6 +458,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
conversation: Conversation,
message: Message,
user: Union[Account, EndUser],
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
stream: bool = False,
) -> Union[ChatbotAppBlockingResponse, Generator[ChatbotAppStreamResponse, None, None]]:
"""
@ -430,6 +470,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
:param message: message
:param user: account or end user
:param stream: is stream
:param workflow_node_execution_repository: optional repository for workflow node execution
:return:
"""
# init generate task pipeline
@ -442,6 +483,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
user=user,
stream=stream,
dialogue_count=self._dialogue_count,
workflow_node_execution_repository=workflow_node_execution_repository,
)
try:

@ -62,6 +62,7 @@ from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage
from core.model_runtime.entities.llm_entities import LLMUsage
from core.model_runtime.utils.encoders import jsonable_encoder
from core.ops.ops_trace_manager import TraceQueueManager
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.enums import SystemVariableKey
from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState
from core.workflow.nodes import NodeType
@ -93,6 +94,7 @@ class AdvancedChatAppGenerateTaskPipeline:
user: Union[Account, EndUser],
stream: bool,
dialogue_count: int,
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
) -> None:
self._base_task_pipeline = BasedGenerateTaskPipeline(
application_generate_entity=application_generate_entity,
@ -123,6 +125,7 @@ class AdvancedChatAppGenerateTaskPipeline:
SystemVariableKey.WORKFLOW_ID: workflow.id,
SystemVariableKey.WORKFLOW_RUN_ID: application_generate_entity.workflow_run_id,
},
workflow_node_execution_repository=workflow_node_execution_repository,
)
self._task_state = WorkflowTaskState()
@ -684,7 +687,9 @@ class AdvancedChatAppGenerateTaskPipeline:
)
elif isinstance(event, QueueMessageReplaceEvent):
# published by moderation
yield self._message_cycle_manager._message_replace_to_stream_response(answer=event.text)
yield self._message_cycle_manager._message_replace_to_stream_response(
answer=event.text, reason=event.reason
)
elif isinstance(event, QueueAdvancedChatMessageEndEvent):
if not graph_runtime_state:
raise ValueError("graph runtime state not initialized.")
@ -695,7 +700,8 @@ class AdvancedChatAppGenerateTaskPipeline:
if output_moderation_answer:
self._task_state.answer = output_moderation_answer
yield self._message_cycle_manager._message_replace_to_stream_response(
answer=output_moderation_answer
answer=output_moderation_answer,
reason=QueueMessageReplaceEvent.MessageReplaceReason.OUTPUT_MODERATION,
)
# Save message

@ -153,6 +153,8 @@ class MessageBasedAppGenerator(BaseAppGenerator):
query = application_generate_entity.query or "New conversation"
else:
query = next(iter(application_generate_entity.inputs.values()), "New conversation")
if isinstance(query, int):
query = str(query)
query = query or "New conversation"
conversation_name = (query[:20] + "") if len(query) > 20 else query

@ -7,6 +7,7 @@ from typing import Any, Literal, Optional, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
from sqlalchemy.orm import sessionmaker
import contexts
from configs import dify_config
@ -22,6 +23,8 @@ from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerat
from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse
from core.model_runtime.errors.invoke import InvokeAuthorizationError
from core.ops.ops_trace_manager import TraceQueueManager
from core.repository import RepositoryFactory
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from extensions.ext_database import db
from factories import file_factory
from models import Account, App, EndUser, Workflow
@ -133,12 +136,23 @@ class WorkflowAppGenerator(BaseAppGenerator):
contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock())
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
params={
"tenant_id": application_generate_entity.app_config.tenant_id,
"app_id": application_generate_entity.app_config.app_id,
"session_factory": session_factory,
}
)
return self._generate(
app_model=app_model,
workflow=workflow,
user=user,
application_generate_entity=application_generate_entity,
invoke_from=invoke_from,
workflow_node_execution_repository=workflow_node_execution_repository,
streaming=streaming,
workflow_thread_pool_id=workflow_thread_pool_id,
)
@ -151,6 +165,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
user: Union[Account, EndUser],
application_generate_entity: WorkflowAppGenerateEntity,
invoke_from: InvokeFrom,
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
streaming: bool = True,
workflow_thread_pool_id: Optional[str] = None,
) -> Union[Mapping[str, Any], Generator[str | Mapping[str, Any], None, None]]:
@ -162,6 +177,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param user: account or end user
:param application_generate_entity: application generate entity
:param invoke_from: invoke from source
:param workflow_node_execution_repository: repository for workflow node execution
:param streaming: is stream
:param workflow_thread_pool_id: workflow thread pool id
"""
@ -193,6 +209,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
workflow=workflow,
queue_manager=queue_manager,
user=user,
workflow_node_execution_repository=workflow_node_execution_repository,
stream=streaming,
)
@ -245,12 +262,23 @@ class WorkflowAppGenerator(BaseAppGenerator):
contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock())
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
params={
"tenant_id": application_generate_entity.app_config.tenant_id,
"app_id": application_generate_entity.app_config.app_id,
"session_factory": session_factory,
}
)
return self._generate(
app_model=app_model,
workflow=workflow,
user=user,
invoke_from=InvokeFrom.DEBUGGER,
application_generate_entity=application_generate_entity,
workflow_node_execution_repository=workflow_node_execution_repository,
streaming=streaming,
)
@ -299,12 +327,23 @@ class WorkflowAppGenerator(BaseAppGenerator):
contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock())
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
params={
"tenant_id": application_generate_entity.app_config.tenant_id,
"app_id": application_generate_entity.app_config.app_id,
"session_factory": session_factory,
}
)
return self._generate(
app_model=app_model,
workflow=workflow,
user=user,
invoke_from=InvokeFrom.DEBUGGER,
application_generate_entity=application_generate_entity,
workflow_node_execution_repository=workflow_node_execution_repository,
streaming=streaming,
)
@ -361,6 +400,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
workflow: Workflow,
queue_manager: AppQueueManager,
user: Union[Account, EndUser],
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
stream: bool = False,
) -> Union[WorkflowAppBlockingResponse, Generator[WorkflowAppStreamResponse, None, None]]:
"""
@ -370,6 +410,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param queue_manager: queue manager
:param user: account or end user
:param stream: is stream
:param workflow_node_execution_repository: optional repository for workflow node execution
:return:
"""
# init generate task pipeline
@ -379,6 +420,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
queue_manager=queue_manager,
user=user,
stream=stream,
workflow_node_execution_repository=workflow_node_execution_repository,
)
try:

@ -54,6 +54,7 @@ from core.app.entities.task_entities import (
from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline
from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage
from core.ops.ops_trace_manager import TraceQueueManager
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.enums import SystemVariableKey
from extensions.ext_database import db
from models.account import Account
@ -82,6 +83,7 @@ class WorkflowAppGenerateTaskPipeline:
queue_manager: AppQueueManager,
user: Union[Account, EndUser],
stream: bool,
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
) -> None:
self._base_task_pipeline = BasedGenerateTaskPipeline(
application_generate_entity=application_generate_entity,
@ -109,6 +111,7 @@ class WorkflowAppGenerateTaskPipeline:
SystemVariableKey.WORKFLOW_ID: workflow.id,
SystemVariableKey.WORKFLOW_RUN_ID: application_generate_entity.workflow_run_id,
},
workflow_node_execution_repository=workflow_node_execution_repository,
)
self._application_generate_entity = application_generate_entity

@ -264,8 +264,16 @@ class QueueMessageReplaceEvent(AppQueueEvent):
QueueMessageReplaceEvent entity
"""
class MessageReplaceReason(StrEnum):
"""
Reason for message replace event
"""
OUTPUT_MODERATION = "output_moderation"
event: QueueEvent = QueueEvent.MESSAGE_REPLACE
text: str
reason: str
class QueueRetrieverResourcesEvent(AppQueueEvent):

@ -148,6 +148,7 @@ class MessageReplaceStreamResponse(StreamResponse):
event: StreamEvent = StreamEvent.MESSAGE_REPLACE
answer: str
reason: str
class AgentThoughtStreamResponse(StreamResponse):

@ -126,12 +126,12 @@ class BasedGenerateTaskPipeline:
if self._output_moderation_handler:
self._output_moderation_handler.stop_thread()
completion = self._output_moderation_handler.moderation_completion(
completion, flagged = self._output_moderation_handler.moderation_completion(
completion=completion, public_event=False
)
self._output_moderation_handler = None
return completion
if flagged:
return completion
return None

@ -182,10 +182,12 @@ class MessageCycleManage:
from_variable_selector=from_variable_selector,
)
def _message_replace_to_stream_response(self, answer: str) -> MessageReplaceStreamResponse:
def _message_replace_to_stream_response(self, answer: str, reason: str = "") -> MessageReplaceStreamResponse:
"""
Message replace to stream response.
:param answer: answer
:return:
"""
return MessageReplaceStreamResponse(task_id=self._application_generate_entity.task_id, answer=answer)
return MessageReplaceStreamResponse(
task_id=self._application_generate_entity.task_id, answer=answer, reason=reason
)

@ -6,7 +6,7 @@ from typing import Any, Optional, Union, cast
from uuid import uuid4
from sqlalchemy import func, select
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.orm import Session
from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom, WorkflowAppGenerateEntity
from core.app.entities.queue_entities import (
@ -49,14 +49,13 @@ from core.file import FILE_MODEL_IDENTITY, File
from core.model_runtime.utils.encoders import jsonable_encoder
from core.ops.entities.trace_entity import TraceTaskName
from core.ops.ops_trace_manager import TraceQueueManager, TraceTask
from core.repository import RepositoryFactory
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.tools.tool_manager import ToolManager
from core.workflow.entities.node_entities import NodeRunMetadataKey
from core.workflow.enums import SystemVariableKey
from core.workflow.nodes import NodeType
from core.workflow.nodes.tool.entities import ToolNodeData
from core.workflow.workflow_entry import WorkflowEntry
from extensions.ext_database import db
from models.account import Account
from models.enums import CreatedByRole, WorkflowRunTriggeredFrom
from models.model import EndUser
@ -76,26 +75,13 @@ class WorkflowCycleManage:
*,
application_generate_entity: Union[AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity],
workflow_system_variables: dict[SystemVariableKey, Any],
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
) -> None:
self._workflow_run: WorkflowRun | None = None
self._workflow_node_executions: dict[str, WorkflowNodeExecution] = {}
self._application_generate_entity = application_generate_entity
self._workflow_system_variables = workflow_system_variables
# Initialize the session factory and repository
# We use the global db engine instead of the session passed to methods
# Disable expire_on_commit to avoid the need for merging objects
self._session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
self._workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
params={
"tenant_id": self._application_generate_entity.app_config.tenant_id,
"app_id": self._application_generate_entity.app_config.app_id,
"session_factory": self._session_factory,
}
)
# We'll still keep the cache for backward compatibility and performance
# but use the repository for database operations
self._workflow_node_execution_repository = workflow_node_execution_repository
def _handle_workflow_run_start(
self,

@ -798,7 +798,25 @@ class ProviderConfiguration(BaseModel):
provider_models = [m for m in provider_models if m.status == ModelStatus.ACTIVE]
# resort provider_models
return sorted(provider_models, key=lambda x: x.model_type.value)
# Optimize sorting logic: first sort by provider.position order, then by model_type.value
# Get the position list for model types (retrieve only once for better performance)
model_type_positions = {}
if hasattr(self.provider, "position") and self.provider.position:
model_type_positions = self.provider.position
def get_sort_key(model: ModelWithProviderEntity):
# Get the position list for the current model type
positions = model_type_positions.get(model.model_type.value, [])
# If the model name is in the position list, use its index for sorting
# Otherwise use a large value (list length) to place undefined models at the end
position_index = positions.index(model.model) if model.model in positions else len(positions)
# Return composite sort key: (model_type value, model position index)
return (model.model_type.value, position_index)
# Sort using the composite sort key
return sorted(provider_models, key=get_sort_key)
def _get_system_provider_models(
self,

@ -1,7 +1,7 @@
# Written by YORKI MINAKO🤡, Edited by Xiaoyi
CONVERSATION_TITLE_PROMPT = """You need to decompose the user's input into "subject" and "intention" in order to accurately figure out what the user's input language actually is.
Notice: the language type user use could be diverse, which can be English, Chinese, Italian, Español, Arabic, Japanese, French, and etc.
MAKE SURE your output is the SAME language as the user's input!
Notice: the language type user uses could be diverse, which can be English, Chinese, Italian, Español, Arabic, Japanese, French, and etc.
ENSURE your output is in the SAME language as the user's input!
Your output is restricted only to: (Input language) Intention + Subject(short as possible)
Your output MUST be a valid JSON.
@ -19,7 +19,7 @@ User Input: hi, yesterday i had some burgers.
example 2:
User Input: hello
{
"Language Type": "The user's input is written in pure English",
"Language Type": "The user's input is pure English",
"Your Reasoning": "The language of my output must be pure English.",
"Your Output": "Greeting myself☺"
}
@ -46,7 +46,7 @@ example 5:
User Input: why小红的年龄is老than小明
{
"Language Type": "The user's input is English-Chinese mixed",
"Your Reasoning": "The English parts are subjective particles, the main intention is written in Chinese, besides, Chinese occupies a greater \"actual meaning\" than English, so the language of my output must be using Chinese.",
"Your Reasoning": "The English parts are filler words, the main intention is written in Chinese, besides, Chinese occupies a greater \"actual meaning\" than English, so the language of my output must be using Chinese.",
"Your Output": "询问小红和小明的年龄"
}
@ -114,6 +114,13 @@ JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE = (
"4. The returned object should contain at least one key-value pair.\n\n"
"5. The returned object should always be in the format: {result: ...}\n\n"
"Example:\n"
"/**\n"
" * Multiplies two numbers together.\n"
" *\n"
" * @param {number} arg1 - The first number to multiply.\n"
" * @param {number} arg2 - The second number to multiply.\n"
" * @returns {{ result: number }} The result of the multiplication.\n"
" */\n"
"function main(arg1, arg2) {\n"
" return {\n"
" result: arg1 * arg2\n"
@ -130,7 +137,7 @@ JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE = (
SUGGESTED_QUESTIONS_AFTER_ANSWER_INSTRUCTION_PROMPT = (
"Please help me predict the three most likely questions that human would ask, "
"and keeping each question under 20 characters.\n"
"and keep each question under 20 characters.\n"
"MAKE SURE your output is the SAME language as the Assistant's latest response. "
"The output must be an array in JSON format following the specified schema:\n"
'["question1","question2","question3"]\n'
@ -157,9 +164,9 @@ Here is a task description for which I would like you to create a high-quality p
</task_description>
Based on task description, please create a well-structured prompt template that another AI could use to consistently complete the task. The prompt template should include:
- Do not include <input> or <output> section and variables in the prompt, assume user will add them at their own will.
- Clear instructions for the AI that will be using this prompt, demarcated with <instructions> tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag.
- Clear instructions for the AI that will be using this prompt, demarcated with <instruction> tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag.
- Relevant examples if needed to clarify the task further, demarcated with <example> tags. Do not include variables in the prompt. Give three pairs of input and output examples.
- Include other relevant sections demarcated with appropriate XML tags like <examples>, <instructions>.
- Include other relevant sections demarcated with appropriate XML tags like <examples>, <instruction>.
- Use the same language as task description.
- Output in ``` xml ``` and start with <instruction>
Please generate the full prompt template with at least 300 words and output only the prompt template.
@ -172,7 +179,7 @@ Here is a task description for which I would like you to create a high-quality p
</task_description>
Based on task description, please create a well-structured prompt template that another AI could use to consistently complete the task. The prompt template should include:
- Descriptive variable names surrounded by {{ }} (two curly brackets) to indicate where the actual values will be substituted in. Choose variable names that clearly indicate the type of value expected. Variable names have to be composed of number, english alphabets and underline and nothing else.
- Clear instructions for the AI that will be using this prompt, demarcated with <instructions> tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag.
- Clear instructions for the AI that will be using this prompt, demarcated with <instruction> tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag.
- Relevant examples if needed to clarify the task further, demarcated with <example> tags. Do not use curly brackets any other than in <instruction> section.
- Any other relevant sections demarcated with appropriate XML tags like <input>, <output>, etc.
- Use the same language as task description.
@ -291,32 +298,30 @@ Your task is to convert simple user descriptions into properly formatted JSON Sc
{
"type": "object",
"properties": {
"properties": {
"songs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"id": {
"type": "string"
},
"duration": {
"type": "string"
},
"aritst": {
"type": "string"
}
"songs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"id": {
"type": "string"
},
"duration": {
"type": "string"
},
"required": [
"name",
"id",
"duration",
"aritst"
]
}
"aritst": {
"type": "string"
}
},
"required": [
"name",
"id",
"duration",
"aritst"
]
}
}
},

@ -134,6 +134,9 @@ class ProviderEntity(BaseModel):
# pydantic configs
model_config = ConfigDict(protected_namespaces=())
# position from plugin _position.yaml
position: Optional[dict[str, list[str]]] = {}
@field_validator("models", mode="before")
@classmethod
def validate_models(cls, v):

@ -46,14 +46,14 @@ class OutputModeration(BaseModel):
if not self.thread:
self.thread = self.start_thread()
def moderation_completion(self, completion: str, public_event: bool = False) -> str:
def moderation_completion(self, completion: str, public_event: bool = False) -> tuple[str, bool]:
self.buffer = completion
self.is_final_chunk = True
result = self.moderation(tenant_id=self.tenant_id, app_id=self.app_id, moderation_buffer=completion)
if not result or not result.flagged:
return completion
return completion, False
if result.action == ModerationAction.DIRECT_OUTPUT:
final_output = result.preset_response
@ -61,9 +61,14 @@ class OutputModeration(BaseModel):
final_output = result.text
if public_event:
self.queue_manager.publish(QueueMessageReplaceEvent(text=final_output), PublishFrom.TASK_PIPELINE)
self.queue_manager.publish(
QueueMessageReplaceEvent(
text=final_output, reason=QueueMessageReplaceEvent.MessageReplaceReason.OUTPUT_MODERATION
),
PublishFrom.TASK_PIPELINE,
)
return final_output
return final_output, True
def start_thread(self) -> threading.Thread:
buffer_size = dify_config.MODERATION_BUFFER_SIZE
@ -112,7 +117,12 @@ class OutputModeration(BaseModel):
# trigger replace event
if self.thread_running:
self.queue_manager.publish(QueueMessageReplaceEvent(text=final_output), PublishFrom.TASK_PIPELINE)
self.queue_manager.publish(
QueueMessageReplaceEvent(
text=final_output, reason=QueueMessageReplaceEvent.MessageReplaceReason.OUTPUT_MODERATION
),
PublishFrom.TASK_PIPELINE,
)
if result.action == ModerationAction.DIRECT_OUTPUT:
break

@ -32,6 +32,7 @@ class LindormVectorStoreConfig(BaseModel):
username: Optional[str] = None
password: Optional[str] = None
using_ugc: Optional[bool] = False
request_timeout: Optional[float] = 1.0 # timeout units: s
@model_validator(mode="before")
@classmethod
@ -251,9 +252,9 @@ class LindormVectorStore(BaseVector):
query = default_vector_search_query(query_vector=query_vector, k=top_k, filters=filters, **kwargs)
try:
params = {}
params = {"timeout": self._client_config.request_timeout}
if self._using_ugc:
params["routing"] = self._routing
params["routing"] = self._routing # type: ignore
response = self._client.search(index=self._collection_name, body=query, params=params)
except Exception:
logger.exception(f"Error executing vector search, query: {query}")
@ -304,8 +305,8 @@ class LindormVectorStore(BaseVector):
routing=routing,
routing_field=self._routing_field,
)
response = self._client.search(index=self._collection_name, body=full_text_query)
params = {"timeout": self._client_config.request_timeout}
response = self._client.search(index=self._collection_name, body=full_text_query, params=params)
docs = []
for hit in response["hits"]["hits"]:
docs.append(
@ -554,6 +555,7 @@ class LindormVectorStoreFactory(AbstractVectorFactory):
username=dify_config.LINDORM_USERNAME,
password=dify_config.LINDORM_PASSWORD,
using_ugc=dify_config.USING_UGC_INDEX,
request_timeout=dify_config.LINDORM_QUERY_TIMEOUT,
)
using_ugc = dify_config.USING_UGC_INDEX
if using_ugc is None:

@ -0,0 +1,243 @@
import json
import uuid
from contextlib import contextmanager
from typing import Any
import psycopg2.extras # type: ignore
import psycopg2.pool # type: ignore
from pydantic import BaseModel, model_validator
from configs import dify_config
from core.rag.datasource.vdb.vector_base import BaseVector
from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory
from core.rag.datasource.vdb.vector_type import VectorType
from core.rag.embedding.embedding_base import Embeddings
from core.rag.models.document import Document
from extensions.ext_redis import redis_client
from models.dataset import Dataset
class VastbaseVectorConfig(BaseModel):
host: str
port: int
user: str
password: str
database: str
min_connection: int
max_connection: int
@model_validator(mode="before")
@classmethod
def validate_config(cls, values: dict) -> dict:
if not values["host"]:
raise ValueError("config VASTBASE_HOST is required")
if not values["port"]:
raise ValueError("config VASTBASE_PORT is required")
if not values["user"]:
raise ValueError("config VASTBASE_USER is required")
if not values["password"]:
raise ValueError("config VASTBASE_PASSWORD is required")
if not values["database"]:
raise ValueError("config VASTBASE_DATABASE is required")
if not values["min_connection"]:
raise ValueError("config VASTBASE_MIN_CONNECTION is required")
if not values["max_connection"]:
raise ValueError("config VASTBASE_MAX_CONNECTION is required")
if values["min_connection"] > values["max_connection"]:
raise ValueError("config VASTBASE_MIN_CONNECTION should less than VASTBASE_MAX_CONNECTION")
return values
SQL_CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS {table_name} (
id UUID PRIMARY KEY,
text TEXT NOT NULL,
meta JSONB NOT NULL,
embedding floatvector({dimension}) NOT NULL
);
"""
SQL_CREATE_INDEX = """
CREATE INDEX IF NOT EXISTS embedding_cosine_v1_idx ON {table_name}
USING hnsw (embedding floatvector_cosine_ops) WITH (m = 16, ef_construction = 64);
"""
class VastbaseVector(BaseVector):
def __init__(self, collection_name: str, config: VastbaseVectorConfig):
super().__init__(collection_name)
self.pool = self._create_connection_pool(config)
self.table_name = f"embedding_{collection_name}"
def get_type(self) -> str:
return VectorType.VASTBASE
def _create_connection_pool(self, config: VastbaseVectorConfig):
return psycopg2.pool.SimpleConnectionPool(
config.min_connection,
config.max_connection,
host=config.host,
port=config.port,
user=config.user,
password=config.password,
database=config.database,
)
@contextmanager
def _get_cursor(self):
conn = self.pool.getconn()
cur = conn.cursor()
try:
yield cur
finally:
cur.close()
conn.commit()
self.pool.putconn(conn)
def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs):
dimension = len(embeddings[0])
self._create_collection(dimension)
return self.add_texts(texts, embeddings)
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
values = []
pks = []
for i, doc in enumerate(documents):
if doc.metadata is not None:
doc_id = doc.metadata.get("doc_id", str(uuid.uuid4()))
pks.append(doc_id)
values.append(
(
doc_id,
doc.page_content,
json.dumps(doc.metadata),
embeddings[i],
)
)
with self._get_cursor() as cur:
psycopg2.extras.execute_values(
cur, f"INSERT INTO {self.table_name} (id, text, meta, embedding) VALUES %s", values
)
return pks
def text_exists(self, id: str) -> bool:
with self._get_cursor() as cur:
cur.execute(f"SELECT id FROM {self.table_name} WHERE id = %s", (id,))
return cur.fetchone() is not None
def get_by_ids(self, ids: list[str]) -> list[Document]:
with self._get_cursor() as cur:
cur.execute(f"SELECT meta, text FROM {self.table_name} WHERE id IN %s", (tuple(ids),))
docs = []
for record in cur:
docs.append(Document(page_content=record[1], metadata=record[0]))
return docs
def delete_by_ids(self, ids: list[str]) -> None:
# Avoiding crashes caused by performing delete operations on empty lists in certain scenarios
# Scenario 1: extract a document fails, resulting in a table not being created.
# Then clicking the retry button triggers a delete operation on an empty list.
if not ids:
return
with self._get_cursor() as cur:
cur.execute(f"DELETE FROM {self.table_name} WHERE id IN %s", (tuple(ids),))
def delete_by_metadata_field(self, key: str, value: str) -> None:
with self._get_cursor() as cur:
cur.execute(f"DELETE FROM {self.table_name} WHERE meta->>%s = %s", (key, value))
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
"""
Search the nearest neighbors to a vector.
:param query_vector: The input vector to search for similar items.
:param top_k: The number of nearest neighbors to return, default is 5.
:return: List of Documents that are nearest to the query vector.
"""
top_k = kwargs.get("top_k", 4)
if not isinstance(top_k, int) or top_k <= 0:
raise ValueError("top_k must be a positive integer")
with self._get_cursor() as cur:
cur.execute(
f"SELECT meta, text, embedding <=> %s AS distance FROM {self.table_name}"
f" ORDER BY distance LIMIT {top_k}",
(json.dumps(query_vector),),
)
docs = []
score_threshold = float(kwargs.get("score_threshold") or 0.0)
for record in cur:
metadata, text, distance = record
score = 1 - distance
metadata["score"] = score
if score > score_threshold:
docs.append(Document(page_content=text, metadata=metadata))
return docs
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
top_k = kwargs.get("top_k", 5)
if not isinstance(top_k, int) or top_k <= 0:
raise ValueError("top_k must be a positive integer")
with self._get_cursor() as cur:
cur.execute(
f"""SELECT meta, text, ts_rank(to_tsvector(coalesce(text, '')), plainto_tsquery(%s)) AS score
FROM {self.table_name}
WHERE to_tsvector(text) @@ plainto_tsquery(%s)
ORDER BY score DESC
LIMIT {top_k}""",
# f"'{query}'" is required in order to account for whitespace in query
(f"'{query}'", f"'{query}'"),
)
docs = []
for record in cur:
metadata, text, score = record
metadata["score"] = score
docs.append(Document(page_content=text, metadata=metadata))
return docs
def delete(self) -> None:
with self._get_cursor() as cur:
cur.execute(f"DROP TABLE IF EXISTS {self.table_name}")
def _create_collection(self, dimension: int):
cache_key = f"vector_indexing_{self._collection_name}"
lock_name = f"{cache_key}_lock"
with redis_client.lock(lock_name, timeout=20):
collection_exist_cache_key = f"vector_indexing_{self._collection_name}"
if redis_client.get(collection_exist_cache_key):
return
with self._get_cursor() as cur:
cur.execute(SQL_CREATE_TABLE.format(table_name=self.table_name, dimension=dimension))
# Vastbase 支持的向量维度取值范围为 [1,16000]
if dimension <= 16000:
cur.execute(SQL_CREATE_INDEX.format(table_name=self.table_name))
redis_client.set(collection_exist_cache_key, 1, ex=3600)
class VastbaseVectorFactory(AbstractVectorFactory):
def init_vector(self, dataset: Dataset, attributes: list, embeddings: Embeddings) -> VastbaseVector:
if dataset.index_struct_dict:
class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"]
collection_name = class_prefix
else:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.VASTBASE, collection_name))
return VastbaseVector(
collection_name=collection_name,
config=VastbaseVectorConfig(
host=dify_config.VASTBASE_HOST or "localhost",
port=dify_config.VASTBASE_PORT,
user=dify_config.VASTBASE_USER or "dify",
password=dify_config.VASTBASE_PASSWORD or "",
database=dify_config.VASTBASE_DATABASE or "dify",
min_connection=dify_config.VASTBASE_MIN_CONNECTION,
max_connection=dify_config.VASTBASE_MAX_CONNECTION,
),
)

@ -74,6 +74,10 @@ class Vector:
from core.rag.datasource.vdb.pgvector.pgvector import PGVectorFactory
return PGVectorFactory
case VectorType.VASTBASE:
from core.rag.datasource.vdb.pyvastbase.vastbase_vector import VastbaseVectorFactory
return VastbaseVectorFactory
case VectorType.PGVECTO_RS:
from core.rag.datasource.vdb.pgvecto_rs.pgvecto_rs import PGVectoRSFactory

@ -7,7 +7,9 @@ class VectorType(StrEnum):
MILVUS = "milvus"
MYSCALE = "myscale"
PGVECTOR = "pgvector"
VASTBASE = "vastbase"
PGVECTO_RS = "pgvecto-rs"
QDRANT = "qdrant"
RELYT = "relyt"
TIDB_VECTOR = "tidb_vector"

@ -35,8 +35,9 @@ class BuiltinToolProviderController(ToolProviderController):
provider_yaml["credentials_for_provider"][credential_name]["name"] = credential_name
credentials_schema = []
for credential in provider_yaml.get("credentials_for_provider", {}).values():
credentials_schema.append(credential)
for credential in provider_yaml.get("credentials_for_provider", {}):
credential_dict = provider_yaml.get("credentials_for_provider", {}).get(credential, {})
credentials_schema.append(credential_dict)
super().__init__(
entity=ToolProviderEntity(

@ -19,3 +19,9 @@ paginated_conversation_variable_fields = {
"has_more": fields.Boolean,
"data": fields.List(fields.Nested(conversation_variable_fields), attribute="data"),
}
conversation_variable_infinite_scroll_pagination_fields = {
"limit": fields.Integer,
"has_more": fields.Boolean,
"data": fields.List(fields.Nested(conversation_variable_fields)),
}

@ -3,8 +3,8 @@ import re
import uuid
from collections.abc import Mapping
from datetime import datetime
from enum import Enum
from typing import TYPE_CHECKING, Optional
from enum import Enum, StrEnum
from typing import TYPE_CHECKING, Any, Literal, Optional, cast
from core.plugin.entities.plugin import GenericProviderID
from core.tools.entities.tool_entities import ToolProviderType
@ -14,9 +14,6 @@ from services.plugin.plugin_service import PluginService
if TYPE_CHECKING:
from models.workflow import Workflow
from enum import StrEnum
from typing import TYPE_CHECKING, Any, Literal, cast
import sqlalchemy as sa
from flask import request
from flask_login import UserMixin # type: ignore

@ -1,14 +1,12 @@
import json
from collections.abc import Mapping, Sequence
from datetime import UTC, datetime
from enum import Enum
from enum import Enum, StrEnum
from typing import TYPE_CHECKING, Any, Optional, Self, Union
from uuid import uuid4
if TYPE_CHECKING:
from models.model import AppMode
from enum import StrEnum
from typing import TYPE_CHECKING
import sqlalchemy as sa
from sqlalchemy import Index, PrimaryKeyConstraint, func

@ -1,6 +1,6 @@
[project]
name = "dify-api"
version = "1.2.0"
version = "1.3.0"
requires-python = ">=3.11,<3.13"
dependencies = [

@ -40,7 +40,7 @@ IMPORT_INFO_REDIS_KEY_PREFIX = "app_import_info:"
CHECK_DEPENDENCIES_REDIS_KEY_PREFIX = "app_check_dependencies:"
IMPORT_INFO_REDIS_EXPIRY = 10 * 60 # 10 minutes
DSL_MAX_SIZE = 10 * 1024 * 1024 # 10MB
CURRENT_DSL_VERSION = "0.1.5"
CURRENT_DSL_VERSION = "0.2.0"
class ImportMode(StrEnum):
@ -77,13 +77,19 @@ def _check_version_compatibility(imported_version: str) -> ImportStatus:
except version.InvalidVersion:
return ImportStatus.FAILED
# Compare major version and minor version
if current_ver.major != imported_ver.major or current_ver.minor != imported_ver.minor:
# If imported version is newer than current, always return PENDING
if imported_ver > current_ver:
return ImportStatus.PENDING
if current_ver.micro != imported_ver.micro:
# If imported version is older than current's major, return PENDING
if imported_ver.major < current_ver.major:
return ImportStatus.PENDING
# If imported version is older than current's minor, return COMPLETED_WITH_WARNINGS
if imported_ver.minor < current_ver.minor:
return ImportStatus.COMPLETED_WITH_WARNINGS
# If imported version equals or is older than current's micro, return COMPLETED
return ImportStatus.COMPLETED

@ -9,9 +9,14 @@ from core.app.entities.app_invoke_entities import InvokeFrom
from core.llm_generator.llm_generator import LLMGenerator
from extensions.ext_database import db
from libs.infinite_scroll_pagination import InfiniteScrollPagination
from models import ConversationVariable
from models.account import Account
from models.model import App, Conversation, EndUser, Message
from services.errors.conversation import ConversationNotExistsError, LastConversationNotExistsError
from services.errors.conversation import (
ConversationNotExistsError,
ConversationVariableNotExistsError,
LastConversationNotExistsError,
)
from services.errors.message import MessageNotExistsError
@ -166,3 +171,50 @@ class ConversationService:
conversation.is_deleted = True
conversation.updated_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit()
@classmethod
def get_conversational_variable(
cls,
app_model: App,
conversation_id: str,
user: Optional[Union[Account, EndUser]],
limit: int,
last_id: Optional[str],
) -> InfiniteScrollPagination:
conversation = cls.get_conversation(app_model, conversation_id, user)
stmt = (
select(ConversationVariable)
.where(ConversationVariable.app_id == app_model.id)
.where(ConversationVariable.conversation_id == conversation.id)
.order_by(ConversationVariable.created_at)
)
with Session(db.engine) as session:
if last_id:
last_variable = session.scalar(stmt.where(ConversationVariable.id == last_id))
if not last_variable:
raise ConversationVariableNotExistsError()
# Filter for variables created after the last_id
stmt = stmt.where(ConversationVariable.created_at > last_variable.created_at)
# Apply limit to query
query_stmt = stmt.limit(limit) # Get one extra to check if there are more
rows = session.scalars(query_stmt).all()
has_more = False
if len(rows) > limit:
has_more = True
rows = rows[:limit] # Remove the extra item
variables = [
{
"created_at": row.created_at,
"updated_at": row.updated_at,
**row.to_variable().model_dump(),
}
for row in rows
]
return InfiniteScrollPagination(variables, limit, has_more)

@ -11,3 +11,7 @@ class ConversationNotExistsError(BaseServiceError):
class ConversationCompletedError(Exception):
pass
class ConversationVariableNotExistsError(BaseServiceError):
pass

@ -0,0 +1,27 @@
from core.rag.datasource.vdb.pyvastbase.vastbase_vector import VastbaseVector, VastbaseVectorConfig
from tests.integration_tests.vdb.test_vector_store import (
AbstractVectorTest,
get_example_text,
setup_mock_redis,
)
class VastbaseVectorTest(AbstractVectorTest):
def __init__(self):
super().__init__()
self.vector = VastbaseVector(
collection_name=self.collection_name,
config=VastbaseVectorConfig(
host="localhost",
port=5434,
user="dify",
password="Difyai123456",
database="dify",
min_connection=1,
max_connection=5,
),
)
def test_vastbase_vector(setup_mock_redis):
VastbaseVectorTest().run_all_tests()

@ -1148,7 +1148,7 @@ wheels = [
[[package]]
name = "dify-api"
version = "1.2.0"
version = "1.3.0"
source = { virtual = "." }
dependencies = [
{ name = "authlib" },

@ -406,6 +406,7 @@ QDRANT_GRPC_PORT=6334
# Milvus configuration. Only available when VECTOR_STORE is `milvus`.
# The milvus uri.
MILVUS_URI=http://host.docker.internal:19530
MILVUS_DATABASE=
MILVUS_TOKEN=
MILVUS_USER=
MILVUS_PASSWORD=
@ -441,6 +442,15 @@ PGVECTOR_MAX_CONNECTION=5
PGVECTOR_PG_BIGM=false
PGVECTOR_PG_BIGM_VERSION=1.2-20240606
# vastbase configurations, only available when VECTOR_STORE is `vastbase`
VASTBASE_HOST=vastbase
VASTBASE_PORT=5432
VASTBASE_USER=dify
VASTBASE_PASSWORD=Difyai123456
VASTBASE_DATABASE=dify
VASTBASE_MIN_CONNECTION=1
VASTBASE_MAX_CONNECTION=5
# pgvecto-rs configurations, only available when VECTOR_STORE is `pgvecto-rs`
PGVECTO_RS_HOST=pgvecto-rs
PGVECTO_RS_PORT=5432
@ -553,6 +563,7 @@ VIKINGDB_SOCKET_TIMEOUT=30
LINDORM_URL=http://lindorm:30070
LINDORM_USERNAME=lindorm
LINDORM_PASSWORD=lindorm
LINDORM_QUERY_TIMEOUT=1
# OceanBase Vector configuration, only available when VECTOR_STORE is `oceanbase`
OCEANBASE_VECTOR_HOST=oceanbase

@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.2.0
image: langgenius/dify-api:1.3.0
restart: always
environment:
# Use the shared environment variables.
@ -31,7 +31,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.2.0
image: langgenius/dify-api:1.3.0
restart: always
environment:
# Use the shared environment variables.
@ -57,7 +57,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.2.0
image: langgenius/dify-web:1.3.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@ -142,7 +142,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.7-local
image: langgenius/dify-plugin-daemon:0.0.9-local
restart: always
environment:
# Use the shared environment variables.
@ -363,6 +363,30 @@ services:
timeout: 3s
retries: 30
# get image from https://www.vastdata.com.cn/
vastbase:
image: vastdata/vastbase-vector
profiles:
- vastbase
restart: always
environment:
- VB_DBCOMPATIBILITY=PG
- VB_DB=dify
- VB_USERNAME=dify
- VB_PASSWORD=Difyai123456
ports:
- '5434:5432'
volumes:
- ./vastbase/lic:/home/vastbase/vastbase/lic
- ./vastbase/data:/home/vastbase/data
- ./vastbase/backup:/home/vastbase/backup
- ./vastbase/backup_log:/home/vastbase/backup_log
healthcheck:
test: [ 'CMD', 'pg_isready' ]
interval: 1s
timeout: 3s
retries: 30
# pgvecto-rs vector store
pgvecto-rs:
image: tensorchord/pgvecto-rs:pg16-v0.3.0

@ -71,7 +71,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.7-local
image: langgenius/dify-plugin-daemon:0.0.9-local
restart: always
env_file:
- ./middleware.env

@ -138,6 +138,7 @@ x-shared-env: &shared-api-worker-env
QDRANT_GRPC_ENABLED: ${QDRANT_GRPC_ENABLED:-false}
QDRANT_GRPC_PORT: ${QDRANT_GRPC_PORT:-6334}
MILVUS_URI: ${MILVUS_URI:-http://host.docker.internal:19530}
MILVUS_DATABASE: ${MILVUS_DATABASE:-}
MILVUS_TOKEN: ${MILVUS_TOKEN:-}
MILVUS_USER: ${MILVUS_USER:-}
MILVUS_PASSWORD: ${MILVUS_PASSWORD:-}
@ -163,6 +164,13 @@ x-shared-env: &shared-api-worker-env
PGVECTOR_MAX_CONNECTION: ${PGVECTOR_MAX_CONNECTION:-5}
PGVECTOR_PG_BIGM: ${PGVECTOR_PG_BIGM:-false}
PGVECTOR_PG_BIGM_VERSION: ${PGVECTOR_PG_BIGM_VERSION:-1.2-20240606}
VASTBASE_HOST: ${VASTBASE_HOST:-vastbase}
VASTBASE_PORT: ${VASTBASE_PORT:-5432}
VASTBASE_USER: ${VASTBASE_USER:-dify}
VASTBASE_PASSWORD: ${VASTBASE_PASSWORD:-Difyai123456}
VASTBASE_DATABASE: ${VASTBASE_DATABASE:-dify}
VASTBASE_MIN_CONNECTION: ${VASTBASE_MIN_CONNECTION:-1}
VASTBASE_MAX_CONNECTION: ${VASTBASE_MAX_CONNECTION:-5}
PGVECTO_RS_HOST: ${PGVECTO_RS_HOST:-pgvecto-rs}
PGVECTO_RS_PORT: ${PGVECTO_RS_PORT:-5432}
PGVECTO_RS_USER: ${PGVECTO_RS_USER:-postgres}
@ -250,6 +258,7 @@ x-shared-env: &shared-api-worker-env
LINDORM_URL: ${LINDORM_URL:-http://lindorm:30070}
LINDORM_USERNAME: ${LINDORM_USERNAME:-lindorm}
LINDORM_PASSWORD: ${LINDORM_PASSWORD:-lindorm}
LINDORM_QUERY_TIMEOUT: ${LINDORM_QUERY_TIMEOUT:-1}
OCEANBASE_VECTOR_HOST: ${OCEANBASE_VECTOR_HOST:-oceanbase}
OCEANBASE_VECTOR_PORT: ${OCEANBASE_VECTOR_PORT:-2881}
OCEANBASE_VECTOR_USER: ${OCEANBASE_VECTOR_USER:-root@test}
@ -479,7 +488,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.2.0
image: langgenius/dify-api:1.3.0
restart: always
environment:
# Use the shared environment variables.
@ -508,7 +517,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.2.0
image: langgenius/dify-api:1.3.0
restart: always
environment:
# Use the shared environment variables.
@ -534,7 +543,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.2.0
image: langgenius/dify-web:1.3.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@ -619,7 +628,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.7-local
image: langgenius/dify-plugin-daemon:0.0.9-local
restart: always
environment:
# Use the shared environment variables.
@ -840,6 +849,30 @@ services:
timeout: 3s
retries: 30
# get image from https://www.vastdata.com.cn/
vastbase:
image: vastdata/vastbase-vector
profiles:
- vastbase
restart: always
environment:
- VB_DBCOMPATIBILITY=PG
- VB_DB=dify
- VB_USERNAME=dify
- VB_PASSWORD=Difyai123456
ports:
- '5434:5432'
volumes:
- ./vastbase/lic:/home/vastbase/vastbase/lic
- ./vastbase/data:/home/vastbase/data
- ./vastbase/backup:/home/vastbase/backup
- ./vastbase/backup_log:/home/vastbase/backup_log
healthcheck:
test: [ 'CMD', 'pg_isready' ]
interval: 1s
timeout: 3s
retries: 30
# pgvecto-rs vector store
pgvecto-rs:
image: tensorchord/pgvecto-rs:pg16-v0.3.0

@ -0,0 +1 @@
/vendor

@ -9,6 +9,21 @@ This is the PHP SDK for the Dify API, which allows you to easily integrate Dify
## Usage
If you want to try the example, you can run `composer install` in this directory.
In exist project, copy the `dify-client.php` to you project, and merge the following to your `composer.json` file, then run `composer install && composer dump-autoload` to install. Guzzle does not require 7.9, other versions have not been tested, but you can try.
```json
{
"require": {
"guzzlehttp/guzzle": "^7.9"
},
"autoload": {
"files": ["path/to/dify-client.php"]
}
}
```
After installing the SDK, you can use it in your project like this:
```php
@ -16,10 +31,6 @@ After installing the SDK, you can use it in your project like this:
require 'vendor/autoload.php';
use YourVendorName\DifyPHP\DifyClient;
use YourVendorName\DifyPHP\CompletionClient;
use YourVendorName\DifyPHP\ChatClient;
$apiKey = 'your-api-key-here';
$difyClient = new DifyClient($apiKey);

@ -0,0 +1,9 @@
{
"require": {
"php": ">=7.2",
"guzzlehttp/guzzle": "^7.9"
},
"autoload": {
"files": ["dify-client.php"]
}
}

@ -0,0 +1,663 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7827c548fdcc7e87cb0ae341dd2c6b1b",
"packages": [
{
"name": "guzzlehttp/guzzle",
"version": "7.9.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "d281ed313b989f213357e3be1a179f02196ac99b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b",
"reference": "d281ed313b989f213357e3be1a179f02196ac99b",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.5.3 || ^2.0.3",
"guzzlehttp/psr7": "^2.7.0",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*",
"guzzle/client-integration-tests": "3.0.2",
"php-http/message-factory": "^1.1",
"phpunit/phpunit": "^8.5.39 || ^9.6.20",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Jeremy Lindblom",
"email": "jeremeamia@gmail.com",
"homepage": "https://github.com/jeremeamia"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle is a PHP HTTP client library",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"psr-18",
"psr-7",
"rest",
"web service"
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.9.2"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
"type": "tidelift"
}
],
"time": "2024-07-24T11:22:20+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c",
"reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.39 || ^9.6.20"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.2.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
"type": "tidelift"
}
],
"time": "2025-03-27T13:27:01+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.7.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16",
"reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.2.5 || ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"ralouphie/getallheaders": "^3.0"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0",
"phpunit/phpunit": "^8.5.39 || ^9.6.20"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.7.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
"type": "tidelift"
}
],
"time": "2025-03-27T12:30:47+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
"homepage": "https://github.com/php-fig/http-client",
"keywords": [
"http",
"http-client",
"psr",
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client"
},
"time": "2023-09-23T14:17:50+00:00"
},
{
"name": "psr/http-factory",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "e616d01114759c4c489f93b099585439f795fe35"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
"reference": "e616d01114759c4c489f93b099585439f795fe35",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-factory/tree/1.0.2"
},
"time": "2023-04-10T20:10:41+00:00"
},
{
"name": "psr/http-message",
"version": "2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

@ -1,7 +1,5 @@
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
class DifyClient {
@ -11,7 +9,7 @@ class DifyClient {
public function __construct($api_key, $base_url = null) {
$this->api_key = $api_key;
$this->base_url = $base_url ?? "https://api.dify.ai/v1/";
$this->base_url = $base_url ?? 'https://api.dify.ai/v1/';
$this->client = new Client([
'base_uri' => $this->base_url,
'headers' => [
@ -19,13 +17,6 @@ class DifyClient {
'Content-Type' => 'application/json',
],
]);
$this->file_client = new Client([
'base_uri' => $this->base_url,
'headers' => [
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'multipart/form-data',
],
]);
}
protected function send_request($method, $endpoint, $data = null, $params = null, $stream = false) {
@ -58,7 +49,7 @@ class DifyClient {
'multipart' => $this->prepareMultipart($data, $files)
];
return $this->file_client->request('POST', 'files/upload', $options);
return $this->client->request('POST', 'files/upload', $options);
}
protected function prepareMultipart($data, $files) {
@ -132,7 +123,7 @@ class ChatClient extends DifyClient {
public function get_suggestions($message_id, $user) {
$params = [
'user' => $user
]
];
return $this->send_request('GET', "messages/{$message_id}/suggested", null, $params);
}
@ -188,10 +179,9 @@ class ChatClient extends DifyClient {
'user' => $user,
];
$options = [
'multipart' => $this->prepareMultipart($data, $files)
'multipart' => $this->prepareMultipart($data, $audio_file)
];
return $this->file_client->request('POST', 'audio-to-text', $options);
return $this->client->request('POST', 'audio-to-text', $options);
}
}

@ -14,6 +14,7 @@ import Loading from '@/app/components/base/loading'
import Badge from '@/app/components/base/badge'
import { useKnowledge } from '@/hooks/use-knowledge'
import cn from '@/utils/classnames'
import { basePath } from '@/utils/var'
export type ISelectDataSetProps = {
isShow: boolean
@ -111,7 +112,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
}}
>
<span className='text-text-tertiary'>{t('appDebug.feature.dataSet.noDataSet')}</span>
<Link href="/datasets/create" className='font-normal text-text-accent'>{t('appDebug.feature.dataSet.toCreate')}</Link>
<Link href={`${basePath}/datasets/create`} className='font-normal text-text-accent'>{t('appDebug.feature.dataSet.toCreate')}</Link>
</div>
)}

@ -29,7 +29,7 @@ const OPTION_MAP = {
iframe: {
getContent: (url: string, token: string) =>
`<iframe
src="${url}${basePath}/chat/${token}"
src="${url}${basePath}/chatbot/${token}"
style="width: 100%; height: 100%; min-height: 700px"
frameborder="0"
allow="microphone">
@ -48,6 +48,7 @@ const OPTION_MAP = {
: ''},
systemVariables: {
// user_id: 'YOU CAN DEFINE USER ID HERE',
// conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID',
},
}
</script>

@ -39,6 +39,7 @@ export type EmbeddedChatbotContextValue = {
chatShouldReloadKey: string
isMobile: boolean
isInstalledApp: boolean
allowResetChat: boolean
appId?: string
handleFeedback: (messageId: string, feedback: Feedback) => void
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
@ -67,6 +68,7 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
chatShouldReloadKey: '',
isMobile: false,
isInstalledApp: false,
allowResetChat: true,
handleFeedback: noop,
currentChatInstanceRef: { current: { handleStop: noop } },
clearChatList: false,

@ -16,6 +16,7 @@ import cn from '@/utils/classnames'
export type IHeaderProps = {
isMobile?: boolean
allowResetChat?: boolean
customerIcon?: React.ReactNode
title: string
theme?: Theme
@ -23,6 +24,7 @@ export type IHeaderProps = {
}
const Header: FC<IHeaderProps> = ({
isMobile,
allowResetChat,
customerIcon,
title,
theme,
@ -57,7 +59,7 @@ const Header: FC<IHeaderProps> = ({
{currentConversationId && (
<Divider type='vertical' className='h-3.5' />
)}
{currentConversationId && (
{currentConversationId && allowResetChat && (
<Tooltip
popupContent={t('share.chat.resetChat')}
>
@ -89,7 +91,7 @@ const Header: FC<IHeaderProps> = ({
</div>
</div>
<div className='flex items-center gap-1'>
{currentConversationId && (
{currentConversationId && allowResetChat && (
<Tooltip
popupContent={t('share.chat.resetChat')}
>

@ -73,9 +73,11 @@ export const useEmbeddedChatbot = () => {
const appId = useMemo(() => appData?.app_id, [appData])
const [userId, setUserId] = useState<string>()
const [conversationId, setConversationId] = useState<string>()
useEffect(() => {
getProcessedSystemVariablesFromUrlParams().then(({ user_id }) => {
getProcessedSystemVariablesFromUrlParams().then(({ user_id, conversation_id }) => {
setUserId(user_id)
setConversationId(conversation_id)
})
}, [])
@ -109,7 +111,9 @@ export const useEmbeddedChatbot = () => {
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
defaultValue: {},
})
const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || '', [appId, conversationIdInfo, userId])
const allowResetChat = !conversationId
const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '',
[appId, conversationIdInfo, userId, conversationId])
const handleConversationIdInfoChange = useCallback((changeConversationId: string) => {
if (appId) {
let prevValue = conversationIdInfo?.[appId || '']
@ -362,6 +366,7 @@ export const useEmbeddedChatbot = () => {
appInfoError,
appInfoLoading,
isInstalledApp,
allowResetChat,
appId,
currentConversationId,
currentConversationItem,

@ -25,6 +25,7 @@ import cn from '@/utils/classnames'
const Chatbot = () => {
const {
isMobile,
allowResetChat,
appInfoError,
appInfoLoading,
appData,
@ -90,6 +91,7 @@ const Chatbot = () => {
>
<Header
isMobile={isMobile}
allowResetChat={allowResetChat}
title={site?.title || ''}
customerIcon={isDify() ? difyIcon : ''}
theme={themeBuilder?.theme}
@ -153,6 +155,7 @@ const EmbeddedChatbotWrapper = () => {
handleNewConversationCompleted,
chatShouldReloadKey,
isInstalledApp,
allowResetChat,
appId,
handleFeedback,
currentChatInstanceRef,
@ -187,6 +190,7 @@ const EmbeddedChatbotWrapper = () => {
chatShouldReloadKey,
isMobile,
isInstalledApp,
allowResetChat,
appId,
handleFeedback,
currentChatInstanceRef,

@ -1,4 +1,5 @@
import type { FC } from 'react'
import { basePath } from '@/utils/var'
type LogoEmbeddedChatAvatarProps = {
className?: string
@ -8,7 +9,7 @@ const LogoEmbeddedChatAvatar: FC<LogoEmbeddedChatAvatarProps> = ({
}) => {
return (
<img
src='/logo/logo-embedded-chat-avatar.png'
src={`${basePath}/logo/logo-embedded-chat-avatar.png`}
className={`block h-10 w-10 ${className}`}
alt='logo'
/>

@ -144,7 +144,7 @@ const WorkflowVariableBlockComponent = ({
}
if (!node)
return null
return Item
return (
<Tooltip

@ -68,7 +68,7 @@ const Toast = ({
</div>
<div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} grow flex-col items-start gap-1`}>
<div className='flex items-center gap-1'>
<div className='system-sm-semibold text-text-primary'>{message}</div>
<div className='system-sm-semibold text-text-primary [word-break:break-word]'>{message}</div>
{customComponent}
</div>
{children && <div className='system-xs-regular text-text-secondary'>

@ -1,3 +1,4 @@
import type { BasicPlan } from '@/app/components/billing/type'
import { Plan, type PlanInfo, Priority } from '@/app/components/billing/type'
const supportModelProviders = 'OpenAI/Anthropic/Llama2/Azure OpenAI/Hugging Face/Replicate'
@ -10,7 +11,7 @@ export const contactSalesUrl = 'https://vikgc6bnu1s.typeform.com/dify-business'
export const getStartedWithCommunityUrl = 'https://github.com/langgenius/dify'
export const getWithPremiumUrl = 'https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6'
export const ALL_PLANS: Record<Plan, PlanInfo> = {
export const ALL_PLANS: Record<BasicPlan, PlanInfo> = {
sandbox: {
level: 1,
price: 0,
@ -22,6 +23,7 @@ export const ALL_PLANS: Record<Plan, PlanInfo> = {
vectorSpace: '50MB',
documentsUploadQuota: 0,
documentsRequestQuota: 10,
apiRateLimit: 5000,
documentProcessingPriority: Priority.standard,
messageRequest: 200,
annotatedResponse: 10,
@ -38,6 +40,7 @@ export const ALL_PLANS: Record<Plan, PlanInfo> = {
vectorSpace: '5GB',
documentsUploadQuota: 0,
documentsRequestQuota: 100,
apiRateLimit: NUM_INFINITE,
documentProcessingPriority: Priority.priority,
messageRequest: 5000,
annotatedResponse: 2000,
@ -54,6 +57,7 @@ export const ALL_PLANS: Record<Plan, PlanInfo> = {
vectorSpace: '20GB',
documentsUploadQuota: 0,
documentsRequestQuota: 1000,
apiRateLimit: NUM_INFINITE,
documentProcessingPriority: Priority.topPriority,
messageRequest: 10000,
annotatedResponse: 5000,
@ -62,7 +66,7 @@ export const ALL_PLANS: Record<Plan, PlanInfo> = {
}
export const defaultPlan = {
type: Plan.sandbox,
type: Plan.sandbox as BasicPlan,
usage: {
documents: 50,
vectorSpace: 1,

@ -2,7 +2,8 @@
import type { FC, ReactNode } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiApps2Line, RiBook2Line, RiBrain2Line, RiChatAiLine, RiFileEditLine, RiFolder6Line, RiGroupLine, RiHardDrive3Line, RiHistoryLine, RiProgress3Line, RiQuestionLine, RiSeoLine } from '@remixicon/react'
import { RiApps2Line, RiBook2Line, RiBrain2Line, RiChatAiLine, RiFileEditLine, RiFolder6Line, RiGroupLine, RiHardDrive3Line, RiHistoryLine, RiProgress3Line, RiQuestionLine, RiSeoLine, RiTerminalBoxLine } from '@remixicon/react'
import type { BasicPlan } from '../type'
import { Plan } from '../type'
import { ALL_PLANS, NUM_INFINITE } from '../config'
import Toast from '../../base/toast'
@ -15,8 +16,8 @@ import { useAppContext } from '@/context/app-context'
import { fetchSubscriptionUrls } from '@/service/billing'
type Props = {
currentPlan: Plan
plan: Plan
currentPlan: BasicPlan
plan: BasicPlan
planRange: PlanRange
canPay: boolean
}
@ -127,8 +128,8 @@ const PlanItem: FC<Props> = ({
<div className='flex flex-col gap-y-1'>
{style[plan].icon}
<div className='flex items-center'>
<div className='text-lg font-semibold uppercase leading-[125%] text-text-primary'>{t(`${i18nPrefix}.name`)}</div>
{isMostPopularPlan && <div className='ml-1 flex items-center justify-center rounded-full border-[0.5px] bg-price-premium-badge-background px-1 py-[3px] text-components-premium-badge-grey-text-stop-0 shadow-xs'>
<div className='grow text-lg font-semibold uppercase leading-[125%] text-text-primary'>{t(`${i18nPrefix}.name`)}</div>
{isMostPopularPlan && <div className='ml-1 flex shrink-0 items-center justify-center rounded-full border-[0.5px] bg-price-premium-badge-background px-1 py-[3px] text-components-premium-badge-grey-text-stop-0 shadow-xs'>
<div className='pl-0.5'>
<SparklesSoft className='size-3' />
</div>
@ -205,6 +206,14 @@ const PlanItem: FC<Props> = ({
label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })}
tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')}
/>
<KeyValue
icon={<RiTerminalBoxLine />}
label={
planInfo.apiRateLimit === NUM_INFINITE ? `${t('billing.plansCommon.unlimitedApiRate')}`
: `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')}`
}
tooltip={planInfo.apiRateLimit === NUM_INFINITE ? null : t('billing.plansCommon.apiRateLimitTooltip') as string}
/>
<KeyValue
icon={<RiProgress3Line />}
label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')}

@ -9,6 +9,9 @@ export enum Priority {
priority = 'priority',
topPriority = 'top-priority',
}
export type BasicPlan = Plan.sandbox | Plan.professional | Plan.team
export type PlanInfo = {
level: number
price: number
@ -20,6 +23,7 @@ export type PlanInfo = {
vectorSpace: string
documentsUploadQuota: number
documentsRequestQuota: number
apiRateLimit: number
documentProcessingPriority: Priority
logHistory: number
messageRequest: number
@ -60,7 +64,7 @@ export type CurrentPlanInfoBackend = {
billing: {
enabled: boolean
subscription: {
plan: Plan
plan: BasicPlan
}
}
members: {

@ -87,7 +87,7 @@ const Doc = ({ appDetail }: IDocProps) => {
<div className={`fixed right-8 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}>
{isTocExpanded
? (
<nav className="toc w-full rounded-lg bg-components-panel-bg p-4 shadow-md">
<nav className="toc max-h-[calc(100vh-150px)] w-full overflow-y-auto rounded-lg bg-components-panel-bg p-4 shadow-md">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-text-primary">{t('appApi.develop.toc')}</h3>
<button

@ -845,6 +845,106 @@ Chat applications support session persistence, allowing previous chat history to
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='Get Conversation Variables'
name='#conversation-variables'
/>
<Row>
<Col>
Retrieve variables from a specific conversation. This endpoint is useful for extracting structured data that was captured during the conversation.
### Path Parameters
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
The ID of the conversation to retrieve variables from.
</Property>
</Properties>
### Query Parameters
<Properties>
<Property name='user' type='string' key='user'>
The user identifier, defined by the developer, must ensure uniqueness within the application
</Property>
<Property name='last_id' type='string' key='last_id'>
(Optional) The ID of the last record on the current page, default is null.
</Property>
<Property name='limit' type='int' key='limit'>
(Optional) How many records to return in one request, default is the most recent 20 entries. Maximum 100, minimum 1.
</Property>
</Properties>
### Response
- `limit` (int) Number of items per page
- `has_more` (bool) Whether there is a next page
- `data` (array[object]) List of variables
- `id` (string) Variable ID
- `name` (string) Variable name
- `value_type` (string) Variable type (string, number, object, etc.)
- `value` (string) Variable value
- `description` (string) Variable description
- `created_at` (int) Creation timestamp
- `updated_at` (int) Last update timestamp
### Errors
- 404, `conversation_not_exists`, Conversation not found
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "Customer name extracted from the conversation",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "Order details from the customer",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'

@ -844,6 +844,106 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='会話変数の取得'
name='#conversation-variables'
/>
<Row>
<Col>
特定の会話から変数を取得します。このエンドポイントは、会話中に取得された構造化データを抽出するのに役立ちます。
### パスパラメータ
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
変数を取得する会話のID。
</Property>
</Properties>
### クエリパラメータ
<Properties>
<Property name='user' type='string' key='user'>
ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。
</Property>
<Property name='last_id' type='string' key='last_id'>
(Optional)現在のページの最後の記録のID、デフォルトはnullです。
</Property>
<Property name='limit' type='int' key='limit'>
(Optional)1回のリクエストで返す記録の数、デフォルトは最新の20件です。最大100、最小1。
</Property>
</Properties>
### レスポンス
- `limit` (int) ページごとのアイテム数
- `has_more` (bool) さらにアイテムがあるかどうか
- `data` (array[object]) 変数のリスト
- `id` (string) 変数ID
- `name` (string) 変数名
- `value_type` (string) 変数タイプ(文字列、数値、真偽値など)
- `value` (string) 変数値
- `description` (string) 変数の説明
- `created_at` (int) 作成タイムスタンプ
- `updated_at` (int) 最終更新タイムスタンプ
### エラー
- 404, `conversation_not_exists`, 会話が見つかりません
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "会話から抽出された顧客名",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "顧客の注文詳細",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'

@ -881,6 +881,106 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='获取对话变量'
name='#conversation-variables'
/>
<Row>
<Col>
从特定对话中检索变量。此端点对于提取对话过程中捕获的结构化数据非常有用。
### 路径参数
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
要从中检索变量的对话ID。
</Property>
</Properties>
### 查询参数
<Properties>
<Property name='user' type='string' key='user'>
用户标识符,由开发人员定义的规则,在应用程序内必须唯一。
</Property>
<Property name='last_id' type='string' key='last_id'>
(选填)当前页最后面一条记录的 ID默认 null
</Property>
<Property name='limit' type='int' key='limit'>
(选填)一次请求返回多少条记录,默认 20 条,最大 100 条,最小 1 条。
</Property>
</Properties>
### 响应
- `limit` (int) 每页项目数
- `has_more` (bool) 是否有更多项目
- `data` (array[object]) 变量列表
- `id` (string) 变量ID
- `name` (string) 变量名称
- `value_type` (string) 变量类型(字符串、数字、布尔等)
- `value` (string) 变量值
- `description` (string) 变量描述
- `created_at` (int) 创建时间戳
- `updated_at` (int) 最后更新时间戳
### 错误
- 404, `conversation_not_exists`, 对话不存在
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "客户名称(从对话中提取)",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "客户的订单详情",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'

@ -878,6 +878,106 @@ Chat applications support session persistence, allowing previous chat history to
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='Get Conversation Variables'
name='#conversation-variables'
/>
<Row>
<Col>
Retrieve variables from a specific conversation. This endpoint is useful for extracting structured data that was captured during the conversation.
### Path Parameters
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
The ID of the conversation to retrieve variables from.
</Property>
</Properties>
### Query Parameters
<Properties>
<Property name='user' type='string' key='user'>
The user identifier, defined by the developer, must ensure uniqueness within the application
</Property>
<Property name='last_id' type='string' key='last_id'>
(Optional) The ID of the last record on the current page, default is null.
</Property>
<Property name='limit' type='int' key='limit'>
(Optional) How many records to return in one request, default is the most recent 20 entries. Maximum 100, minimum 1.
</Property>
</Properties>
### Response
- `limit` (int) Number of items per page
- `has_more` (bool) Whether there is a next page
- `data` (array[object]) List of variables
- `id` (string) Variable ID
- `name` (string) Variable name
- `value_type` (string) Variable type (string, number, object, etc.)
- `value` (string) Variable value
- `description` (string) Variable description
- `created_at` (int) Creation timestamp
- `updated_at` (int) Last update timestamp
### Errors
- 404, `conversation_not_exists`, Conversation not found
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "Customer name extracted from the conversation",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "Order details from the customer",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'

@ -876,6 +876,106 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='会話変数の取得'
name='#conversation-variables'
/>
<Row>
<Col>
特定の会話から変数を取得します。このエンドポイントは、会話中に取得された構造化データを抽出するのに役立ちます。
### パスパラメータ
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
変数を取得する会話のID。
</Property>
</Properties>
### クエリパラメータ
<Properties>
<Property name='user' type='string' key='user'>
ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。
</Property>
<Property name='last_id' type='string' key='last_id'>
(Optional)現在のページの最後のレコードのID、デフォルトはnullです。
</Property>
<Property name='limit' type='int' key='limit'>
(Optional)1回のリクエストで返すレコードの数、デフォルトは最新の20件です。最大100、最小1。
</Property>
</Properties>
### レスポンス
- `limit` (int) ページごとのアイテム数
- `has_more` (bool) さらにアイテムがあるかどうか
- `data` (array[object]) 変数のリスト
- `id` (string) 変数ID
- `name` (string) 変数名
- `value_type` (string) 変数タイプ(文字列、数値、真偽値など)
- `value` (string) 変数値
- `description` (string) 変数の説明
- `created_at` (int) 作成タイムスタンプ
- `updated_at` (int) 最終更新タイムスタンプ
### エラー
- 404, `conversation_not_exists`, 会話が見つかりません
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "会話から抽出された顧客名",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "顧客の注文詳細",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'

@ -893,6 +893,106 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='获取对话变量'
name='#conversation-variables'
/>
<Row>
<Col>
从特定对话中检索变量。此端点对于提取对话过程中捕获的结构化数据非常有用。
### 路径参数
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
要从中检索变量的对话ID。
</Property>
</Properties>
### 查询参数
<Properties>
<Property name='user' type='string' key='user'>
用户标识符,由开发人员定义的规则,在应用程序内必须唯一。
</Property>
<Property name='last_id' type='string' key='last_id'>
(选填)当前页最后面一条记录的 ID默认 null
</Property>
<Property name='limit' type='int' key='limit'>
(选填)一次请求返回多少条记录,默认 20 条,最大 100 条,最小 1 条。
</Property>
</Properties>
### 响应
- `limit` (int) 每页项目数
- `has_more` (bool) 是否有更多项目
- `data` (array[object]) 变量列表
- `id` (string) 变量ID
- `name` (string) 变量名称
- `value_type` (string) 变量类型(字符串、数字、布尔等)
- `value` (string) 变量值
- `description` (string) 变量描述
- `created_at` (int) 创建时间戳
- `updated_at` (int) 最后更新时间戳
### 错误
- 404, `conversation_not_exists`, 对话不存在
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "客户名称(从对话中提取)",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "客户的订单详情",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'

@ -3,7 +3,6 @@ import type {
Model,
ModelProvider,
} from '../declarations'
import { basePath } from '@/utils/var'
import { useLanguage } from '../hooks'
import { Group } from '@/app/components/base/icons/src/vender/other'
import { OpenaiBlue, OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
@ -31,7 +30,7 @@ const ModelIcon: FC<ModelIconProps> = ({
if (provider?.icon_small) {
return (
<div className={cn('flex h-5 w-5 items-center justify-center', isDeprecated && 'opacity-50', className)}>
<img alt='model-icon' src={basePath + renderI18nObject(provider.icon_small, language)}/>
<img alt='model-icon' src={renderI18nObject(provider.icon_small, language)}/>
</div>
)
}

@ -39,6 +39,7 @@ const Input: FC<InputProps> = ({
return (
<div className='relative'>
<input
autoComplete="new-password"
tabIndex={0}
className={`
block h-8 w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-sm

@ -14,6 +14,7 @@ import Nav from '../nav'
import type { NavItem } from '../nav/nav-selector'
import { fetchDatasetDetail, fetchDatasets } from '@/service/datasets'
import type { DataSetListResponse } from '@/models/datasets'
import { basePath } from '@/utils/var'
const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
if (!pageIndex || previousPageData.has_more)
@ -56,7 +57,7 @@ const DatasetNav = () => {
icon_background: dataset.icon_background,
})) as NavItem[]}
createText={t('common.menus.newDataset')}
onCreate={() => router.push('/datasets/create')}
onCreate={() => router.push(`${basePath}/datasets/create`)}
onLoadmore={handleLoadmore}
/>
)

@ -3,9 +3,15 @@ import ChatVariableButton from '@/app/components/workflow/header/chat-variable-b
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { useIsChatMode } from '../../hooks'
const ChatVariableTrigger = () => {
const { nodesReadOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
if (!isChatMode)
return null
return <ChatVariableButton disabled={nodesReadOnly} />
}
export default memo(ChatVariableTrigger)

@ -74,7 +74,7 @@ const WorkflowPanelOnRight = () => {
)
}
{
showChatVariablePanel && (
showChatVariablePanel && isChatMode && (
<ChatVariablePanel />
)
}

@ -316,6 +316,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
nodesConnectable={!nodesReadOnly}
nodesFocusable={!nodesReadOnly}
edgesFocusable={!nodesReadOnly}
panOnScroll
panOnDrag={controlMode === ControlMode.Hand && !workflowReadOnly}
zoomOnPinch={!workflowReadOnly}
zoomOnScroll={!workflowReadOnly}

@ -772,6 +772,9 @@ export const getVarType = ({
const isStructuredOutputVar = !!targetVar.children?.schema?.properties
if (isStructuredOutputVar) {
if (valueSelector.length === 2) { // root
return VarType.object
}
let currProperties = targetVar.children.schema;
(valueSelector as ValueSelector).slice(2).forEach((key, i) => {
const isLast = i === valueSelector.length - 3

@ -47,8 +47,14 @@ const VariableModal = ({
return
if (!value)
return notify({ type: 'error', message: 'value can not be empty' })
if (!env && envList.some(env => env.name === name))
// Add check for duplicate name when editing
if (env && env.name !== name && envList.some(e => e.name === name))
return notify({ type: 'error', message: 'name is existed' })
// Original check for create new variable
if (!env && envList.some(e => e.name === name))
return notify({ type: 'error', message: 'name is existed' })
onSave({
id: env ? env.id : uuid4(),
value_type: type,

@ -16,6 +16,7 @@ import Button from '@/app/components/base/button'
import { fetchInitValidateStatus, fetchSetupStatus, setup } from '@/service/common'
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
import { basePath } from '@/utils/var'
const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
@ -80,12 +81,12 @@ const InstallForm = () => {
fetchSetupStatus().then((res: SetupStatusResponse) => {
if (res.step === 'finished') {
localStorage.setItem('setup_status', 'finished')
router.push('/signin')
router.push(`${basePath}/signin`)
}
else {
fetchInitValidateStatus().then((res: InitValidateStatusResponse) => {
if (res.status === 'not_started')
router.push('/init')
router.push(`${basePath}/init`)
})
}
setLoading(false)

@ -54,6 +54,9 @@ const LocaleLayout = async ({
data-public-indexing-max-segmentation-tokens-length={process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH}
data-public-loop-node-max-count={process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT}
data-public-max-iterations-num={process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM}
data-public-enable-website-jinareader={process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER}
data-public-enable-website-firecrawl={process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL}
data-public-enable-website-watercrawl={process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL}
>
<BrowserInitor>
<SentryInitor>

@ -306,12 +306,12 @@ export const MAX_ITERATIONS_NUM = maxIterationsNum
export const ENABLE_WEBSITE_JINAREADER = process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER !== undefined
? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER === 'true'
: true
: globalThis.document?.body?.getAttribute('data-public-enable-website-jinareader') === 'true' || true
export const ENABLE_WEBSITE_FIRECRAWL = process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL !== undefined
? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL === 'true'
: true
: globalThis.document?.body?.getAttribute('data-public-enable-website-firecrawl') === 'true' || true
export const ENABLE_WEBSITE_WATERCRAWL = process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL !== undefined
? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL === 'true'
: true
: globalThis.document?.body?.getAttribute('data-public-enable-website-watercrawl') === 'true' || true

@ -17,6 +17,7 @@ import {
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Model, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { RETRIEVE_METHOD } from '@/types/app'
import type { BasicPlan } from '@/app/components/billing/type'
import { Plan, type UsagePlanInfo } from '@/app/components/billing/type'
import { fetchCurrentPlanInfo } from '@/service/billing'
import { parseCurrentPlan } from '@/app/components/billing/utils'
@ -34,7 +35,7 @@ type ProviderContextState = {
supportRetrievalMethods: RETRIEVE_METHOD[]
isAPIKeySet: boolean
plan: {
type: Plan
type: BasicPlan
usage: UsagePlanInfo
total: UsagePlanInfo
}

@ -55,6 +55,10 @@ const translation = {
vectorSpaceTooltip: 'Documents with the High Quality indexing mode will consume Knowledge Data Storage resources. When Knowledge Data Storage reaches the limit, new documents will not be uploaded.',
documentsRequestQuota: '{{count,number}}/min Knowledge Request Rate Limit',
documentsRequestQuotaTooltip: 'Specifies the total number of actions a workspace can perform per minute within the knowledge base, including dataset creation, deletion, updates, document uploads, modifications, archiving, and knowledge base queries. This metric is used to evaluate the performance of knowledge base requests. For example, if a Sandbox user performs 10 consecutive hit tests within one minute, their workspace will be temporarily restricted from performing the following actions for the next minute: dataset creation, deletion, updates, and document uploads or modifications. ',
apiRateLimit: 'API Rate Limit',
apiRateLimitUnit: '{{count,number}}/day',
unlimitedApiRate: 'No API Rate Limit',
apiRateLimitTooltip: 'API Rate Limit applies to all requests made through the Dify API, including text generation, chat conversations, workflow executions, and document processing.',
documentProcessingPriority: ' Document Processing',
documentProcessingPriorityUpgrade: 'Process more data with higher accuracy at faster speeds.',
priority: {

@ -194,6 +194,17 @@ const translation = {
noParams: 'パラメータは必要ありません',
placeholder: 'アプリを選択...',
},
structOutput: {
moreFillTip: '最大10レベルのネストを表示します',
required: '必須',
LLMResponse: 'LLMのレスポンス',
configure: '設定',
notConfiguredTip: '構造化出力が未設定です',
structured: '構造化出力',
structuredTip: '構造化出力は、モデルが常に指定されたJSONスキーマに準拠した応答を生成することを保証する機能です。',
modelNotSupported: 'モデルが対応していません',
modelNotSupportedTip: '現在のモデルはこの機能に対応しておらず、自動的にプロンプトインジェクションに切り替わります。',
},
}
export default translation

@ -54,6 +54,10 @@ const translation = {
vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、知識データストレージのリソースを消費します。知識データストレージの上限に達すると、新しいドキュメントはアップロードされません。',
documentsRequestQuota: '{{count,number}}/分のナレッジ リクエストのレート制限',
documentsRequestQuotaTooltip: 'ナレッジベース内でワークスペースが1分間に実行できる操作の総数を示します。これには、データセットの作成、削除、更新、ドキュメントのアップロード、修正、アーカイブ、およびナレッジベースクエリが含まれます。この指標は、ナレッジベースリクエストのパフォーマンスを評価するために使用されます。例えば、Sandbox ユーザーが1分間に10回連続でヒットテストを実行した場合、そのワークスペースは次の1分間、データセットの作成、削除、更新、ドキュメントのアップロードや修正などの操作を一時的に実行できなくなります。',
apiRateLimit: 'APIレート制限',
apiRateLimitUnit: '{{count,number}}/日',
unlimitedApiRate: '無制限のAPIコール',
apiRateLimitTooltip: 'APIレート制限は、テキスト生成、チャットボット、ワークフロー、ドキュメント処理など、Dify API経由のすべてのリクエストに適用されます。',
documentProcessingPriority: '文書処理',
documentProcessingPriorityUpgrade: 'より高い精度と高速な速度でデータを処理します。',
priority: {
@ -100,17 +104,17 @@ const translation = {
},
plans: {
sandbox: {
name: 'Sandbox(サンドボックス)',
name: 'Sandbox',
for: '主要機能の無料体験',
description: '主要機能を無料で体験',
},
professional: {
name: 'Professional(プロフェッショナル)',
name: 'Professional',
for: '個人開発者/小規模チーム向け',
description: '個人開発者・小規模チームに最適',
},
team: {
name: 'Team(チーム)',
name: 'Team',
for: '中規模チーム向け',
description: '成長期のチームに必要な機能を備えたプラン',
},

@ -56,6 +56,7 @@ const translation = {
viewDetails: '詳細を見る',
copied: 'コピーしました',
in: '中',
format: 'フォーマット',
},
errorMsg: {
fieldRequired: '{{field}}は必要です',

@ -70,6 +70,10 @@ const translation = {
pasteHere: 'ここに貼り付け',
pointerMode: 'ポインターモード',
handMode: 'ハンドモード',
exportImage: '画像を出力',
exportPNG: 'PNGで出力',
exportJPEG: 'JPEGで出力',
exportSVG: 'SVGで出力',
model: 'モデル',
workflowAsTool: 'ワークフローをツールどして公開する',
configureRequired: '設定が必要',
@ -420,6 +424,34 @@ const translation = {
variable: '変数',
},
sysQueryInUser: 'ユーザーメッセージにsys.queryを含めてください',
jsonSchema: {
title: '構造化データスキーマ',
instruction: '指示',
promptTooltip: 'テキスト説明から標準JSONスキーマを自動生成できます。',
promptPlaceholder: 'JSONスキーマを入力...',
generate: '生成',
import: 'JSONインポート',
generateJsonSchema: 'スキーマ生成',
generationTip: '自然言語で簡単にJSONスキーマを作成可能です。',
generating: 'JSONスキーマを生成中...',
generatedResult: '生成結果',
resultTip: 'こちらが生成された結果です。ご満足いただけない場合は、前の画面に戻ってプロンプトを修正できます。',
back: '前に戻る',
regenerate: '再生成する',
apply: '適用',
doc: '構造化出力の詳細を見る',
resetDefaults: '初期化',
required: '必須項目',
addField: 'フィールドを追加',
addChildField: 'サブフィールドを追加',
showAdvancedOptions: '詳細設定',
stringValidations: '文字列検証',
fieldNamePlaceholder: 'フィールド名',
descriptionPlaceholder: '説明を入力',
warningTips: {
saveSchema: '編集中のフィールドを確定してから保存してください。',
},
},
},
knowledgeRetrieval: {
queryVariable: '検索変数',

@ -54,6 +54,10 @@ const translation = {
vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。',
documentsRequestQuota: '{{count,number}}/分钟 知识库请求频率限制',
documentsRequestQuotaTooltip: '指每分钟内一个空间在知识库中可执行的操作总数包括数据集的创建、删除、更新文档的上传、修改、归档以及知识库查询等用于评估知识库请求的性能。例如Sandbox 用户在 1 分钟内连续执行 10 次命中测试,其工作区将在接下来的 1 分钟内无法继续执行以下操作:数据集的创建、删除、更新,文档的上传、修改等操作。',
apiRateLimit: 'API 请求频率限制',
apiRateLimitUnit: '{{count,number}} 次/天',
unlimitedApiRate: 'API 请求频率无限制',
apiRateLimitTooltip: 'API 请求频率限制涵盖所有通过 Dify API 发起的调用,例如文本生成、聊天对话、工作流执行和文档处理等。',
documentProcessingPriority: '文档处理',
documentProcessingPriorityUpgrade: '以更快的速度、更高的精度处理更多的数据。',
priority: {

@ -1,4 +1,4 @@
const { basePath } = require('./utils/var-basePath')
const { basePath, assetPrefix } = require('./utils/var-basePath')
const { codeInspectorPlugin } = require('code-inspector-plugin')
const withMDX = require('@next/mdx')({
extension: /\.mdx?$/,
@ -16,6 +16,7 @@ const withMDX = require('@next/mdx')({
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath,
assetPrefix,
webpack: (config, { dev, isServer }) => {
config.plugins.push(codeInspectorPlugin({ bundler: 'webpack' }))
return config

@ -1,6 +1,6 @@
{
"name": "dify-web",
"version": "1.2.0",
"version": "1.3.0",
"private": true,
"engines": {
"node": ">=v22.11.0"
@ -61,6 +61,7 @@
"ahooks": "^3.8.4",
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"copy-to-clipboard": "^3.3.3",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",

@ -115,6 +115,9 @@ importers:
classnames:
specifier: ^2.5.1
version: 2.5.1
clsx:
specifier: ^2.1.1
version: 2.1.1
copy-to-clipboard:
specifier: ^3.3.3
version: 3.3.3

@ -81,7 +81,7 @@
// 3) APPEND it to the document body right away:
document.body.appendChild(preloadedIframe);
// ─── End Fix Snippet
if(iframeUrl.length > 2048) {
if (iframeUrl.length > 2048) {
console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load");
}
@ -252,6 +252,8 @@
} else {
startX = e.clientX - element.offsetLeft;
startY = e.clientY - element.offsetTop;
startClientX = e.clientX;
startClientY = e.clientY;
}
document.addEventListener("mousemove", drag);
document.addEventListener("touchmove", drag, { passive: false });

@ -35,4 +35,4 @@
<svg id="closeIcon" style="display:none" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 18L6 6M6 18L18 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",t),p.draggable){var r=n;var a=p.dragAxis||"both";let s,d,t,l;function o(e){u=!1,"touchstart"===e.type?(s=e.touches[0].clientX-r.offsetLeft,d=e.touches[0].clientY-r.offsetTop,t=e.touches[0].clientX,l=e.touches[0].clientY):(s=e.clientX-r.offsetLeft,d=e.clientY-r.offsetTop),document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-l;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){r.style.transition="none",r.style.cursor="grabbing";i=document.getElementById(h);i&&(i.style.display="none",y("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-s,window.innerHeight-n.touches[0].clientY-d):(e=n.clientX-s,window.innerHeight-n.clientY-d);o=r.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==a&&"both"!==a||r.style.setProperty(`--${m}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==a&&"both"!==a||r.style.setProperty(`--${m}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),r.style.transition="",r.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}r.addEventListener("mousedown",o),r.addEventListener("touchstart",o)}}e.style.display="none",document.body.appendChild(e),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),document.getElementById(m)||n()}else console.error(t+" is empty or token is not provided")}function y(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function l(e){"Escape"===e.key&&(e=document.getElementById(h))&&"none"!==e.style.display&&(e.style.display="none",y("open"))}document.addEventListener("keydown",l),p?.dynamicScript?e():document.body.onload=e})();
`,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",t),p.draggable){var r=n;var a=p.dragAxis||"both";let s,d,t,l;function o(e){u=!1,l=("touchstart"===e.type?(s=e.touches[0].clientX-r.offsetLeft,d=e.touches[0].clientY-r.offsetTop,t=e.touches[0].clientX,e.touches[0]):(s=e.clientX-r.offsetLeft,d=e.clientY-r.offsetTop,t=e.clientX,e)).clientY,document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-l;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){r.style.transition="none",r.style.cursor="grabbing";i=document.getElementById(h);i&&(i.style.display="none",y("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-s,window.innerHeight-n.touches[0].clientY-d):(e=n.clientX-s,window.innerHeight-n.clientY-d);o=r.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==a&&"both"!==a||r.style.setProperty(`--${m}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==a&&"both"!==a||r.style.setProperty(`--${m}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),r.style.transition="",r.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}r.addEventListener("mousedown",o),r.addEventListener("touchstart",o)}}e.style.display="none",document.body.appendChild(e),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),document.getElementById(m)||n()}else console.error(t+" is empty or token is not provided")}function y(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function l(e){"Escape"===e.key&&(e=document.getElementById(h))&&"none"!==e.style.display&&(e.style.display="none",y("open"))}document.addEventListener("keydown",l),p?.dynamicScript?e():document.body.onload=e})();

@ -2,4 +2,5 @@
// same as the one exported from var.ts
module.exports = {
basePath: '',
assetPrefix: '',
}

Loading…
Cancel
Save