Merge branch 'main' into feat/rag-pipeline

pull/21398/head
twwu 11 months ago
commit 0a9f50e85f

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
npm add -g pnpm@10.8.0 npm add -g pnpm@10.11.1
cd web && pnpm install cd web && pnpm install
pipx install uv pipx install uv

@ -846,6 +846,9 @@ def clear_orphaned_file_records(force: bool):
{"type": "text", "table": "workflow_node_executions", "column": "outputs"}, {"type": "text", "table": "workflow_node_executions", "column": "outputs"},
{"type": "text", "table": "conversations", "column": "introduction"}, {"type": "text", "table": "conversations", "column": "introduction"},
{"type": "text", "table": "conversations", "column": "system_instruction"}, {"type": "text", "table": "conversations", "column": "system_instruction"},
{"type": "text", "table": "accounts", "column": "avatar"},
{"type": "text", "table": "apps", "column": "icon"},
{"type": "text", "table": "sites", "column": "icon"},
{"type": "json", "table": "messages", "column": "inputs"}, {"type": "json", "table": "messages", "column": "inputs"},
{"type": "json", "table": "messages", "column": "message"}, {"type": "json", "table": "messages", "column": "message"},
] ]

@ -60,8 +60,7 @@ class NacosHttpClient:
sign_str = tenant + "+" sign_str = tenant + "+"
if group: if group:
sign_str = sign_str + group + "+" sign_str = sign_str + group + "+"
if sign_str: sign_str += ts # Directly concatenate ts without conditional checks, because the nacos auth header forced it.
sign_str += ts
return sign_str return sign_str
def get_access_token(self, force_refresh=False): def get_access_token(self, force_refresh=False):

@ -70,7 +70,7 @@ class ModelConfigConverter:
if not model_mode: if not model_mode:
model_mode = LLMMode.CHAT.value model_mode = LLMMode.CHAT.value
if model_schema and model_schema.model_properties.get(ModelPropertyKey.MODE): if model_schema and model_schema.model_properties.get(ModelPropertyKey.MODE):
model_mode = LLMMode.value_of(model_schema.model_properties[ModelPropertyKey.MODE]).value model_mode = LLMMode(model_schema.model_properties[ModelPropertyKey.MODE]).value
if not model_schema: if not model_schema:
raise ValueError(f"Model {model_name} not exist.") raise ValueError(f"Model {model_name} not exist.")

@ -15,6 +15,7 @@ from core.helper.code_executor.python3.python3_transformer import Python3Templat
from core.helper.code_executor.template_transformer import TemplateTransformer from core.helper.code_executor.template_transformer import TemplateTransformer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
code_execution_endpoint_url = URL(str(dify_config.CODE_EXECUTION_ENDPOINT))
class CodeExecutionError(Exception): class CodeExecutionError(Exception):
@ -64,7 +65,7 @@ class CodeExecutor:
:param code: code :param code: code
:return: :return:
""" """
url = URL(str(dify_config.CODE_EXECUTION_ENDPOINT)) / "v1" / "sandbox" / "run" url = code_execution_endpoint_url / "v1" / "sandbox" / "run"
headers = {"X-Api-Key": dify_config.CODE_EXECUTION_API_KEY} headers = {"X-Api-Key": dify_config.CODE_EXECUTION_API_KEY}

@ -7,29 +7,28 @@ from configs import dify_config
from core.helper.download import download_with_size_limit from core.helper.download import download_with_size_limit
from core.plugin.entities.marketplace import MarketplacePluginDeclaration from core.plugin.entities.marketplace import MarketplacePluginDeclaration
marketplace_api_url = URL(str(dify_config.MARKETPLACE_API_URL))
def get_plugin_pkg_url(plugin_unique_identifier: str):
return (URL(str(dify_config.MARKETPLACE_API_URL)) / "api/v1/plugins/download").with_query( def get_plugin_pkg_url(plugin_unique_identifier: str) -> str:
unique_identifier=plugin_unique_identifier return str((marketplace_api_url / "api/v1/plugins/download").with_query(unique_identifier=plugin_unique_identifier))
)
def download_plugin_pkg(plugin_unique_identifier: str): def download_plugin_pkg(plugin_unique_identifier: str):
url = str(get_plugin_pkg_url(plugin_unique_identifier)) return download_with_size_limit(get_plugin_pkg_url(plugin_unique_identifier), dify_config.PLUGIN_MAX_PACKAGE_SIZE)
return download_with_size_limit(url, dify_config.PLUGIN_MAX_PACKAGE_SIZE)
def batch_fetch_plugin_manifests(plugin_ids: list[str]) -> Sequence[MarketplacePluginDeclaration]: def batch_fetch_plugin_manifests(plugin_ids: list[str]) -> Sequence[MarketplacePluginDeclaration]:
if len(plugin_ids) == 0: if len(plugin_ids) == 0:
return [] return []
url = str(URL(str(dify_config.MARKETPLACE_API_URL)) / "api/v1/plugins/batch") url = str(marketplace_api_url / "api/v1/plugins/batch")
response = requests.post(url, json={"plugin_ids": plugin_ids}) response = requests.post(url, json={"plugin_ids": plugin_ids})
response.raise_for_status() response.raise_for_status()
return [MarketplacePluginDeclaration(**plugin) for plugin in response.json()["data"]["plugins"]] return [MarketplacePluginDeclaration(**plugin) for plugin in response.json()["data"]["plugins"]]
def record_install_plugin_event(plugin_unique_identifier: str): def record_install_plugin_event(plugin_unique_identifier: str):
url = str(URL(str(dify_config.MARKETPLACE_API_URL)) / "api/v1/stats/plugins/install_count") url = str(marketplace_api_url / "api/v1/stats/plugins/install_count")
response = requests.post(url, json={"unique_identifier": plugin_unique_identifier}) response = requests.post(url, json={"unique_identifier": plugin_unique_identifier})
response.raise_for_status() response.raise_for_status()

