From 0babdffe3e5dd1b116f45d22cd6189bac509668b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=99=93=E9=98=B3?= Date: Thu, 24 Apr 2025 18:04:57 +0800 Subject: [PATCH 1/9] feat: support vastbase vector database (#16308) --- api/commands.py | 1 + api/configs/middleware/__init__.py | 2 + .../middleware/vdb/vastbase_vector_config.py | 45 ++++ api/controllers/console/datasets/datasets.py | 2 + .../rag/datasource/vdb/pyvastbase/__init__.py | 0 .../vdb/pyvastbase/vastbase_vector.py | 243 ++++++++++++++++++ api/core/rag/datasource/vdb/vector_factory.py | 4 + api/core/rag/datasource/vdb/vector_type.py | 2 + .../vdb/pyvastbase/__init__.py | 0 .../vdb/pyvastbase/test_vastbase_vector.py | 27 ++ docker/.env.example | 9 + docker/docker-compose-template.yaml | 24 ++ docker/docker-compose.yaml | 31 +++ 13 files changed, 390 insertions(+) create mode 100644 api/configs/middleware/vdb/vastbase_vector_config.py create mode 100644 api/core/rag/datasource/vdb/pyvastbase/__init__.py create mode 100644 api/core/rag/datasource/vdb/pyvastbase/vastbase_vector.py create mode 100644 api/tests/integration_tests/vdb/pyvastbase/__init__.py create mode 100644 api/tests/integration_tests/vdb/pyvastbase/test_vastbase_vector.py diff --git a/api/commands.py b/api/commands.py index e70d6e0b49..c5394c6f87 100644 --- a/api/commands.py +++ b/api/commands.py @@ -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, diff --git a/api/configs/middleware/__init__.py b/api/configs/middleware/__init__.py index c2ad24094a..d285515998 100644 --- a/api/configs/middleware/__init__.py +++ b/api/configs/middleware/__init__.py @@ -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, diff --git a/api/configs/middleware/vdb/vastbase_vector_config.py b/api/configs/middleware/vdb/vastbase_vector_config.py new file mode 100644 index 0000000000..816d6df90a --- /dev/null +++ b/api/configs/middleware/vdb/vastbase_vector_config.py @@ -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, + ) diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py index 752d124735..43615af709 100644 --- a/api/controllers/console/datasets/datasets.py +++ b/api/controllers/console/datasets/datasets.py @@ -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 diff --git a/api/core/rag/datasource/vdb/pyvastbase/__init__.py b/api/core/rag/datasource/vdb/pyvastbase/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/rag/datasource/vdb/pyvastbase/vastbase_vector.py b/api/core/rag/datasource/vdb/pyvastbase/vastbase_vector.py new file mode 100644 index 0000000000..a61d571e16 --- /dev/null +++ b/api/core/rag/datasource/vdb/pyvastbase/vastbase_vector.py @@ -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, + ), + ) diff --git a/api/core/rag/datasource/vdb/vector_factory.py b/api/core/rag/datasource/vdb/vector_factory.py index 05158cc7ca..66e002312a 100644 --- a/api/core/rag/datasource/vdb/vector_factory.py +++ b/api/core/rag/datasource/vdb/vector_factory.py @@ -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 diff --git a/api/core/rag/datasource/vdb/vector_type.py b/api/core/rag/datasource/vdb/vector_type.py index 0421be3458..7a81565e37 100644 --- a/api/core/rag/datasource/vdb/vector_type.py +++ b/api/core/rag/datasource/vdb/vector_type.py @@ -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" diff --git a/api/tests/integration_tests/vdb/pyvastbase/__init__.py b/api/tests/integration_tests/vdb/pyvastbase/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/vdb/pyvastbase/test_vastbase_vector.py b/api/tests/integration_tests/vdb/pyvastbase/test_vastbase_vector.py new file mode 100644 index 0000000000..3d7873442b --- /dev/null +++ b/api/tests/integration_tests/vdb/pyvastbase/test_vastbase_vector.py @@ -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() diff --git a/docker/.env.example b/docker/.env.example index 0b80dccb37..102aba3e8d 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -441,6 +441,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 diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 9da06df2b0..9ab1304492 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -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 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index ea9332bf2e..eb1a13f819 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -163,6 +163,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} @@ -840,6 +847,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 From 759584f8c519f2fa47aa26bb9628fe85b6cd9c96 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 25 Apr 2025 09:06:07 +0800 Subject: [PATCH 2/9] fix: not render conversation var in prompt editor (#18728) --- .../prompt-editor/plugins/workflow-variable-block/component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx index 2f6c3374a7..50ff29633a 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx @@ -144,7 +144,7 @@ const WorkflowVariableBlockComponent = ({ } if (!node) - return null + return Item return ( Date: Fri, 25 Apr 2025 09:42:58 +0800 Subject: [PATCH 3/9] [Lindorm VDB] Add the QUERY_TIMEOUT parameter to force the search query to fail. (#18613) Co-authored-by: jiangzhijie --- api/.env.example | 1 + api/configs/middleware/vdb/lindorm_config.py | 1 + api/core/rag/datasource/vdb/lindorm/lindorm_vector.py | 10 ++++++---- docker/.env.example | 1 + docker/docker-compose.yaml | 1 + 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/api/.env.example b/api/.env.example index b5820fcdc2..2cc6410cdd 100644 --- a/api/.env.example +++ b/api/.env.example @@ -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 diff --git a/api/configs/middleware/vdb/lindorm_config.py b/api/configs/middleware/vdb/lindorm_config.py index 95e1d1cfca..e80e3f4a35 100644 --- a/api/configs/middleware/vdb/lindorm_config.py +++ b/api/configs/middleware/vdb/lindorm_config.py @@ -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) diff --git a/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py b/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py index 643ac2df4e..e9ff1ce43d 100644 --- a/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py +++ b/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py @@ -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: diff --git a/docker/.env.example b/docker/.env.example index 102aba3e8d..5c5ee4b548 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -562,6 +562,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 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index eb1a13f819..c8b7a006e9 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -257,6 +257,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} From 5e2b3b34e588b5ba2c0d8e08524fa337716b89d0 Mon Sep 17 00:00:00 2001 From: just2gooo <7363124+just2gooo@users.noreply.github.com> Date: Fri, 25 Apr 2025 10:08:06 +0800 Subject: [PATCH 4/9] issue: #17056 : Add a reason field to the message_replace event (#17195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 聂政 --- .../advanced_chat/generate_task_pipeline.py | 7 +++++-- api/core/app/entities/queue_entities.py | 8 ++++++++ api/core/app/entities/task_entities.py | 1 + .../based_generate_task_pipeline.py | 6 +++--- .../app/task_pipeline/message_cycle_manage.py | 6 ++++-- api/core/moderation/output_moderation.py | 20 ++++++++++++++----- 6 files changed, 36 insertions(+), 12 deletions(-) diff --git a/api/core/app/apps/advanced_chat/generate_task_pipeline.py b/api/core/app/apps/advanced_chat/generate_task_pipeline.py index 3bf6c330db..baefca0c3f 100644 --- a/api/core/app/apps/advanced_chat/generate_task_pipeline.py +++ b/api/core/app/apps/advanced_chat/generate_task_pipeline.py @@ -684,7 +684,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 +697,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 diff --git a/api/core/app/entities/queue_entities.py b/api/core/app/entities/queue_entities.py index 3702326406..7228020e9b 100644 --- a/api/core/app/entities/queue_entities.py +++ b/api/core/app/entities/queue_entities.py @@ -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): diff --git a/api/core/app/entities/task_entities.py b/api/core/app/entities/task_entities.py index f23ee1b9fd..817699bd20 100644 --- a/api/core/app/entities/task_entities.py +++ b/api/core/app/entities/task_entities.py @@ -148,6 +148,7 @@ class MessageReplaceStreamResponse(StreamResponse): event: StreamEvent = StreamEvent.MESSAGE_REPLACE answer: str + reason: str class AgentThoughtStreamResponse(StreamResponse): diff --git a/api/core/app/task_pipeline/based_generate_task_pipeline.py b/api/core/app/task_pipeline/based_generate_task_pipeline.py index a2e06d4e1f..5331c0cc94 100644 --- a/api/core/app/task_pipeline/based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/based_generate_task_pipeline.py @@ -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 diff --git a/api/core/app/task_pipeline/message_cycle_manage.py b/api/core/app/task_pipeline/message_cycle_manage.py index 6223b33b67..fde506639f 100644 --- a/api/core/app/task_pipeline/message_cycle_manage.py +++ b/api/core/app/task_pipeline/message_cycle_manage.py @@ -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 + ) diff --git a/api/core/moderation/output_moderation.py b/api/core/moderation/output_moderation.py index e595be126c..2ec315417f 100644 --- a/api/core/moderation/output_moderation.py +++ b/api/core/moderation/output_moderation.py @@ -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 From 2627e221f20d0fa39faa13b542aab44b502e529a Mon Sep 17 00:00:00 2001 From: luckylhb90 Date: Fri, 25 Apr 2025 05:08:16 +0300 Subject: [PATCH 5/9] fix: buildin tool provider credentials_for_provider (#18725) Co-authored-by: hobo.l --- api/core/tools/builtin_tool/provider.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/core/tools/builtin_tool/provider.py b/api/core/tools/builtin_tool/provider.py index 4f733f0ea1..cf75bd3d7e 100644 --- a/api/core/tools/builtin_tool/provider.py +++ b/api/core/tools/builtin_tool/provider.py @@ -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( From 12836f9db9fb9da8eab85d414ddf0ec68f8f3434 Mon Sep 17 00:00:00 2001 From: Alex Chim <132866042+AlexChim1231@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:52:25 +0800 Subject: [PATCH 6/9] Resolves #18536 Retreive conversation variables (#18581) --- .../service_api/app/conversation.py | 28 +++++ api/fields/conversation_variable_fields.py | 6 ++ api/services/conversation_service.py | 54 +++++++++- api/services/errors/conversation.py | 4 + .../template/template_advanced_chat.en.mdx | 100 ++++++++++++++++++ .../template/template_advanced_chat.ja.mdx | 100 ++++++++++++++++++ .../template/template_advanced_chat.zh.mdx | 100 ++++++++++++++++++ .../develop/template/template_chat.en.mdx | 100 ++++++++++++++++++ .../develop/template/template_chat.ja.mdx | 100 ++++++++++++++++++ .../develop/template/template_chat.zh.mdx | 100 ++++++++++++++++++ 10 files changed, 691 insertions(+), 1 deletion(-) diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index 334f2c5620..55600a3fd0 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -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//name", endpoint="conversation_name") api.add_resource(ConversationApi, "/conversations") api.add_resource(ConversationDetailApi, "/conversations/", endpoint="conversation_detail") +api.add_resource(ConversationVariablesApi, "/conversations//variables", endpoint="conversation_variables") diff --git a/api/fields/conversation_variable_fields.py b/api/fields/conversation_variable_fields.py index c6385efb5a..3aa3838def 100644 --- a/api/fields/conversation_variable_fields.py +++ b/api/fields/conversation_variable_fields.py @@ -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)), +} diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index 6485cbf37d..afdaa49465 100644 --- a/api/services/conversation_service.py +++ b/api/services/conversation_service.py @@ -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) diff --git a/api/services/errors/conversation.py b/api/services/errors/conversation.py index 139dd9a70a..f8051e3417 100644 --- a/api/services/errors/conversation.py +++ b/api/services/errors/conversation.py @@ -11,3 +11,7 @@ class ConversationNotExistsError(BaseServiceError): class ConversationCompletedError(Exception): pass + + +class ConversationVariableNotExistsError(BaseServiceError): + pass diff --git a/web/app/components/develop/template/template_advanced_chat.en.mdx b/web/app/components/develop/template/template_advanced_chat.en.mdx index 9502f20124..f645133030 100644 --- a/web/app/components/develop/template/template_advanced_chat.en.mdx +++ b/web/app/components/develop/template/template_advanced_chat.en.mdx @@ -845,6 +845,106 @@ Chat applications support session persistence, allowing previous chat history to --- + + + + Retrieve variables from a specific conversation. This endpoint is useful for extracting structured data that was captured during the conversation. + + ### Path Parameters + + + + The ID of the conversation to retrieve variables from. + + + + ### Query Parameters + + + + The user identifier, defined by the developer, must ensure uniqueness within the application + + + (Optional) The ID of the last record on the current page, default is null. + + + (Optional) How many records to return in one request, default is the most recent 20 entries. Maximum 100, minimum 1. + + + + ### 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 + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```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}' + ``` + + + + ```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 + } + ] + } + ``` + + + + +--- + + + + 特定の会話から変数を取得します。このエンドポイントは、会話中に取得された構造化データを抽出するのに役立ちます。 + + ### パスパラメータ + + + + 変数を取得する会話のID。 + + + + ### クエリパラメータ + + + + ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。 + + + (Optional)現在のページの最後の記録のID、デフォルトはnullです。 + + + (Optional)1回のリクエストで返す記録の数、デフォルトは最新の20件です。最大100、最小1。 + + + + ### レスポンス + + - `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`, 会話が見つかりません + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```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}' + ``` + + + + ```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 + } + ] + } + ``` + + + + +--- + + + + 从特定对话中检索变量。此端点对于提取对话过程中捕获的结构化数据非常有用。 + + ### 路径参数 + + + + 要从中检索变量的对话ID。 + + + + ### 查询参数 + + + + 用户标识符,由开发人员定义的规则,在应用程序内必须唯一。 + + + (选填)当前页最后面一条记录的 ID,默认 null + + + (选填)一次请求返回多少条记录,默认 20 条,最大 100 条,最小 1 条。 + + + + ### 响应 + + - `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`, 对话不存在 + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```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}' + ``` + + + + ```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 + } + ] + } + ``` + + + + +--- + + + + Retrieve variables from a specific conversation. This endpoint is useful for extracting structured data that was captured during the conversation. + + ### Path Parameters + + + + The ID of the conversation to retrieve variables from. + + + + ### Query Parameters + + + + The user identifier, defined by the developer, must ensure uniqueness within the application + + + (Optional) The ID of the last record on the current page, default is null. + + + (Optional) How many records to return in one request, default is the most recent 20 entries. Maximum 100, minimum 1. + + + + ### 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 + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```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}' + ``` + + + + ```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 + } + ] + } + ``` + + + + +--- + + + + 特定の会話から変数を取得します。このエンドポイントは、会話中に取得された構造化データを抽出するのに役立ちます。 + + ### パスパラメータ + + + + 変数を取得する会話のID。 + + + + ### クエリパラメータ + + + + ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。 + + + (Optional)現在のページの最後のレコードのID、デフォルトはnullです。 + + + (Optional)1回のリクエストで返すレコードの数、デフォルトは最新の20件です。最大100、最小1。 + + + + ### レスポンス + + - `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`, 会話が見つかりません + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```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}' + ``` + + + + ```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 + } + ] + } + ``` + + + + +--- + + + + 从特定对话中检索变量。此端点对于提取对话过程中捕获的结构化数据非常有用。 + + ### 路径参数 + + + + 要从中检索变量的对话ID。 + + + + ### 查询参数 + + + + 用户标识符,由开发人员定义的规则,在应用程序内必须唯一。 + + + (选填)当前页最后面一条记录的 ID,默认 null + + + (选填)一次请求返回多少条记录,默认 20 条,最大 100 条,最小 1 条。 + + + + ### 响应 + + - `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`, 对话不存在 + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```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}' + ``` + + + + ```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 + } + ] + } + ``` + + + + +--- + Date: Fri, 25 Apr 2025 12:12:30 +0800 Subject: [PATCH 7/9] fix: enable Milvus database configuration via .env(#18707) (#18714) Co-authored-by: jiawei.wang Co-authored-by: crazywoola <427733928@qq.com> --- docker/.env.example | 1 + docker/docker-compose.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/docker/.env.example b/docker/.env.example index 5c5ee4b548..83d975cec5 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -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= diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index c8b7a006e9..8edcd497c6 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -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:-} From fc4e11d12760ccdeccfa59c96672eba5aeb1d0f6 Mon Sep 17 00:00:00 2001 From: crStiv Date: Fri, 25 Apr 2025 08:42:10 +0300 Subject: [PATCH 8/9] fix: wording in README.md (#18751) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 65e8001dd2..efb37d6083 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ README in বাংলা

-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
** 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.
- > 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 From a575fbca9414bc0d684f92d73dcc8b027dc7eb07 Mon Sep 17 00:00:00 2001 From: NFish Date: Fri, 25 Apr 2025 14:37:04 +0800 Subject: [PATCH 9/9] fix: update api rate limit on Pricing page (#18755) --- web/app/components/billing/config.ts | 8 ++++++-- .../components/billing/pricing/plan-item.tsx | 19 ++++++++++++++----- web/app/components/billing/type.ts | 6 +++++- web/context/provider-context.tsx | 3 ++- web/i18n/en-US/billing.ts | 4 ++++ web/i18n/ja-JP/billing.ts | 10 +++++++--- web/i18n/zh-Hans/billing.ts | 4 ++++ 7 files changed, 42 insertions(+), 12 deletions(-) diff --git a/web/app/components/billing/config.ts b/web/app/components/billing/config.ts index 52651259ef..1d5fbc7491 100644 --- a/web/app/components/billing/config.ts +++ b/web/app/components/billing/config.ts @@ -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 = { +export const ALL_PLANS: Record = { sandbox: { level: 1, price: 0, @@ -22,6 +23,7 @@ export const ALL_PLANS: Record = { vectorSpace: '50MB', documentsUploadQuota: 0, documentsRequestQuota: 10, + apiRateLimit: 5000, documentProcessingPriority: Priority.standard, messageRequest: 200, annotatedResponse: 10, @@ -38,6 +40,7 @@ export const ALL_PLANS: Record = { 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 = { 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 = { } export const defaultPlan = { - type: Plan.sandbox, + type: Plan.sandbox as BasicPlan, usage: { documents: 50, vectorSpace: 1, diff --git a/web/app/components/billing/pricing/plan-item.tsx b/web/app/components/billing/pricing/plan-item.tsx index a0b8685989..07af0ffec8 100644 --- a/web/app/components/billing/pricing/plan-item.tsx +++ b/web/app/components/billing/pricing/plan-item.tsx @@ -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 = ({
{style[plan].icon}
-
{t(`${i18nPrefix}.name`)}
- {isMostPopularPlan &&
+
{t(`${i18nPrefix}.name`)}
+ {isMostPopularPlan &&
@@ -205,6 +206,14 @@ const PlanItem: FC = ({ label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })} tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')} /> + } + 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} + /> } label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')} diff --git a/web/app/components/billing/type.ts b/web/app/components/billing/type.ts index 28bce37098..2f5728ceef 100644 --- a/web/app/components/billing/type.ts +++ b/web/app/components/billing/type.ts @@ -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: { diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index bd997380e7..90af9aae0c 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -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 } diff --git a/web/i18n/en-US/billing.ts b/web/i18n/en-US/billing.ts index 893e730842..57358dcf36 100644 --- a/web/i18n/en-US/billing.ts +++ b/web/i18n/en-US/billing.ts @@ -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: { diff --git a/web/i18n/ja-JP/billing.ts b/web/i18n/ja-JP/billing.ts index 891779d0b6..bcb509b85b 100644 --- a/web/i18n/ja-JP/billing.ts +++ b/web/i18n/ja-JP/billing.ts @@ -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: '成長期のチームに必要な機能を備えたプラン', }, diff --git a/web/i18n/zh-Hans/billing.ts b/web/i18n/zh-Hans/billing.ts index 8bddbfc2ba..e5fbff77b0 100644 --- a/web/i18n/zh-Hans/billing.ts +++ b/web/i18n/zh-Hans/billing.ts @@ -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: {