@ -17,19 +17,6 @@ class LLMMode(StrEnum):
COMPLETION = "completion" COMPLETION = "completion"
CHAT = "chat" CHAT = "chat"
@classmethod
def value_of(cls, value: str) -> "LLMMode":
"""
Get value of given mode.
:param value: mode value
:return: mode
"""
for mode in cls:
if mode.value == value:
return mode
raise ValueError(f"invalid mode value {value}")
class LLMUsage(ModelUsage): class LLMUsage(ModelUsage):
""" """

@ -1,7 +1,11 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from sqlalchemy.orm import Session
from core.ops.entities.config_entity import BaseTracingConfig from core.ops.entities.config_entity import BaseTracingConfig
from core.ops.entities.trace_entity import BaseTraceInfo from core.ops.entities.trace_entity import BaseTraceInfo
from extensions.ext_database import db
from models import Account, App, TenantAccountJoin
class BaseTraceInstance(ABC): class BaseTraceInstance(ABC):
@ -24,3 +28,38 @@ class BaseTraceInstance(ABC):
Subclasses must implement specific tracing logic for activities. Subclasses must implement specific tracing logic for activities.
""" """
... ...
def get_service_account_with_tenant(self, app_id: str) -> Account:
"""
Get service account for an app and set up its tenant.
Args:
app_id: The ID of the app
Returns:
Account: The service account with tenant set up
Raises:
ValueError: If app, creator account or tenant cannot be found
"""
with Session(db.engine, expire_on_commit=False) as session:
# Get the app to find its creator
app = session.query(App).filter(App.id == app_id).first()
if not app:
raise ValueError(f"App with id {app_id} not found")
if not app.created_by:
raise ValueError(f"App with id {app_id} has no creator (created_by is None)")
service_account = session.query(Account).filter(Account.id == app.created_by).first()
if not service_account:
raise ValueError(f"Creator account with id {app.created_by} not found for app {app_id}")
current_tenant = (
session.query(TenantAccountJoin).filter_by(account_id=service_account.id, current=True).first()
)
if not current_tenant:
raise ValueError(f"Current tenant not found for account {service_account.id}")
service_account.set_tenant_id(current_tenant.tenant_id)
return service_account

@ -4,7 +4,7 @@ from datetime import datetime, timedelta
from typing import Optional from typing import Optional
from langfuse import Langfuse # type: ignore from langfuse import Langfuse # type: ignore
from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.orm import sessionmaker
from core.ops.base_trace_instance import BaseTraceInstance from core.ops.base_trace_instance import BaseTraceInstance
from core.ops.entities.config_entity import LangfuseConfig from core.ops.entities.config_entity import LangfuseConfig
@ -31,8 +31,7 @@ from core.ops.utils import filter_none_values
from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository
from core.workflow.nodes.enums import NodeType from core.workflow.nodes.enums import NodeType
from extensions.ext_database import db from extensions.ext_database import db
from models import Account, App, EndUser, WorkflowNodeExecutionTriggeredFrom from models import EndUser, WorkflowNodeExecutionTriggeredFrom
from models.account import TenantAccountJoin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -115,28 +114,11 @@ class LangFuseDataTrace(BaseTraceInstance):
# through workflow_run_id get all_nodes_execution using repository # through workflow_run_id get all_nodes_execution using repository
session_factory = sessionmaker(bind=db.engine) session_factory = sessionmaker(bind=db.engine)
# Find the app's creator account # Find the app's creator account
with Session(db.engine, expire_on_commit=False) as session: app_id = trace_info.metadata.get("app_id")
# Get the app to find its creator if not app_id:
app_id = trace_info.metadata.get("app_id") raise ValueError("No app_id found in trace_info metadata")
if not app_id:
raise ValueError("No app_id found in trace_info metadata") service_account = self.get_service_account_with_tenant(app_id)
app = session.query(App).filter(App.id == app_id).first()
if not app:
raise ValueError(f"App with id {app_id} not found")
if not app.created_by:
raise ValueError(f"App with id {app_id} has no creator (created_by is None)")
service_account = session.query(Account).filter(Account.id == app.created_by).first()
if not service_account:
raise ValueError(f"Creator account with id {app.created_by} not found for app {app_id}")
current_tenant = (
session.query(TenantAccountJoin).filter_by(account_id=service_account.id, current=True).first()
)
if not current_tenant:
raise ValueError(f"Current tenant not found for account {service_account.id}")
service_account.set_tenant_id(current_tenant.tenant_id)
workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
session_factory=session_factory, session_factory=session_factory,

@ -6,7 +6,7 @@ from typing import Optional, cast
from langsmith import Client from langsmith import Client
from langsmith.schemas import RunBase from langsmith.schemas import RunBase
from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.orm import sessionmaker
from core.ops.base_trace_instance import BaseTraceInstance from core.ops.base_trace_instance import BaseTraceInstance
from core.ops.entities.config_entity import LangSmithConfig from core.ops.entities.config_entity import LangSmithConfig
@ -31,7 +31,7 @@ from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey
from core.workflow.nodes.enums import NodeType from core.workflow.nodes.enums import NodeType
from extensions.ext_database import db from extensions.ext_database import db
from models import Account, App, EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -139,22 +139,11 @@ class LangSmithDataTrace(BaseTraceInstance):
# through workflow_run_id get all_nodes_execution using repository # through workflow_run_id get all_nodes_execution using repository
session_factory = sessionmaker(bind=db.engine) session_factory = sessionmaker(bind=db.engine)
# Find the app's creator account # Find the app's creator account
with Session(db.engine, expire_on_commit=False) as session: app_id = trace_info.metadata.get("app_id")
# Get the app to find its creator if not app_id:
app_id = trace_info.metadata.get("app_id") raise ValueError("No app_id found in trace_info metadata")
if not app_id:
raise ValueError("No app_id found in trace_info metadata") service_account = self.get_service_account_with_tenant(app_id)
app = session.query(App).filter(App.id == app_id).first()
if not app:
raise ValueError(f"App with id {app_id} not found")
if not app.created_by:
raise ValueError(f"App with id {app_id} has no creator (created_by is None)")
service_account = session.query(Account).filter(Account.id == app.created_by).first()
if not service_account:
raise ValueError(f"Creator account with id {app.created_by} not found for app {app_id}")
workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
session_factory=session_factory, session_factory=session_factory,

@ -6,7 +6,7 @@ from typing import Optional, cast
from opik import Opik, Trace from opik import Opik, Trace
from opik.id_helpers import uuid4_to_uuid7 from opik.id_helpers import uuid4_to_uuid7
from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.orm import sessionmaker
from core.ops.base_trace_instance import BaseTraceInstance from core.ops.base_trace_instance import BaseTraceInstance
from core.ops.entities.config_entity import OpikConfig from core.ops.entities.config_entity import OpikConfig
@ -25,7 +25,7 @@ from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey
from core.workflow.nodes.enums import NodeType from core.workflow.nodes.enums import NodeType
from extensions.ext_database import db from extensions.ext_database import db
from models import Account, App, EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -154,22 +154,11 @@ class OpikDataTrace(BaseTraceInstance):
# through workflow_run_id get all_nodes_execution using repository # through workflow_run_id get all_nodes_execution using repository
session_factory = sessionmaker(bind=db.engine) session_factory = sessionmaker(bind=db.engine)
# Find the app's creator account # Find the app's creator account
with Session(db.engine, expire_on_commit=False) as session: app_id = trace_info.metadata.get("app_id")
# Get the app to find its creator if not app_id:
app_id = trace_info.metadata.get("app_id") raise ValueError("No app_id found in trace_info metadata")
if not app_id:
raise ValueError("No app_id found in trace_info metadata") service_account = self.get_service_account_with_tenant(app_id)
app = session.query(App).filter(App.id == app_id).first()
if not app:
raise ValueError(f"App with id {app_id} not found")
if not app.created_by:
raise ValueError(f"App with id {app_id} has no creator (created_by is None)")
service_account = session.query(Account).filter(Account.id == app.created_by).first()
if not service_account:
raise ValueError(f"Creator account with id {app.created_by} not found for app {app_id}")
workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
session_factory=session_factory, session_factory=session_factory,

@ -6,7 +6,7 @@ from typing import Any, Optional, cast
import wandb import wandb
import weave import weave
from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.orm import sessionmaker
from core.ops.base_trace_instance import BaseTraceInstance from core.ops.base_trace_instance import BaseTraceInstance
from core.ops.entities.config_entity import WeaveConfig from core.ops.entities.config_entity import WeaveConfig
@ -26,7 +26,7 @@ from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey
from core.workflow.nodes.enums import NodeType from core.workflow.nodes.enums import NodeType
from extensions.ext_database import db from extensions.ext_database import db
from models import Account, App, EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -133,22 +133,11 @@ class WeaveDataTrace(BaseTraceInstance):
# through workflow_run_id get all_nodes_execution using repository # through workflow_run_id get all_nodes_execution using repository
session_factory = sessionmaker(bind=db.engine) session_factory = sessionmaker(bind=db.engine)
# Find the app's creator account # Find the app's creator account
with Session(db.engine, expire_on_commit=False) as session: app_id = trace_info.metadata.get("app_id")
# Get the app to find its creator if not app_id:
app_id = trace_info.metadata.get("app_id") raise ValueError("No app_id found in trace_info metadata")
if not app_id:
raise ValueError("No app_id found in trace_info metadata") service_account = self.get_service_account_with_tenant(app_id)
app = session.query(App).filter(App.id == app_id).first()
if not app:
raise ValueError(f"App with id {app_id} not found")
if not app.created_by:
raise ValueError(f"App with id {app_id} has no creator (created_by is None)")
service_account = session.query(Account).filter(Account.id == app.created_by).first()
if not service_account:
raise ValueError(f"Creator account with id {app.created_by} not found for app {app_id}")
workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
session_factory=session_factory, session_factory=session_factory,

@ -31,8 +31,7 @@ from core.plugin.impl.exc import (
PluginUniqueIdentifierError, PluginUniqueIdentifierError,
) )
plugin_daemon_inner_api_baseurl = dify_config.PLUGIN_DAEMON_URL plugin_daemon_inner_api_baseurl = URL(str(dify_config.PLUGIN_DAEMON_URL))
plugin_daemon_inner_api_key = dify_config.PLUGIN_DAEMON_KEY
T = TypeVar("T", bound=(BaseModel | dict | list | bool | str)) T = TypeVar("T", bound=(BaseModel | dict | list | bool | str))
@ -53,9 +52,9 @@ class BasePluginClient:
""" """
Make a request to the plugin daemon inner API. Make a request to the plugin daemon inner API.
""" """
url = URL(str(plugin_daemon_inner_api_baseurl)) / path url = plugin_daemon_inner_api_baseurl / path
headers = headers or {} headers = headers or {}
headers["X-Api-Key"] = plugin_daemon_inner_api_key headers["X-Api-Key"] = dify_config.PLUGIN_DAEMON_KEY
headers["Accept-Encoding"] = "gzip, deflate, br" headers["Accept-Encoding"] = "gzip, deflate, br"
if headers.get("Content-Type") == "application/json" and isinstance(data, dict): if headers.get("Content-Type") == "application/json" and isinstance(data, dict):

@ -142,7 +142,7 @@ class ElasticSearchVector(BaseVector):
if score > score_threshold: if score > score_threshold:
if doc.metadata is not None: if doc.metadata is not None:
doc.metadata["score"] = score doc.metadata["score"] = score
docs.append(doc) docs.append(doc)
return docs return docs

@ -97,6 +97,10 @@ class MilvusVector(BaseVector):
try: try:
milvus_version = self._client.get_server_version() milvus_version = self._client.get_server_version()
# Check if it's Zilliz Cloud - it supports full-text search with Milvus 2.5 compatibility
if "Zilliz Cloud" in milvus_version:
return True
# For standard Milvus installations, check version number
return version.parse(milvus_version).base_version >= version.parse("2.5.0").base_version return version.parse(milvus_version).base_version >= version.parse("2.5.0").base_version
except Exception as e: except Exception as e:
logger.warning(f"Failed to check Milvus version: {str(e)}. Disabling hybrid search.") logger.warning(f"Failed to check Milvus version: {str(e)}. Disabling hybrid search.")

@ -168,7 +168,7 @@ class ApiTool(Tool):
cookies[parameter["name"]] = value cookies[parameter["name"]] = value
elif parameter["in"] == "header": elif parameter["in"] == "header":
headers[parameter["name"]] = value headers[parameter["name"]] = str(value)
# check if there is a request body and handle it # check if there is a request body and handle it
if "requestBody" in self.api_bundle.openapi and self.api_bundle.openapi["requestBody"] is not None: if "requestBody" in self.api_bundle.openapi and self.api_bundle.openapi["requestBody"] is not None:

@ -55,6 +55,13 @@ class ApiBasedToolSchemaParser:
# convert parameters # convert parameters
parameters = [] parameters = []
if "parameters" in interface["operation"]: if "parameters" in interface["operation"]:
for i, parameter in enumerate(interface["operation"]["parameters"]):
if "$ref" in parameter:
root = openapi
reference = parameter["$ref"].split("/")[1:]
for ref in reference:
root = root[ref]
interface["operation"]["parameters"][i] = root
for parameter in interface["operation"]["parameters"]: for parameter in interface["operation"]["parameters"]:
tool_parameter = ToolParameter( tool_parameter = ToolParameter(
name=parameter["name"], name=parameter["name"],

@ -53,7 +53,6 @@ from core.workflow.nodes.end.end_stream_processor import EndStreamProcessor
from core.workflow.nodes.enums import ErrorStrategy, FailBranchSourceHandle from core.workflow.nodes.enums import ErrorStrategy, FailBranchSourceHandle
from core.workflow.nodes.event import RunCompletedEvent, RunRetrieverResourceEvent, RunStreamChunkEvent from core.workflow.nodes.event import RunCompletedEvent, RunRetrieverResourceEvent, RunStreamChunkEvent
from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING
from extensions.ext_database import db
from models.enums import UserFrom from models.enums import UserFrom
from models.workflow import WorkflowType from models.workflow import WorkflowType
@ -607,8 +606,6 @@ class GraphEngine:
error=str(e), error=str(e),
) )
) )
finally:
db.session.remove()
def _run_node( def _run_node(
self, self,
@ -646,7 +643,6 @@ class GraphEngine:
agent_strategy=agent_strategy, agent_strategy=agent_strategy,
) )
db.session.close()
max_retries = node_instance.node_data.retry_config.max_retries max_retries = node_instance.node_data.retry_config.max_retries
retry_interval = node_instance.node_data.retry_config.retry_interval_seconds retry_interval = node_instance.node_data.retry_config.retry_interval_seconds
retries = 0 retries = 0
@ -863,8 +859,6 @@ class GraphEngine:
except Exception as e: except Exception as e:
logger.exception(f"Node {node_instance.node_data.title} run failed") logger.exception(f"Node {node_instance.node_data.title} run failed")
raise e raise e
finally:
db.session.close()
def _append_variables_recursively(self, node_id: str, variable_key_list: list[str], variable_value: VariableValue): def _append_variables_recursively(self, node_id: str, variable_key_list: list[str], variable_value: VariableValue):
""" """

@ -2,6 +2,9 @@ import json
from collections.abc import Generator, Mapping, Sequence from collections.abc import Generator, Mapping, Sequence
from typing import Any, Optional, cast from typing import Any, Optional, cast
from sqlalchemy import select
from sqlalchemy.orm import Session
from core.agent.entities import AgentToolEntity from core.agent.entities import AgentToolEntity
from core.agent.plugin_entities import AgentStrategyParameter from core.agent.plugin_entities import AgentStrategyParameter
from core.memory.token_buffer_memory import TokenBufferMemory from core.memory.token_buffer_memory import TokenBufferMemory
@ -320,15 +323,12 @@ class AgentNode(ToolNode):
return None return None
conversation_id = conversation_id_variable.value conversation_id = conversation_id_variable.value
# get conversation with Session(db.engine, expire_on_commit=False) as session:
conversation = ( stmt = select(Conversation).where(Conversation.app_id == self.app_id, Conversation.id == conversation_id)
db.session.query(Conversation) conversation = session.scalar(stmt)
.filter(Conversation.app_id == self.app_id, Conversation.id == conversation_id)
.first()
)
if not conversation: if not conversation:
return None return None
memory = TokenBufferMemory(conversation=conversation, model_instance=model_instance) memory = TokenBufferMemory(conversation=conversation, model_instance=model_instance)

@ -8,6 +8,7 @@ from typing import Any, Optional, cast
from sqlalchemy import Float, and_, func, or_, text from sqlalchemy import Float, and_, func, or_, text
from sqlalchemy import cast as sqlalchemy_cast from sqlalchemy import cast as sqlalchemy_cast
from sqlalchemy.orm import Session
from core.app.app_config.entities import DatasetRetrieveConfigEntity from core.app.app_config.entities import DatasetRetrieveConfigEntity
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
@ -95,14 +96,15 @@ class KnowledgeRetrievalNode(LLMNode):
redis_client.zremrangebyscore(key, 0, current_time - 60000) redis_client.zremrangebyscore(key, 0, current_time - 60000)
request_count = redis_client.zcard(key) request_count = redis_client.zcard(key)
if request_count > knowledge_rate_limit.limit: if request_count > knowledge_rate_limit.limit:
# add ratelimit record with Session(db.engine) as session:
rate_limit_log = RateLimitLog( # add ratelimit record
tenant_id=self.tenant_id, rate_limit_log = RateLimitLog(
subscription_plan=knowledge_rate_limit.subscription_plan, tenant_id=self.tenant_id,
operation="knowledge", subscription_plan=knowledge_rate_limit.subscription_plan,
) operation="knowledge",
db.session.add(rate_limit_log) )
db.session.commit() session.add(rate_limit_log)
session.commit()
return NodeRunResult( return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED, status=WorkflowNodeExecutionStatus.FAILED,
inputs=variables, inputs=variables,
@ -173,7 +175,9 @@ class KnowledgeRetrievalNode(LLMNode):
dataset_retrieval = DatasetRetrieval() dataset_retrieval = DatasetRetrieval()
if node_data.retrieval_mode == DatasetRetrieveConfigEntity.RetrieveStrategy.SINGLE.value: if node_data.retrieval_mode == DatasetRetrieveConfigEntity.RetrieveStrategy.SINGLE.value:
# fetch model config # fetch model config
model_instance, model_config = self._fetch_model_config(node_data.single_retrieval_config.model) # type: ignore if node_data.single_retrieval_config is None:
raise ValueError("single_retrieval_config is required")
model_instance, model_config = self.get_model_config(node_data.single_retrieval_config.model)
# check model is support tool calling # check model is support tool calling
model_type_instance = model_config.provider_model_bundle.model_type_instance model_type_instance = model_config.provider_model_bundle.model_type_instance
model_type_instance = cast(LargeLanguageModel, model_type_instance) model_type_instance = cast(LargeLanguageModel, model_type_instance)
@ -424,7 +428,7 @@ class KnowledgeRetrievalNode(LLMNode):
raise ValueError("metadata_model_config is required") raise ValueError("metadata_model_config is required")
# get metadata model instance # get metadata model instance
# fetch model config # fetch model config
model_instance, model_config = self._fetch_model_config(node_data.metadata_model_config) # type: ignore model_instance, model_config = self.get_model_config(metadata_model_config)
# fetch prompt messages # fetch prompt messages
prompt_template = self._get_prompt_template( prompt_template = self._get_prompt_template(
node_data=node_data, node_data=node_data,
@ -550,14 +554,7 @@ class KnowledgeRetrievalNode(LLMNode):
variable_mapping[node_id + ".query"] = node_data.query_variable_selector variable_mapping[node_id + ".query"] = node_data.query_variable_selector
return variable_mapping return variable_mapping
def _fetch_model_config(self, model: ModelConfig) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]: # type: ignore def get_model_config(self, model: ModelConfig) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]:
"""
Fetch model config
:param model: model
:return:
"""
if model is None:
raise ValueError("model is required")
model_name = model.name model_name = model.name
provider_name = model.provider provider_name = model.provider

@ -7,6 +7,8 @@ from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any, Optional, cast from typing import TYPE_CHECKING, Any, Optional, cast
import json_repair import json_repair
from sqlalchemy import select, update
from sqlalchemy.orm import Session
from configs import dify_config from configs import dify_config
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
@ -303,8 +305,6 @@ class LLMNode(BaseNode[LLMNodeData]):
prompt_messages: Sequence[PromptMessage], prompt_messages: Sequence[PromptMessage],
stop: Optional[Sequence[str]] = None, stop: Optional[Sequence[str]] = None,
) -> Generator[NodeEvent, None, None]: ) -> Generator[NodeEvent, None, None]:
db.session.close()
invoke_result = model_instance.invoke_llm( invoke_result = model_instance.invoke_llm(
prompt_messages=list(prompt_messages), prompt_messages=list(prompt_messages),
model_parameters=node_data_model.completion_params, model_parameters=node_data_model.completion_params,
@ -603,15 +603,11 @@ class LLMNode(BaseNode[LLMNodeData]):
return None return None
conversation_id = conversation_id_variable.value conversation_id = conversation_id_variable.value
# get conversation with Session(db.engine, expire_on_commit=False) as session:
conversation = ( stmt = select(Conversation).where(Conversation.app_id == self.app_id, Conversation.id == conversation_id)
db.session.query(Conversation) conversation = session.scalar(stmt)
.filter(Conversation.app_id == self.app_id, Conversation.id == conversation_id) if not conversation:
.first() return None
)
if not conversation:
return None
memory = TokenBufferMemory(conversation=conversation, model_instance=model_instance) memory = TokenBufferMemory(conversation=conversation, model_instance=model_instance)
@ -847,20 +843,24 @@ class LLMNode(BaseNode[LLMNodeData]):
used_quota = 1 used_quota = 1
if used_quota is not None and system_configuration.current_quota_type is not None: if used_quota is not None and system_configuration.current_quota_type is not None:
db.session.query(Provider).filter( with Session(db.engine) as session:
Provider.tenant_id == tenant_id, stmt = (
# TODO: Use provider name with prefix after the data migration. update(Provider)
Provider.provider_name == ModelProviderID(model_instance.provider).provider_name, .where(
Provider.provider_type == ProviderType.SYSTEM.value, Provider.tenant_id == tenant_id,
Provider.quota_type == system_configuration.current_quota_type.value, # TODO: Use provider name with prefix after the data migration.
Provider.quota_limit > Provider.quota_used, Provider.provider_name == ModelProviderID(model_instance.provider).provider_name,
).update( Provider.provider_type == ProviderType.SYSTEM.value,
{ Provider.quota_type == system_configuration.current_quota_type.value,
"quota_used": Provider.quota_used + used_quota, Provider.quota_limit > Provider.quota_used,
"last_used": datetime.now(tz=UTC).replace(tzinfo=None), )
} .values(
) quota_used=Provider.quota_used + used_quota,
db.session.commit() last_used=datetime.now(tz=UTC).replace(tzinfo=None),
)
)
session.execute(stmt)
session.commit()
@classmethod @classmethod
def _extract_variable_selector_to_variable_mapping( def _extract_variable_selector_to_variable_mapping(

@ -31,7 +31,6 @@ from core.workflow.entities.workflow_node_execution import WorkflowNodeExecution
from core.workflow.nodes.enums import NodeType from core.workflow.nodes.enums import NodeType
from core.workflow.nodes.llm import LLMNode, ModelConfig from core.workflow.nodes.llm import LLMNode, ModelConfig
from core.workflow.utils import variable_template_parser from core.workflow.utils import variable_template_parser
from extensions.ext_database import db
from .entities import ParameterExtractorNodeData from .entities import ParameterExtractorNodeData
from .exc import ( from .exc import (
@ -259,8 +258,6 @@ class ParameterExtractorNode(LLMNode):
tools: list[PromptMessageTool], tools: list[PromptMessageTool],
stop: list[str], stop: list[str],
) -> tuple[str, LLMUsage, Optional[AssistantPromptMessage.ToolCall]]: ) -> tuple[str, LLMUsage, Optional[AssistantPromptMessage.ToolCall]]:
db.session.close()
invoke_result = model_instance.invoke_llm( invoke_result = model_instance.invoke_llm(
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
model_parameters=node_data_model.completion_params, model_parameters=node_data_model.completion_params,

@ -79,9 +79,13 @@ class QuestionClassifierNode(LLMNode):
memory=memory, memory=memory,
max_token_limit=rest_token, max_token_limit=rest_token,
) )
# Some models (e.g. Gemma, Mistral) force roles alternation (user/assistant/user/assistant...).
# If both self._get_prompt_template and self._fetch_prompt_messages append a user prompt,
# two consecutive user prompts will be generated, causing model's error.
# To avoid this, set sys_query to an empty string so that only one user prompt is appended at the end.
prompt_messages, stop = self._fetch_prompt_messages( prompt_messages, stop = self._fetch_prompt_messages(
prompt_template=prompt_template, prompt_template=prompt_template,
sys_query=query, sys_query="",
memory=memory, memory=memory,
model_config=model_config, model_config=model_config,
sys_files=files, sys_files=files,

@ -1,7 +1,8 @@
from typing import Literal, Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from core.variables.types import SegmentType
from core.workflow.nodes.base import BaseNodeData from core.workflow.nodes.base import BaseNodeData
@ -17,7 +18,7 @@ class AdvancedSettings(BaseModel):
Group. Group.
""" """
output_type: Literal["string", "number", "object", "array[string]", "array[number]", "array[object]"] output_type: SegmentType
variables: list[list[str]] variables: list[list[str]]
group_name: str group_name: str

@ -28,7 +28,8 @@ class SMTPClient:
else: else:
smtp = smtplib.SMTP(self.server, self.port, timeout=10) smtp = smtplib.SMTP(self.server, self.port, timeout=10)
if self.username and self.password: # Only authenticate if both username and password are non-empty
if self.username and self.password and self.username.strip() and self.password.strip():
smtp.login(self.username, self.password) smtp.login(self.username, self.password)
msg = MIMEMultipart() msg = MIMEMultipart()

@ -14,7 +14,7 @@ dependencies = [
"chardet~=5.1.0", "chardet~=5.1.0",
"flask~=3.1.0", "flask~=3.1.0",
"flask-compress~=1.17", "flask-compress~=1.17",
"flask-cors~=5.0.0", "flask-cors~=6.0.0",
"flask-login~=0.6.3", "flask-login~=0.6.3",
"flask-migrate~=4.0.7", "flask-migrate~=4.0.7",
"flask-restful~=0.3.10", "flask-restful~=0.3.10",
@ -36,7 +36,6 @@ dependencies = [
"mailchimp-transactional~=1.0.50", "mailchimp-transactional~=1.0.50",
"markdown~=3.5.1", "markdown~=3.5.1",
"numpy~=1.26.4", "numpy~=1.26.4",
"oci~=2.135.1",
"openai~=1.61.0", "openai~=1.61.0",
"openpyxl~=3.1.5", "openpyxl~=3.1.5",
"opik~=1.7.25", "opik~=1.7.25",
@ -143,13 +142,16 @@ dev = [
"types-requests~=2.32.0", "types-requests~=2.32.0",
"types-requests-oauthlib~=2.0.0", "types-requests-oauthlib~=2.0.0",
"types-shapely~=2.0.0", "types-shapely~=2.0.0",
"types-simplejson~=3.20.0", "types-simplejson>=3.20.0",
"types-six~=1.17.0", "types-six>=1.17.0",
"types-tensorflow~=2.18.0", "types-tensorflow>=2.18.0",
"types-tqdm~=4.67.0", "types-tqdm>=4.67.0",
"types-ujson~=5.10.0", "types-ujson>=5.10.0",
"boto3-stubs>=1.38.20", "boto3-stubs>=1.38.20",
"types-jmespath>=1.0.2.20240106", "types-jmespath>=1.0.2.20240106",
"types_pyOpenSSL>=24.1.0",
"types_cffi>=1.17.0",
"types_setuptools>=80.9.0",
] ]
############################################################ ############################################################

File diff suppressed because it is too large Load Diff

@ -6,7 +6,7 @@ LABEL maintainer="takatost@gmail.com"
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories # RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache tzdata RUN apk add --no-cache tzdata
RUN npm install -g pnpm@10.8.0 RUN npm install -g pnpm@10.11.1
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"

@ -366,8 +366,9 @@ export const useChat = (
if (!newResponseItem) if (!newResponseItem)
return return
const isUseAgentThought = newResponseItem.agent_thoughts?.length > 0
updateChatTreeNode(responseItem.id, { updateChatTreeNode(responseItem.id, {
content: newResponseItem.answer, content: isUseAgentThought ? '' : newResponseItem.answer,
log: [ log: [
...newResponseItem.message, ...newResponseItem.message,
...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant' ...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'

@ -2,7 +2,7 @@
// DON NOT EDIT IT MANUALLY // DON NOT EDIT IT MANUALLY
import * as React from 'react' import * as React from 'react'
import data from './OpenaiTale.json' import data from './OpenaiTeal.json'
import IconBase from '@/app/components/base/icons/IconBase' import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase' import type { IconData } from '@/app/components/base/icons/IconBase'
@ -15,6 +15,6 @@ const Icon = (
}, },
) => <IconBase {...props} ref={ref} data={data as IconData} /> ) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'OpenaiTale' Icon.displayName = 'OpenaiTeal'
export default Icon export default Icon

@ -28,9 +28,9 @@ export { default as Microsoft } from './Microsoft'
export { default as OpenaiBlack } from './OpenaiBlack' export { default as OpenaiBlack } from './OpenaiBlack'
export { default as OpenaiBlue } from './OpenaiBlue' export { default as OpenaiBlue } from './OpenaiBlue'
export { default as OpenaiGreen } from './OpenaiGreen' export { default as OpenaiGreen } from './OpenaiGreen'
export { default as OpenaiTeal } from './OpenaiTeal'
export { default as OpenaiText } from './OpenaiText' export { default as OpenaiText } from './OpenaiText'
export { default as OpenaiTransparent } from './OpenaiTransparent' export { default as OpenaiTransparent } from './OpenaiTransparent'
export { default as OpenaiTale } from './OpenaiTale'
export { default as OpenaiViolet } from './OpenaiViolet' export { default as OpenaiViolet } from './OpenaiViolet'
export { default as OpenaiYellow } from './OpenaiYellow' export { default as OpenaiYellow } from './OpenaiYellow'
export { default as OpenllmText } from './OpenllmText' export { default as OpenllmText } from './OpenllmText'

@ -37,14 +37,15 @@ export default function Radio({
const isChecked = groupContext ? groupContext.value === value : checked const isChecked = groupContext ? groupContext.value === value : checked
const divClassName = ` const divClassName = `
flex items-center py-1 relative flex items-center py-1 relative
px-7 cursor-pointer hover:bg-gray-200 rounded px-7 cursor-pointer text-text-secondary rounded
bg-components-option-card-option-bg hover:bg-components-option-card-option-bg-hover hover:shadow-xs
` `
return ( return (
<div className={cn( <div className={cn(
s.label, s.label,
disabled ? s.disabled : '', disabled ? s.disabled : '',
isChecked ? 'bg-white shadow' : '', isChecked ? 'bg-components-option-card-option-bg-hover shadow-xs' : '',
divClassName, divClassName,
className)} className)}
onClick={() => handleChange(value)} onClick={() => handleChange(value)}

@ -5,7 +5,7 @@ import type {
} from '../declarations' } from '../declarations'
import { useLanguage } from '../hooks' import { useLanguage } from '../hooks'
import { Group } from '@/app/components/base/icons/src/vender/other' import { Group } from '@/app/components/base/icons/src/vender/other'
import { OpenaiBlue, OpenaiTale, OpenaiViolet, OpenaiYellow } from '@/app/components/base/icons/src/public/llm' import { OpenaiBlue, OpenaiTeal, OpenaiViolet, OpenaiYellow } from '@/app/components/base/icons/src/public/llm'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { renderI18nObject } from '@/i18n' import { renderI18nObject } from '@/i18n'
@ -25,7 +25,7 @@ const ModelIcon: FC<ModelIconProps> = ({
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o')) if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o'))
return <div className='flex items-center justify-center'><OpenaiYellow className={cn('h-5 w-5', className)} /></div> return <div className='flex items-center justify-center'><OpenaiYellow className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4.1')) if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4.1'))
return <div className='flex items-center justify-center'><OpenaiTale className={cn('h-5 w-5', className)} /></div> return <div className='flex items-center justify-center'><OpenaiTeal className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4o')) if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4o'))
return <div className='flex items-center justify-center'><OpenaiBlue className={cn('h-5 w-5', className)} /></div> return <div className='flex items-center justify-center'><OpenaiBlue className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('gpt-4')) if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('gpt-4'))

@ -83,7 +83,12 @@ const InstallByDSLList: FC<Props> = ({
useEffect(() => { useEffect(() => {
if (!isFetchingMarketplaceDataById && infoGetById?.data.plugins) { if (!isFetchingMarketplaceDataById && infoGetById?.data.plugins) {
const payloads = infoGetById?.data.plugins const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const p = d as GitHubItemAndMarketPlaceDependency
const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0]
return infoGetById.data.plugins.find(item => item.plugin_id === id)!
})
const payloads = sortedList
const failedIndex: number[] = [] const failedIndex: number[] = []
const nextPlugins = produce(pluginsRef.current, (draft) => { const nextPlugins = produce(pluginsRef.current, (draft) => {
marketPlaceInDSLIndex.forEach((index, i) => { marketPlaceInDSLIndex.forEach((index, i) => {

@ -148,7 +148,7 @@ const CodeEditor: FC<Props> = ({
{isShowVarPicker && ( {isShowVarPicker && (
<div <div
ref={popupRef} ref={popupRef}
className='w-[228px] space-y-1 rounded-lg border border-gray-200 bg-white p-1 shadow-lg' className='w-[228px] space-y-1 rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg'
style={{ style={{
position: 'fixed', position: 'fixed',
top: popupPosition.y, top: popupPosition.y,

@ -43,7 +43,7 @@ const VarReferencePicker: FC<Props> = ({
offset={4} offset={4}
> >
<PortalToFollowElemTrigger onClick={() => setOpen(!open)} className='w-[120px] cursor-pointer'> <PortalToFollowElemTrigger onClick={() => setOpen(!open)} className='w-[120px] cursor-pointer'>
<div className='flex h-8 items-center justify-between rounded-lg border-0 bg-components-button-secondary-bg px-2.5 text-[13px] text-text-primary'> <div className='flex h-8 items-center justify-between rounded-lg border-0 bg-components-input-bg-normal px-2.5 text-[13px] text-text-primary'>
<div className='w-0 grow truncate capitalize' title={value}>{value}</div> <div className='w-0 grow truncate capitalize' title={value}>{value}</div>
<RiArrowDownSLine className='h-3.5 w-3.5 shrink-0 text-text-secondary' /> <RiArrowDownSLine className='h-3.5 w-3.5 shrink-0 text-text-secondary' />
</div> </div>

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import ConditionValueMethod from './condition-value-method' import ConditionValueMethod from './condition-value-method'
import type { ConditionValueMethodProps } from './condition-value-method' import type { ConditionValueMethodProps } from './condition-value-method'
import ConditionVariableSelector from './condition-variable-selector' import ConditionVariableSelector from './condition-variable-selector'
import ConditionCommonVariableSelector from './condition-common-variable-selector.tsx' import ConditionCommonVariableSelector from './condition-common-variable-selector'
import type { import type {
Node, Node,
NodeOutPutVar, NodeOutPutVar,

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import ConditionValueMethod from './condition-value-method' import ConditionValueMethod from './condition-value-method'
import type { ConditionValueMethodProps } from './condition-value-method' import type { ConditionValueMethodProps } from './condition-value-method'
import ConditionVariableSelector from './condition-variable-selector' import ConditionVariableSelector from './condition-variable-selector'
import ConditionCommonVariableSelector from './condition-common-variable-selector.tsx' import ConditionCommonVariableSelector from './condition-common-variable-selector'
import type { import type {
Node, Node,
NodeOutPutVar, NodeOutPutVar,

Loading…
Cancel
Save