diff --git a/README.md b/README.md index 297595c58..1d8e3415b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Static Badge - docker pull infiniflow/ragflow:v0.22.0 + docker pull infiniflow/ragflow:v0.22.1 Latest Release @@ -85,6 +85,7 @@ Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io). ## 🔥 Latest Updates +- 2025-11-19 Supports Gemini 3 Pro. - 2025-11-12 Supports data synchronization from Confluence, AWS S3, Discord, Google Drive. - 2025-10-23 Supports MinerU & Docling as document parsing methods. - 2025-10-15 Supports orchestrable ingestion pipeline. @@ -93,8 +94,6 @@ Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io). - 2025-05-23 Adds a Python/JavaScript code executor component to Agent. - 2025-05-05 Supports cross-language query. - 2025-03-19 Supports using a multi-modal model to make sense of images within PDF or DOCX files. -- 2024-12-18 Upgrades Document Layout Analysis model in DeepDoc. -- 2024-08-22 Support text to SQL statements through RAG. ## 🎉 Stay Tuned @@ -188,12 +187,12 @@ releases! 🌟 > All Docker images are built for x86 platforms. We don't currently offer Docker images for ARM64. > If you are on an ARM64 platform, follow [this guide](https://ragflow.io/docs/dev/build_docker_image) to build a Docker image compatible with your system. -> The command below downloads the `v0.22.0` edition of the RAGFlow Docker image. See the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.22.0`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server. +> The command below downloads the `v0.22.1` edition of the RAGFlow Docker image. See the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.22.1`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server. ```bash $ cd ragflow/docker - # Optional: use a stable tag (see releases: https://github.com/infiniflow/ragflow/releases), e.g.: git checkout v0.22.0 + # Optional: use a stable tag (see releases: https://github.com/infiniflow/ragflow/releases), e.g.: git checkout v0.22.1 # This steps ensures the **entrypoint.sh** file in the code matches the Docker image version. # Use CPU for DeepDoc tasks: diff --git a/README_id.md b/README_id.md index b5230c8bc..7c2aa1337 100644 --- a/README_id.md +++ b/README_id.md @@ -22,7 +22,7 @@ Lencana Daring - docker pull infiniflow/ragflow:v0.22.0 + docker pull infiniflow/ragflow:v0.22.1 Rilis Terbaru @@ -85,6 +85,7 @@ Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io). ## 🔥 Pembaruan Terbaru +- 2025-11-19 Mendukung Gemini 3 Pro. - 2025-11-12 Mendukung sinkronisasi data dari Confluence, AWS S3, Discord, Google Drive. - 2025-10-23 Mendukung MinerU & Docling sebagai metode penguraian dokumen. - 2025-10-15 Dukungan untuk jalur data yang terorkestrasi. @@ -186,12 +187,12 @@ Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io). > Semua gambar Docker dibangun untuk platform x86. Saat ini, kami tidak menawarkan gambar Docker untuk ARM64. > Jika Anda menggunakan platform ARM64, [silakan gunakan panduan ini untuk membangun gambar Docker yang kompatibel dengan sistem Anda](https://ragflow.io/docs/dev/build_docker_image). -> Perintah di bawah ini mengunduh edisi v0.22.0 dari gambar Docker RAGFlow. Silakan merujuk ke tabel berikut untuk deskripsi berbagai edisi RAGFlow. Untuk mengunduh edisi RAGFlow yang berbeda dari v0.22.0, perbarui variabel RAGFLOW_IMAGE di docker/.env sebelum menggunakan docker compose untuk memulai server. +> Perintah di bawah ini mengunduh edisi v0.22.1 dari gambar Docker RAGFlow. Silakan merujuk ke tabel berikut untuk deskripsi berbagai edisi RAGFlow. Untuk mengunduh edisi RAGFlow yang berbeda dari v0.22.1, perbarui variabel RAGFLOW_IMAGE di docker/.env sebelum menggunakan docker compose untuk memulai server. ```bash $ cd ragflow/docker - # Opsional: gunakan tag stabil (lihat releases: https://github.com/infiniflow/ragflow/releases), contoh: git checkout v0.22.0 + # Opsional: gunakan tag stabil (lihat releases: https://github.com/infiniflow/ragflow/releases), contoh: git checkout v0.22.1 # This steps ensures the **entrypoint.sh** file in the code matches the Docker image version. # Use CPU for DeepDoc tasks: diff --git a/README_ja.md b/README_ja.md index 96dc661e3..e72ae77f1 100644 --- a/README_ja.md +++ b/README_ja.md @@ -22,7 +22,7 @@ Static Badge - docker pull infiniflow/ragflow:v0.22.0 + docker pull infiniflow/ragflow:v0.22.1 Latest Release @@ -66,6 +66,7 @@ ## 🔥 最新情報 +- 2025-11-19 Gemini 3 Proをサポートしています - 2025-11-12 Confluence、AWS S3、Discord、Google Drive からのデータ同期をサポートします。 - 2025-10-23 ドキュメント解析方法として MinerU と Docling をサポートします。 - 2025-10-15 オーケストレーションされたデータパイプラインのサポート。 @@ -166,12 +167,12 @@ > 現在、公式に提供されているすべての Docker イメージは x86 アーキテクチャ向けにビルドされており、ARM64 用の Docker イメージは提供されていません。 > ARM64 アーキテクチャのオペレーティングシステムを使用している場合は、[このドキュメント](https://ragflow.io/docs/dev/build_docker_image)を参照して Docker イメージを自分でビルドしてください。 -> 以下のコマンドは、RAGFlow Docker イメージの v0.22.0 エディションをダウンロードします。異なる RAGFlow エディションの説明については、以下の表を参照してください。v0.22.0 とは異なるエディションをダウンロードするには、docker/.env ファイルの RAGFLOW_IMAGE 変数を適宜更新し、docker compose を使用してサーバーを起動してください。 +> 以下のコマンドは、RAGFlow Docker イメージの v0.22.1 エディションをダウンロードします。異なる RAGFlow エディションの説明については、以下の表を参照してください。v0.22.1 とは異なるエディションをダウンロードするには、docker/.env ファイルの RAGFLOW_IMAGE 変数を適宜更新し、docker compose を使用してサーバーを起動してください。 ```bash $ cd ragflow/docker - # 任意: 安定版タグを利用 (一覧: https://github.com/infiniflow/ragflow/releases) 例: git checkout v0.22.0 + # 任意: 安定版タグを利用 (一覧: https://github.com/infiniflow/ragflow/releases) 例: git checkout v0.22.1 # この手順は、コード内の entrypoint.sh ファイルが Docker イメージのバージョンと一致していることを確認します。 # Use CPU for DeepDoc tasks: diff --git a/README_ko.md b/README_ko.md index 51f4169ff..4a19de8cb 100644 --- a/README_ko.md +++ b/README_ko.md @@ -22,7 +22,7 @@ Static Badge - docker pull infiniflow/ragflow:v0.22.0 + docker pull infiniflow/ragflow:v0.22.1 Latest Release @@ -67,6 +67,7 @@ ## 🔥 업데이트 +- 2025-11-19 Gemini 3 Pro를 지원합니다. - 2025-11-12 Confluence, AWS S3, Discord, Google Drive에서 데이터 동기화를 지원합니다. - 2025-10-23 문서 파싱 방법으로 MinerU 및 Docling을 지원합니다. - 2025-10-15 조정된 데이터 파이프라인 지원. @@ -168,12 +169,12 @@ > 모든 Docker 이미지는 x86 플랫폼을 위해 빌드되었습니다. 우리는 현재 ARM64 플랫폼을 위한 Docker 이미지를 제공하지 않습니다. > ARM64 플랫폼을 사용 중이라면, [시스템과 호환되는 Docker 이미지를 빌드하려면 이 가이드를 사용해 주세요](https://ragflow.io/docs/dev/build_docker_image). - > 아래 명령어는 RAGFlow Docker 이미지의 v0.22.0 버전을 다운로드합니다. 다양한 RAGFlow 버전에 대한 설명은 다음 표를 참조하십시오. v0.22.0과 다른 RAGFlow 버전을 다운로드하려면, docker/.env 파일에서 RAGFLOW_IMAGE 변수를 적절히 업데이트한 후 docker compose를 사용하여 서버를 시작하십시오. + > 아래 명령어는 RAGFlow Docker 이미지의 v0.22.1 버전을 다운로드합니다. 다양한 RAGFlow 버전에 대한 설명은 다음 표를 참조하십시오. v0.22.1과 다른 RAGFlow 버전을 다운로드하려면, docker/.env 파일에서 RAGFLOW_IMAGE 변수를 적절히 업데이트한 후 docker compose를 사용하여 서버를 시작하십시오. ```bash $ cd ragflow/docker - # Optional: use a stable tag (see releases: https://github.com/infiniflow/ragflow/releases), e.g.: git checkout v0.22.0 + # Optional: use a stable tag (see releases: https://github.com/infiniflow/ragflow/releases), e.g.: git checkout v0.22.1 # 이 단계는 코드의 entrypoint.sh 파일이 Docker 이미지 버전과 일치하도록 보장합니다. # Use CPU for DeepDoc tasks: diff --git a/README_pt_br.md b/README_pt_br.md index 5d4d39e5e..c963874f4 100644 --- a/README_pt_br.md +++ b/README_pt_br.md @@ -22,7 +22,7 @@ Badge Estático - docker pull infiniflow/ragflow:v0.22.0 + docker pull infiniflow/ragflow:v0.22.1 Última Versão @@ -86,6 +86,7 @@ Experimente nossa demo em [https://demo.ragflow.io](https://demo.ragflow.io). ## 🔥 Últimas Atualizações +- 19-11-2025 Suporta Gemini 3 Pro. - 12-11-2025 Suporta a sincronização de dados do Confluence, AWS S3, Discord e Google Drive. - 23-10-2025 Suporta MinerU e Docling como métodos de análise de documentos. - 15-10-2025 Suporte para pipelines de dados orquestrados. @@ -186,12 +187,12 @@ Experimente nossa demo em [https://demo.ragflow.io](https://demo.ragflow.io). > Todas as imagens Docker são construídas para plataformas x86. Atualmente, não oferecemos imagens Docker para ARM64. > Se você estiver usando uma plataforma ARM64, por favor, utilize [este guia](https://ragflow.io/docs/dev/build_docker_image) para construir uma imagem Docker compatível com o seu sistema. - > O comando abaixo baixa a edição`v0.22.0` da imagem Docker do RAGFlow. Consulte a tabela a seguir para descrições de diferentes edições do RAGFlow. Para baixar uma edição do RAGFlow diferente da `v0.22.0`, atualize a variável `RAGFLOW_IMAGE` conforme necessário no **docker/.env** antes de usar `docker compose` para iniciar o servidor. + > O comando abaixo baixa a edição`v0.22.1` da imagem Docker do RAGFlow. Consulte a tabela a seguir para descrições de diferentes edições do RAGFlow. Para baixar uma edição do RAGFlow diferente da `v0.22.1`, atualize a variável `RAGFLOW_IMAGE` conforme necessário no **docker/.env** antes de usar `docker compose` para iniciar o servidor. ```bash $ cd ragflow/docker - # Opcional: use uma tag estável (veja releases: https://github.com/infiniflow/ragflow/releases), ex.: git checkout v0.22.0 + # Opcional: use uma tag estável (veja releases: https://github.com/infiniflow/ragflow/releases), ex.: git checkout v0.22.1 # Esta etapa garante que o arquivo entrypoint.sh no código corresponda à versão da imagem do Docker. # Use CPU for DeepDoc tasks: diff --git a/README_tzh.md b/README_tzh.md index 57f2e8196..bdcb5ea0b 100644 --- a/README_tzh.md +++ b/README_tzh.md @@ -22,7 +22,7 @@ Static Badge - docker pull infiniflow/ragflow:v0.22.0 + docker pull infiniflow/ragflow:v0.22.1 Latest Release @@ -85,6 +85,7 @@ ## 🔥 近期更新 +- 2025-11-19 支援 Gemini 3 Pro. - 2025-11-12 支援從 Confluence、AWS S3、Discord、Google Drive 進行資料同步。 - 2025-10-23 支援 MinerU 和 Docling 作為文件解析方法。 - 2025-10-15 支援可編排的資料管道。 @@ -185,12 +186,12 @@ > 所有 Docker 映像檔都是為 x86 平台建置的。目前,我們不提供 ARM64 平台的 Docker 映像檔。 > 如果您使用的是 ARM64 平台,請使用 [這份指南](https://ragflow.io/docs/dev/build_docker_image) 來建置適合您系統的 Docker 映像檔。 -> 執行以下指令會自動下載 RAGFlow Docker 映像 `v0.22.0`。請參考下表查看不同 Docker 發行版的說明。如需下載不同於 `v0.22.0` 的 Docker 映像,請在執行 `docker compose` 啟動服務之前先更新 **docker/.env** 檔案內的 `RAGFLOW_IMAGE` 變數。 +> 執行以下指令會自動下載 RAGFlow Docker 映像 `v0.22.1`。請參考下表查看不同 Docker 發行版的說明。如需下載不同於 `v0.22.1` 的 Docker 映像,請在執行 `docker compose` 啟動服務之前先更新 **docker/.env** 檔案內的 `RAGFLOW_IMAGE` 變數。 ```bash $ cd ragflow/docker - # 可選:使用穩定版標籤(查看發佈:https://github.com/infiniflow/ragflow/releases),例:git checkout v0.22.0 + # 可選:使用穩定版標籤(查看發佈:https://github.com/infiniflow/ragflow/releases),例:git checkout v0.22.1 # 此步驟確保程式碼中的 entrypoint.sh 檔案與 Docker 映像版本一致。 # Use CPU for DeepDoc tasks: diff --git a/README_zh.md b/README_zh.md index 91004f549..8a10eaa22 100644 --- a/README_zh.md +++ b/README_zh.md @@ -22,7 +22,7 @@ Static Badge - docker pull infiniflow/ragflow:v0.22.0 + docker pull infiniflow/ragflow:v0.22.1 Latest Release @@ -85,6 +85,7 @@ ## 🔥 近期更新 +- 2025-11-19 支持 Gemini 3 Pro. - 2025-11-12 支持从 Confluence、AWS S3、Discord、Google Drive 进行数据同步。 - 2025-10-23 支持 MinerU 和 Docling 作为文档解析方法。 - 2025-10-15 支持可编排的数据管道。 @@ -186,12 +187,12 @@ > 请注意,目前官方提供的所有 Docker 镜像均基于 x86 架构构建,并不提供基于 ARM64 的 Docker 镜像。 > 如果你的操作系统是 ARM64 架构,请参考[这篇文档](https://ragflow.io/docs/dev/build_docker_image)自行构建 Docker 镜像。 - > 运行以下命令会自动下载 RAGFlow Docker 镜像 `v0.22.0`。请参考下表查看不同 Docker 发行版的描述。如需下载不同于 `v0.22.0` 的 Docker 镜像,请在运行 `docker compose` 启动服务之前先更新 **docker/.env** 文件内的 `RAGFLOW_IMAGE` 变量。 + > 运行以下命令会自动下载 RAGFlow Docker 镜像 `v0.22.1`。请参考下表查看不同 Docker 发行版的描述。如需下载不同于 `v0.22.1` 的 Docker 镜像,请在运行 `docker compose` 启动服务之前先更新 **docker/.env** 文件内的 `RAGFLOW_IMAGE` 变量。 ```bash $ cd ragflow/docker - # 可选:使用稳定版本标签(查看发布:https://github.com/infiniflow/ragflow/releases),例如:git checkout v0.22.0 + # 可选:使用稳定版本标签(查看发布:https://github.com/infiniflow/ragflow/releases),例如:git checkout v0.22.1 # 这一步确保代码中的 entrypoint.sh 文件与 Docker 镜像的版本保持一致。 # Use CPU for DeepDoc tasks: diff --git a/admin/client/README.md b/admin/client/README.md index 07de0ab69..01a027839 100644 --- a/admin/client/README.md +++ b/admin/client/README.md @@ -48,7 +48,7 @@ It consists of a server-side Service and a command-line client (CLI), both imple 1. Ensure the Admin Service is running. 2. Install ragflow-cli. ```bash - pip install ragflow-cli==0.22.0 + pip install ragflow-cli==0.22.1 ``` 3. Launch the CLI client: ```bash diff --git a/admin/server/routes.py b/admin/server/routes.py index 20bffb514..29a4bbd22 100644 --- a/admin/server/routes.py +++ b/admin/server/routes.py @@ -30,12 +30,12 @@ admin_bp = Blueprint('admin', __name__, url_prefix='/api/v1/admin') @admin_bp.route('/login', methods=['POST']) -async def login(): - if not await request.json: +def login(): + if not request.json: return error_response('Authorize admin failed.' ,400) try: - email = await request.json.get("email", "") - password = await request.json.get("password", "") + email = request.json.get("email", "") + password = request.json.get("password", "") return login_admin(email, password) except Exception as e: return error_response(str(e), 500) @@ -76,9 +76,9 @@ def list_users(): @admin_bp.route('/users', methods=['POST']) @login_required @check_admin_auth -async def create_user(): +def create_user(): try: - data = await request.get_json() + data = request.get_json() if not data or 'username' not in data or 'password' not in data: return error_response("Username and password are required", 400) @@ -120,9 +120,9 @@ def delete_user(username): @admin_bp.route('/users//password', methods=['PUT']) @login_required @check_admin_auth -async def change_password(username): +def change_password(username): try: - data = await request.get_json() + data = request.get_json() if not data or 'new_password' not in data: return error_response("New password is required", 400) @@ -139,9 +139,9 @@ async def change_password(username): @admin_bp.route('/users//activate', methods=['PUT']) @login_required @check_admin_auth -async def alter_user_activate_status(username): +def alter_user_activate_status(username): try: - data = await request.get_json() + data = request.get_json() if not data or 'activate_status' not in data: return error_response("Activation status is required", 400) activate_status = data['activate_status'] @@ -253,9 +253,9 @@ def restart_service(service_id): @admin_bp.route('/roles', methods=['POST']) @login_required @check_admin_auth -async def create_role(): +def create_role(): try: - data = await request.get_json() + data = request.get_json() if not data or 'role_name' not in data: return error_response("Role name is required", 400) role_name: str = data['role_name'] @@ -269,9 +269,9 @@ async def create_role(): @admin_bp.route('/roles/', methods=['PUT']) @login_required @check_admin_auth -async def update_role(role_name: str): +def update_role(role_name: str): try: - data = await request.get_json() + data = request.get_json() if not data or 'description' not in data: return error_response("Role description is required", 400) description: str = data['description'] @@ -317,9 +317,9 @@ def get_role_permission(role_name: str): @admin_bp.route('/roles//permission', methods=['POST']) @login_required @check_admin_auth -async def grant_role_permission(role_name: str): +def grant_role_permission(role_name: str): try: - data = await request.get_json() + data = request.get_json() if not data or 'actions' not in data or 'resource' not in data: return error_response("Permission is required", 400) actions: list = data['actions'] @@ -333,9 +333,9 @@ async def grant_role_permission(role_name: str): @admin_bp.route('/roles//permission', methods=['DELETE']) @login_required @check_admin_auth -async def revoke_role_permission(role_name: str): +def revoke_role_permission(role_name: str): try: - data = await request.get_json() + data = request.get_json() if not data or 'actions' not in data or 'resource' not in data: return error_response("Permission is required", 400) actions: list = data['actions'] @@ -349,9 +349,9 @@ async def revoke_role_permission(role_name: str): @admin_bp.route('/users//role', methods=['PUT']) @login_required @check_admin_auth -async def update_user_role(user_name: str): +def update_user_role(user_name: str): try: - data = await request.get_json() + data = request.get_json() if not data or 'role_name' not in data: return error_response("Role name is required", 400) role_name: str = data['role_name'] diff --git a/agent/canvas.py b/agent/canvas.py index c156c133c..667e457f5 100644 --- a/agent/canvas.py +++ b/agent/canvas.py @@ -216,6 +216,38 @@ class Graph: else: cur = getattr(cur, key, None) return cur + + def set_variable_value(self, exp: str,value): + exp = exp.strip("{").strip("}").strip(" ").strip("{").strip("}") + if exp.find("@") < 0: + self.globals[exp] = value + return + cpn_id, var_nm = exp.split("@") + cpn = self.get_component(cpn_id) + if not cpn: + raise Exception(f"Can't find variable: '{cpn_id}@{var_nm}'") + parts = var_nm.split(".", 1) + root_key = parts[0] + rest = parts[1] if len(parts) > 1 else "" + if not rest: + cpn["obj"].set_output(root_key, value) + return + root_val = cpn["obj"].output(root_key) + if not root_val: + root_val = {} + cpn["obj"].set_output(root_key, self.set_variable_param_value(root_val,rest,value)) + + def set_variable_param_value(self, obj: Any, path: str, value) -> Any: + cur = obj + keys = path.split('.') + if not path: + return value + for key in keys: + if key not in cur or not isinstance(cur[key], dict): + cur[key] = {} + cur = cur[key] + cur[keys[-1]] = value + return obj def is_canceled(self) -> bool: return has_canceled(self.task_id) @@ -269,7 +301,7 @@ class Canvas(Graph): self.retrieval = [] self.memory = [] for k in self.globals.keys(): - if k.startswith("sys."): + if k.startswith("sys.") or k.startswith("env."): if isinstance(self.globals[k], str): self.globals[k] = "" elif isinstance(self.globals[k], int): diff --git a/agent/component/data_operations.py b/agent/component/data_operations.py index fab7d8c0f..cddd20996 100644 --- a/agent/component/data_operations.py +++ b/agent/component/data_operations.py @@ -1,3 +1,18 @@ +# +# Copyright 2024 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# from abc import ABC import ast import os diff --git a/agent/component/iteration.py b/agent/component/iteration.py index a39147d8f..cff09d622 100644 --- a/agent/component/iteration.py +++ b/agent/component/iteration.py @@ -32,6 +32,7 @@ class IterationParam(ComponentParamBase): def __init__(self): super().__init__() self.items_ref = "" + self.veriable={} def get_input_form(self) -> dict[str, dict]: return { diff --git a/agent/component/list_operations.py b/agent/component/list_operations.py index 9ae8c2e04..6016f7585 100644 --- a/agent/component/list_operations.py +++ b/agent/component/list_operations.py @@ -47,7 +47,9 @@ class ListOperations(ComponentBase,ABC): def _invoke(self, **kwargs): self.input_objects=[] inputs = getattr(self._param, "query", None) - self.inputs=self._canvas.get_variable_value(inputs) + self.inputs = self._canvas.get_variable_value(inputs) + if not isinstance(self.inputs, list): + raise TypeError("The input of List Operations should be an array.") self.set_input_value(inputs, self.inputs) if self._param.operations == "topN": self._topN() diff --git a/agent/component/variable_assigner.py b/agent/component/variable_assigner.py new file mode 100644 index 000000000..08b283343 --- /dev/null +++ b/agent/component/variable_assigner.py @@ -0,0 +1,192 @@ +# +# Copyright 2024 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from abc import ABC +import os +import numbers +from agent.component.base import ComponentBase, ComponentParamBase +from api.utils.api_utils import timeout + +class VariableAssignerParam(ComponentParamBase): + """ + Define the Variable Assigner component parameters. + """ + def __init__(self): + super().__init__() + self.variables=[] + + def check(self): + return True + + def get_input_form(self) -> dict[str, dict]: + return { + "items": { + "type": "json", + "name": "Items" + } + } + +class VariableAssigner(ComponentBase,ABC): + component_name = "VariableAssigner" + + @timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60))) + def _invoke(self, **kwargs): + if not isinstance(self._param.variables,list): + return + else: + for item in self._param.variables: + if any([not item.get("variable"), not item.get("operator"), not item.get("parameter")]): + assert "Variable is not complete." + variable=item["variable"] + operator=item["operator"] + parameter=item["parameter"] + variable_value=self._canvas.get_variable_value(variable) + new_variable=self._operate(variable_value,operator,parameter) + self._canvas.set_variable_value(variable, new_variable) + + def _operate(self,variable,operator,parameter): + if operator == "overwrite": + return self._overwrite(parameter) + elif operator == "clear": + return self._clear(variable) + elif operator == "set": + return self._set(variable,parameter) + elif operator == "append": + return self._append(variable,parameter) + elif operator == "extend": + return self._extend(variable,parameter) + elif operator == "remove_first": + return self._remove_first(variable) + elif operator == "remove_last": + return self._remove_last(variable) + elif operator == "+=": + return self._add(variable,parameter) + elif operator == "-=": + return self._subtract(variable,parameter) + elif operator == "*=": + return self._multiply(variable,parameter) + elif operator == "/=": + return self._divide(variable,parameter) + else: + return + + def _overwrite(self,parameter): + return self._canvas.get_variable_value(parameter) + + def _clear(self,variable): + if isinstance(variable,list): + return [] + elif isinstance(variable,str): + return "" + elif isinstance(variable,dict): + return {} + elif isinstance(variable,int): + return 0 + elif isinstance(variable,float): + return 0.0 + elif isinstance(variable,bool): + return False + else: + return None + + def _set(self,variable,parameter): + if variable is None: + return self._canvas.get_value_with_variable(parameter) + elif isinstance(variable,str): + return self._canvas.get_value_with_variable(parameter) + elif isinstance(variable,bool): + return parameter + elif isinstance(variable,int): + return parameter + elif isinstance(variable,float): + return parameter + else: + return parameter + + def _append(self,variable,parameter): + parameter=self._canvas.get_variable_value(parameter) + if variable is None: + variable=[] + if not isinstance(variable,list): + return "ERROR:VARIABLE_NOT_LIST" + elif len(variable)!=0 and not isinstance(parameter,type(variable[0])): + return "ERROR:PARAMETER_NOT_LIST_ELEMENT_TYPE" + else: + variable.append(parameter) + return variable + + def _extend(self,variable,parameter): + parameter=self._canvas.get_variable_value(parameter) + if variable is None: + variable=[] + if not isinstance(variable,list): + return "ERROR:VARIABLE_NOT_LIST" + elif not isinstance(parameter,list): + return "ERROR:PARAMETER_NOT_LIST" + elif len(variable)!=0 and len(parameter)!=0 and not isinstance(parameter[0],type(variable[0])): + return "ERROR:PARAMETER_NOT_LIST_ELEMENT_TYPE" + else: + return variable + parameter + + def _remove_first(self,variable): + if len(variable)==0: + return variable + if not isinstance(variable,list): + return "ERROR:VARIABLE_NOT_LIST" + else: + return variable[1:] + + def _remove_last(self,variable): + if len(variable)==0: + return variable + if not isinstance(variable,list): + return "ERROR:VARIABLE_NOT_LIST" + else: + return variable[:-1] + + def is_number(self, value): + if isinstance(value, bool): + return False + return isinstance(value, numbers.Number) + + def _add(self,variable,parameter): + if self.is_number(variable) and self.is_number(parameter): + return variable + parameter + else: + return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER" + + def _subtract(self,variable,parameter): + if self.is_number(variable) and self.is_number(parameter): + return variable - parameter + else: + return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER" + + def _multiply(self,variable,parameter): + if self.is_number(variable) and self.is_number(parameter): + return variable * parameter + else: + return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER" + + def _divide(self,variable,parameter): + if self.is_number(variable) and self.is_number(parameter): + if parameter==0: + return "ERROR:DIVIDE_BY_ZERO" + else: + return variable/parameter + else: + return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER" + + def thoughts(self) -> str: + return "Assign variables from canvas." \ No newline at end of file diff --git a/agent/templates/customer_service.json b/agent/templates/customer_service.json index 24e022fed..fc3704e53 100644 --- a/agent/templates/customer_service.json +++ b/agent/templates/customer_service.json @@ -11,707 +11,952 @@ "zh": "多智能体系统,用于智能客服场景。基于用户意图分类,使用主智能体识别用户需求类型,并将任务分配给子智能体进行处理。"}, "canvas_type": "Agent", "dsl": { - "components": { - "Agent:RottenRiversDo": { - "downstream": [ - "Message:PurpleCitiesSee" - ], - "obj": { - "component_name": "Agent", - "params": { - "delay_after_error": 1, - "description": "", - "exception_comment": "", - "exception_default_value": "", - "exception_goto": [], - "exception_method": null, - "frequencyPenaltyEnabled": false, - "frequency_penalty": 0.7, - "llm_id": "deepseek-chat@DeepSeek", - "maxTokensEnabled": false, - "max_retries": 3, - "max_rounds": 2, - "max_tokens": 256, - "mcp": [], - "message_history_window_size": 12, - "outputs": { - "content": { - "type": "string", - "value": "" - } - }, - "presencePenaltyEnabled": false, - "presence_penalty": 0.4, - "prompts": [ - { - "content": "The user query is {sys.query}", - "role": "user" - } - ], - "sys_prompt": "# Role \n\nYou are **Customer Server Agent**. Classify every user message; handle **contact** yourself. This is a multi-agent system.\n\n## Categories \n\n1. **contact** \u2013 user gives phone, e\u2011mail, WeChat, Line, Discord, etc. \n\n2. **casual** \u2013 small talk, not about the product. \n\n3. **complain** \u2013 complaints or profanity about the product/service. \n\n4. **product** \u2013 questions on product use, appearance, function, or errors.\n\n## If contact \n\nReply with one random item below\u2014do not change wording or call sub\u2011agents: \n\n1. Okay, I've already written this down. What else can I do for you? \n\n2. Got it. What else can I do for you? \n\n3. Thanks for your trust! Our expert will contact you ASAP. Anything else I can help with? \n\n4. Thanks! Anything else I can do for you?\n\n\n---\n\n\n## Otherwise (casual\u202f/\u202fcomplain\u202f/\u202fproduct) \n\nLet Sub\u2011Agent returns its answer\n\n## Sub\u2011Agent \n\n- casual \u2192 **Casual Agent** \nThis is an agent for handles casual conversationk.\n\n- complain \u2192 **Soothe Agent** \nThis is an agent for handles complaints or emotional input.\n\n- product \u2192 **Product Agent** \nThis is an agent for handles product-related queries and can use the `Retrieval` tool.\n\n## Importance\n\n- When the Sub\u2011Agent returns its answer, forward that answer to the user verbatim \u2014 do not add, edit, or reason further.\n ", - "temperature": 0.1, - "temperatureEnabled": true, - "tools": [ - { - "component_name": "Agent", - "id": "Agent:SlowKiwisBehave", - "name": "Casual Agent", - "params": { - "delay_after_error": 1, - "description": "This is an agent for handles casual conversationk.", - "exception_comment": "", - "exception_default_value": "", - "exception_goto": [], - "exception_method": null, - "frequencyPenaltyEnabled": false, - "frequency_penalty": 0.3, - "llm_id": "deepseek-chat@DeepSeek", - "maxTokensEnabled": false, - "max_retries": 1, - "max_rounds": 1, - "max_tokens": 4096, - "mcp": [], - "message_history_window_size": 12, - "outputs": { - "content": { - "type": "string", - "value": "" - } - }, - "parameter": "Balance", - "presencePenaltyEnabled": false, - "presence_penalty": 0.2, - "prompts": [ - { - "content": "{sys.query}", - "role": "user" - } - ], - "sys_prompt": "You are a friendly and casual conversational assistant. \n\nYour primary goal is to engage users in light and enjoyable daily conversation. \n\n- Keep a natural, relaxed, and positive tone. \n\n- Avoid sensitive, controversial, or negative topics. \n\n- You may gently guide the conversation by introducing related casual topics if the user shows interest. \n\n", - "temperature": 0.5, - "temperatureEnabled": true, - "tools": [], - "topPEnabled": false, - "top_p": 0.85, - "user_prompt": "This is the order you need to send to the agent.", - "visual_files_var": "" - } - }, - { - "component_name": "Agent", - "id": "Agent:PoorTaxesRescue", - "name": "Soothe Agent", - "params": { - "delay_after_error": 1, - "description": "This is an agent for handles complaints or emotional input.", - "exception_comment": "", - "exception_default_value": "", - "exception_goto": [], - "exception_method": null, - "frequencyPenaltyEnabled": false, - "frequency_penalty": 0.3, - "llm_id": "deepseek-chat@DeepSeek", - "maxTokensEnabled": false, - "max_retries": 1, - "max_rounds": 1, - "max_tokens": 4096, - "mcp": [], - "message_history_window_size": 12, - "outputs": { - "content": { - "type": "string", - "value": "" - } - }, - "parameter": "Balance", - "presencePenaltyEnabled": false, - "presence_penalty": 0.2, - "prompts": [ - { - "content": "{sys.query}", - "role": "user" - } - ], - "sys_prompt": "You are an empathetic mood-soothing assistant. \n\nYour role is to comfort and encourage users when they feel upset or frustrated. \n\n- Use a warm, kind, and understanding tone. \n\n- Focus on showing empathy and emotional support rather than solving the problem directly. \n\n- Always encourage users with positive and reassuring statements. ", - "temperature": 0.5, - "temperatureEnabled": true, - "tools": [], - "topPEnabled": false, - "top_p": 0.85, - "user_prompt": "This is the order you need to send to the agent.", - "visual_files_var": "" - } - }, - { - "component_name": "Agent", - "id": "Agent:SillyTurkeysRest", - "name": "Product Agent", - "params": { - "delay_after_error": 1, - "description": "This is an agent for handles product-related queries and can use the `Retrieval` tool.", - "exception_comment": "", - "exception_default_value": "", - "exception_goto": [], - "exception_method": null, - "frequencyPenaltyEnabled": false, - "frequency_penalty": 0.7, - "llm_id": "deepseek-chat@DeepSeek", - "maxTokensEnabled": false, - "max_retries": 3, - "max_rounds": 2, - "max_tokens": 256, - "mcp": [], - "message_history_window_size": 12, - "outputs": { - "content": { - "type": "string", - "value": "" - } - }, - "presencePenaltyEnabled": false, - "presence_penalty": 0.4, - "prompts": [ - { - "content": "{sys.query}", - "role": "user" - } - ], - "sys_prompt": "# Role \n\nYou are a Product Information Advisor with access to the **Retrieval** tool.\n\n# Workflow \n\n1. Run **Retrieval** with a focused query from the user\u2019s question. \n\n2. Draft the reply **strictly** from the returned passages. \n\n3. If nothing relevant is retrieved, reply: \n\n \u201cI cannot find relevant documents in the knowledge base.\u201d\n\n# Rules \n\n- No assumptions, guesses, or extra\u2011KB knowledge. \n\n- Factual, concise. Use bullets / numbers when helpful. \n\n", - "temperature": 0.1, - "temperatureEnabled": true, - "tools": [ - { - "component_name": "Retrieval", - "name": "Retrieval", - "params": { - "cross_languages": [], - "description": "This is a product knowledge base", - "empty_response": "", - "kb_ids": [], - "keywords_similarity_weight": 0.7, - "outputs": { - "formalized_content": { - "type": "string", - "value": "" - } - }, - "rerank_id": "", - "similarity_threshold": 0.2, - "top_k": 1024, - "top_n": 8, - "use_kg": false - } - } - ], - "topPEnabled": false, - "top_p": 0.3, - "user_prompt": "This is the order you need to send to the agent.", - "visual_files_var": "" - } - } - ], - "topPEnabled": false, - "top_p": 0.3, - "user_prompt": "", - "visual_files_var": "" - } - }, - "upstream": [ - "begin" - ] + "components": { + "Agent:DullTownsHope": { + "downstream": [ + "VariableAggregator:FuzzyBerriesFlow" + ], + "obj": { + "component_name": "Agent", + "params": { + "delay_after_error": 1, + "description": "", + "exception_comment": "", + "exception_default_value": "", + "exception_goto": [], + "exception_method": null, + "frequencyPenaltyEnabled": false, + "frequency_penalty": 0.3, + "llm_id": "deepseek-chat@DeepSeek", + "maxTokensEnabled": false, + "max_retries": 3, + "max_rounds": 5, + "max_tokens": 4096, + "mcp": [], + "message_history_window_size": 12, + "outputs": { + "content": { + "type": "string", + "value": "" + } }, - "Message:PurpleCitiesSee": { - "downstream": [], - "obj": { - "component_name": "Message", - "params": { - "content": [ - "{Agent:RottenRiversDo@content}" - ] - } - }, - "upstream": [ - "Agent:RottenRiversDo" - ] - }, - "begin": { - "downstream": [ - "Agent:RottenRiversDo" - ], - "obj": { - "component_name": "Begin", - "params": { - "enablePrologue": true, - "inputs": {}, - "mode": "conversational", - "prologue": "Hi! I'm an official AI customer service representative. How can I help you?" - } - }, - "upstream": [] - } - }, - "globals": { - "sys.conversation_turns": 0, - "sys.files": [], - "sys.query": "", - "sys.user_id": "" - }, - "graph": { - "edges": [ + "parameter": "Balance", + "presencePenaltyEnabled": false, + "presence_penalty": 0.2, + "prompts": [ { - "data": { - "isHovered": false - }, - "id": "xy-edge__beginstart-Agent:RottenRiversDoend", - "source": "begin", - "sourceHandle": "start", - "target": "Agent:RottenRiversDo", - "targetHandle": "end" - }, - { - "data": { - "isHovered": false - }, - "id": "xy-edge__Agent:RottenRiversDoagentBottom-Agent:SlowKiwisBehaveagentTop", - "source": "Agent:RottenRiversDo", - "sourceHandle": "agentBottom", - "target": "Agent:SlowKiwisBehave", - "targetHandle": "agentTop" - }, - { - "data": { - "isHovered": false - }, - "id": "xy-edge__Agent:RottenRiversDoagentBottom-Agent:PoorTaxesRescueagentTop", - "source": "Agent:RottenRiversDo", - "sourceHandle": "agentBottom", - "target": "Agent:PoorTaxesRescue", - "targetHandle": "agentTop" - }, - { - "data": { - "isHovered": false - }, - "id": "xy-edge__Agent:RottenRiversDoagentBottom-Agent:SillyTurkeysRestagentTop", - "source": "Agent:RottenRiversDo", - "sourceHandle": "agentBottom", - "target": "Agent:SillyTurkeysRest", - "targetHandle": "agentTop" - }, - { - "data": { - "isHovered": false - }, - "id": "xy-edge__Agent:SillyTurkeysResttool-Tool:CrazyShirtsKissend", - "source": "Agent:SillyTurkeysRest", - "sourceHandle": "tool", - "target": "Tool:CrazyShirtsKiss", - "targetHandle": "end" - }, - { - "data": { - "isHovered": false - }, - "id": "xy-edge__Agent:RottenRiversDostart-Message:PurpleCitiesSeeend", - "source": "Agent:RottenRiversDo", - "sourceHandle": "start", - "target": "Message:PurpleCitiesSee", - "targetHandle": "end" + "content": "The user query is {sys.query}", + "role": "user" } ], - "nodes": [ - { - "data": { - "form": { - "enablePrologue": true, - "inputs": {}, - "mode": "conversational", - "prologue": "Hi! I'm an official AI customer service representative. How can I help you?" - }, - "label": "Begin", - "name": "begin" - }, - "id": "begin", - "measured": { - "height": 48, - "width": 200 - }, - "position": { - "x": 50, - "y": 200 - }, - "selected": false, - "sourcePosition": "left", - "targetPosition": "right", - "type": "beginNode" - }, - { - "data": { - "form": { - "delay_after_error": 1, - "description": "", - "exception_comment": "", - "exception_default_value": "", - "exception_goto": [], - "exception_method": null, - "frequencyPenaltyEnabled": false, - "frequency_penalty": 0.7, - "llm_id": "deepseek-chat@DeepSeek", - "maxTokensEnabled": false, - "max_retries": 3, - "max_rounds": 2, - "max_tokens": 256, - "mcp": [], - "message_history_window_size": 12, - "outputs": { - "content": { - "type": "string", - "value": "" - } - }, - "presencePenaltyEnabled": false, - "presence_penalty": 0.4, - "prompts": [ - { - "content": "The user query is {sys.query}", - "role": "user" - } - ], - "sys_prompt": "# Role \n\nYou are **Customer Server Agent**. Classify every user message; handle **contact** yourself. This is a multi-agent system.\n\n## Categories \n\n1. **contact** \u2013 user gives phone, e\u2011mail, WeChat, Line, Discord, etc. \n\n2. **casual** \u2013 small talk, not about the product. \n\n3. **complain** \u2013 complaints or profanity about the product/service. \n\n4. **product** \u2013 questions on product use, appearance, function, or errors.\n\n## If contact \n\nReply with one random item below\u2014do not change wording or call sub\u2011agents: \n\n1. Okay, I've already written this down. What else can I do for you? \n\n2. Got it. What else can I do for you? \n\n3. Thanks for your trust! Our expert will contact you ASAP. Anything else I can help with? \n\n4. Thanks! Anything else I can do for you?\n\n\n---\n\n\n## Otherwise (casual\u202f/\u202fcomplain\u202f/\u202fproduct) \n\nLet Sub\u2011Agent returns its answer\n\n## Sub\u2011Agent \n\n- casual \u2192 **Casual Agent** \nThis is an agent for handles casual conversationk.\n\n- complain \u2192 **Soothe Agent** \nThis is an agent for handles complaints or emotional input.\n\n- product \u2192 **Product Agent** \nThis is an agent for handles product-related queries and can use the `Retrieval` tool.\n\n## Importance\n\n- When the Sub\u2011Agent returns its answer, forward that answer to the user verbatim \u2014 do not add, edit, or reason further.\n ", - "temperature": 0.1, - "temperatureEnabled": true, - "tools": [], - "topPEnabled": false, - "top_p": 0.3, - "user_prompt": "", - "visual_files_var": "" - }, - "label": "Agent", - "name": "Customer Server Agent" - }, - "dragging": false, - "id": "Agent:RottenRiversDo", - "measured": { - "height": 84, - "width": 200 - }, - "position": { - "x": 350, - "y": 198.88981333505626 - }, - "selected": false, - "sourcePosition": "right", - "targetPosition": "left", - "type": "agentNode" - }, - { - "data": { - "form": { - "delay_after_error": 1, - "description": "This is an agent for handles casual conversationk.", - "exception_comment": "", - "exception_default_value": "", - "exception_goto": [], - "exception_method": null, - "frequencyPenaltyEnabled": false, - "frequency_penalty": 0.3, - "llm_id": "deepseek-chat@DeepSeek", - "maxTokensEnabled": false, - "max_retries": 1, - "max_rounds": 1, - "max_tokens": 4096, - "mcp": [], - "message_history_window_size": 12, - "outputs": { - "content": { - "type": "string", - "value": "" - } - }, - "parameter": "Balance", - "presencePenaltyEnabled": false, - "presence_penalty": 0.2, - "prompts": [ - { - "content": "{sys.query}", - "role": "user" - } - ], - "sys_prompt": "You are a friendly and casual conversational assistant. \n\nYour primary goal is to engage users in light and enjoyable daily conversation. \n\n- Keep a natural, relaxed, and positive tone. \n\n- Avoid sensitive, controversial, or negative topics. \n\n- You may gently guide the conversation by introducing related casual topics if the user shows interest. \n\n", - "temperature": 0.5, - "temperatureEnabled": true, - "tools": [], - "topPEnabled": false, - "top_p": 0.85, - "user_prompt": "This is the order you need to send to the agent.", - "visual_files_var": "" - }, - "label": "Agent", - "name": "Casual Agent" - }, - "dragging": false, - "id": "Agent:SlowKiwisBehave", - "measured": { - "height": 84, - "width": 200 - }, - "position": { - "x": 124.4782938105834, - "y": 402.1704532368496 - }, - "selected": false, - "sourcePosition": "right", - "targetPosition": "left", - "type": "agentNode" - }, - { - "data": { - "form": { - "delay_after_error": 1, - "description": "This is an agent for handles complaints or emotional input.", - "exception_comment": "", - "exception_default_value": "", - "exception_goto": [], - "exception_method": null, - "frequencyPenaltyEnabled": false, - "frequency_penalty": 0.3, - "llm_id": "deepseek-chat@DeepSeek", - "maxTokensEnabled": false, - "max_retries": 1, - "max_rounds": 1, - "max_tokens": 4096, - "mcp": [], - "message_history_window_size": 12, - "outputs": { - "content": { - "type": "string", - "value": "" - } - }, - "parameter": "Balance", - "presencePenaltyEnabled": false, - "presence_penalty": 0.2, - "prompts": [ - { - "content": "{sys.query}", - "role": "user" - } - ], - "sys_prompt": "You are an empathetic mood-soothing assistant. \n\nYour role is to comfort and encourage users when they feel upset or frustrated. \n\n- Use a warm, kind, and understanding tone. \n\n- Focus on showing empathy and emotional support rather than solving the problem directly. \n\n- Always encourage users with positive and reassuring statements. ", - "temperature": 0.5, - "temperatureEnabled": true, - "tools": [], - "topPEnabled": false, - "top_p": 0.85, - "user_prompt": "This is the order you need to send to the agent.", - "visual_files_var": "" - }, - "label": "Agent", - "name": "Soothe Agent" - }, - "dragging": false, - "id": "Agent:PoorTaxesRescue", - "measured": { - "height": 84, - "width": 200 - }, - "position": { - "x": 402.02090711979577, - "y": 363.3139199638186 - }, - "selected": false, - "sourcePosition": "right", - "targetPosition": "left", - "type": "agentNode" - }, - { - "data": { - "form": { - "delay_after_error": 1, - "description": "This is an agent for handles product-related queries and can use the `Retrieval` tool.", - "exception_comment": "", - "exception_default_value": "", - "exception_goto": [], - "exception_method": null, - "frequencyPenaltyEnabled": false, - "frequency_penalty": 0.7, - "llm_id": "deepseek-chat@DeepSeek", - "maxTokensEnabled": false, - "max_retries": 3, - "max_rounds": 2, - "max_tokens": 256, - "mcp": [], - "message_history_window_size": 12, - "outputs": { - "content": { - "type": "string", - "value": "" - } - }, - "presencePenaltyEnabled": false, - "presence_penalty": 0.4, - "prompts": [ - { - "content": "{sys.query}", - "role": "user" - } - ], - "sys_prompt": "# Role \n\nYou are a Product Information Advisor with access to the **Retrieval** tool.\n\n# Workflow \n\n1. Run **Retrieval** with a focused query from the user\u2019s question. \n\n2. Draft the reply **strictly** from the returned passages. \n\n3. If nothing relevant is retrieved, reply: \n\n \u201cI cannot find relevant documents in the knowledge base.\u201d\n\n# Rules \n\n- No assumptions, guesses, or extra\u2011KB knowledge. \n\n- Factual, concise. Use bullets / numbers when helpful. \n\n", - "temperature": 0.1, - "temperatureEnabled": true, - "tools": [ - { - "component_name": "Retrieval", - "name": "Retrieval", - "params": { - "cross_languages": [], - "description": "This is a product knowledge base", - "empty_response": "", - "kb_ids": [], - "keywords_similarity_weight": 0.7, - "outputs": { - "formalized_content": { - "type": "string", - "value": "" - } - }, - "rerank_id": "", - "similarity_threshold": 0.2, - "top_k": 1024, - "top_n": 8, - "use_kg": false - } - } - ], - "topPEnabled": false, - "top_p": 0.3, - "user_prompt": "This is the order you need to send to the agent.", - "visual_files_var": "" - }, - "label": "Agent", - "name": "Product Agent" - }, - "dragging": false, - "id": "Agent:SillyTurkeysRest", - "measured": { - "height": 84, - "width": 200 - }, - "position": { - "x": 684.0042670887832, - "y": 317.79626670112515 - }, - "selected": false, - "sourcePosition": "right", - "targetPosition": "left", - "type": "agentNode" - }, - { - "data": { - "form": { - "description": "This is an agent for a specific task.", - "user_prompt": "This is the order you need to send to the agent." - }, - "label": "Tool", - "name": "flow.tool_0" - }, - "dragging": false, - "id": "Tool:CrazyShirtsKiss", - "measured": { - "height": 48, - "width": 200 - }, - "position": { - "x": 659.7339736658578, - "y": 443.3638400568565 - }, - "selected": false, - "sourcePosition": "right", - "targetPosition": "left", - "type": "toolNode" - }, - { - "data": { - "form": { - "content": [ - "{Agent:RottenRiversDo@content}" - ] - }, - "label": "Message", - "name": "Response" - }, - "dragging": false, - "id": "Message:PurpleCitiesSee", - "measured": { - "height": 56, - "width": 200 - }, - "position": { - "x": 675.534293293706, - "y": 158.92309339708154 - }, - "selected": false, - "sourcePosition": "right", - "targetPosition": "left", - "type": "messageNode" - }, - { - "data": { - "form": { - "text": "This is a multi-agent system for intelligent customer service processing based on user intent classification. It uses the lead-agent to identify the type of user needs, assign tasks to sub-agents for processing, and finally the lead agent outputs the results." - }, - "label": "Note", - "name": "Workflow Overall Description" - }, - "dragHandle": ".note-drag-handle", - "dragging": false, - "height": 140, - "id": "Note:MoodyTurtlesCount", - "measured": { - "height": 140, - "width": 385 - }, - "position": { - "x": -59.311679338397, - "y": -2.2203733298874866 - }, - "resizing": false, - "selected": false, - "sourcePosition": "right", - "targetPosition": "left", - "type": "noteNode", - "width": 385 - }, - { - "data": { - "form": { - "text": "Answers will be given strictly according to the content retrieved from the knowledge base." - }, - "label": "Note", - "name": "Product Agent " - }, - "dragHandle": ".note-drag-handle", - "dragging": false, - "id": "Note:ColdCoinsBathe", - "measured": { - "height": 136, - "width": 249 - }, - "position": { - "x": 994.4238924667025, - "y": 329.08949370720796 - }, - "selected": false, - "sourcePosition": "right", - "targetPosition": "left", - "type": "noteNode" - } - ] + "sys_prompt": "You are an empathetic mood-soothing assistant. \n\nYour role is to comfort and encourage users when they feel upset or frustrated. \n\n- Use a warm, kind, and understanding tone. \n\n- Focus on showing empathy and emotional support rather than solving the problem directly. \n\n- Always encourage users with positive and reassuring statements. ", + "temperature": 0.5, + "temperatureEnabled": true, + "tools": [], + "topPEnabled": false, + "top_p": 0.85, + "user_prompt": "", + "visual_files_var": "" + } }, - "history": [], - "messages": [], - "path": [], - "retrieval": [] + "upstream": [ + "Categorize:DullFriendsThank" + ] + }, + "Agent:KhakiSunsJudge": { + "downstream": [ + "VariableAggregator:FuzzyBerriesFlow" + ], + "obj": { + "component_name": "Agent", + "params": { + "delay_after_error": 1, + "description": "", + "exception_comment": "", + "exception_default_value": "", + "exception_goto": [], + "exception_method": null, + "frequencyPenaltyEnabled": false, + "frequency_penalty": 0.7, + "llm_id": "deepseek-chat@DeepSeek", + "maxTokensEnabled": false, + "max_retries": 3, + "max_rounds": 5, + "max_tokens": 256, + "mcp": [], + "message_history_window_size": 12, + "outputs": { + "content": { + "type": "string", + "value": "" + } + }, + "presencePenaltyEnabled": false, + "presence_penalty": 0.4, + "prompts": [ + { + "content": "The user query is {sys.query}\n\nThe relevant document are {Retrieval:ShyPumasJoke@formalized_content}", + "role": "user" + } + ], + "sys_prompt": "You are a highly professional product information advisor. \n\nYour only mission is to provide accurate, factual, and structured answers to all product-related queries.\n\nAbsolutely no assumptions, guesses, or fabricated content are allowed. \n\n**Key Principles:**\n\n1. **Strict Database Reliance:** \n\n - Every answer must be based solely on the verified product information stored in the relevant documen.\n\n - You are NOT allowed to invent, speculate, or infer details beyond what is retrieved. \n\n - If you cannot find relevant data, respond with: *\"I cannot find this information in our official product database. Please check back later or provide more details for further search.\"*\n\n2. **Information Accuracy and Structure:** \n\n - Provide information in a clear, concise, and professional way. \n\n - Use bullet points or numbered lists if there are multiple key points (e.g., features, price, warranty, technical specifications). \n\n - Always specify the version or model number when applicable to avoid confusion.\n\n3. **Tone and Style:** \n\n - Maintain a polite, professional, and helpful tone at all times. \n\n - Avoid marketing exaggeration or promotional language; stay strictly factual. \n\n - Do not express personal opinions; only cite official product data.\n\n4. **User Guidance:** \n\n - If the user’s query is unclear or too broad, politely request clarification or guide them to provide more specific product details (e.g., product name, model, version). \n\n - Example: *\"Could you please specify the product model or category so I can retrieve the most relevant information for you?\"*\n\n5. **Response Length and Formatting:** \n\n - Keep each answer within 100–150 words for general queries. \n\n - For complex or multi-step explanations, you may extend to 200–250 words, but always remain clear and well-structured.\n\n6. **Critical Reminder:** \n\nYour authority and reliability depend entirely on the relevant document responses. Any fabricated, speculative, or unverified content will be considered a critical failure of your role.\n\n\n", + "temperature": 0.1, + "temperatureEnabled": true, + "tools": [], + "topPEnabled": false, + "top_p": 0.3, + "user_prompt": "", + "visual_files_var": "" + } + }, + "upstream": [ + "Retrieval:ShyPumasJoke" + ] + }, + "Agent:TwelveOwlsWatch": { + "downstream": [ + "VariableAggregator:FuzzyBerriesFlow" + ], + "obj": { + "component_name": "Agent", + "params": { + "delay_after_error": 1, + "description": "", + "exception_comment": "", + "exception_default_value": "", + "exception_goto": [], + "exception_method": null, + "frequencyPenaltyEnabled": false, + "frequency_penalty": 0.3, + "llm_id": "deepseek-chat@DeepSeek", + "maxTokensEnabled": false, + "max_retries": 3, + "max_rounds": 5, + "max_tokens": 4096, + "mcp": [], + "message_history_window_size": 12, + "outputs": { + "content": { + "type": "string", + "value": "" + } + }, + "parameter": "Balance", + "presencePenaltyEnabled": false, + "presence_penalty": 0.2, + "prompts": [ + { + "content": "The user query is {sys.query}", + "role": "user" + } + ], + "sys_prompt": "You are a friendly and casual conversational assistant. \n\nYour primary goal is to engage users in light and enjoyable daily conversation. \n\n- Keep a natural, relaxed, and positive tone. \n\n- Avoid sensitive, controversial, or negative topics. \n\n- You may gently guide the conversation by introducing related casual topics if the user shows interest. \n\n", + "temperature": 0.5, + "temperatureEnabled": true, + "tools": [], + "topPEnabled": false, + "top_p": 0.85, + "user_prompt": "", + "visual_files_var": "" + } + }, + "upstream": [ + "Categorize:DullFriendsThank" + ] + }, + "Categorize:DullFriendsThank": { + "downstream": [ + "Message:BreezyDonutsHeal", + "Agent:TwelveOwlsWatch", + "Agent:DullTownsHope", + "Retrieval:ShyPumasJoke" + ], + "obj": { + "component_name": "Categorize", + "params": { + "category_description": { + "1. contact": { + "description": "This answer provide a specific contact information, like e-mail, phone number, wechat number, line number, twitter, discord, etc,.", + "examples": [ + "My phone number is 203921\nkevinhu.hk@gmail.com\nThis is my discord number: johndowson_29384\n13212123432\n8379829" + ], + "to": [ + "Message:BreezyDonutsHeal" + ] + }, + "2. casual": { + "description": "The question is not about the product usage, appearance and how it works. Just casual chat.", + "examples": [ + "How are you doing?\nWhat is your name?\nAre you a robot?\nWhat's the weather?\nWill it rain?" + ], + "to": [ + "Agent:TwelveOwlsWatch" + ] + }, + "3. complain": { + "description": "Complain even curse about the product or service you provide. But the comment is not specific enough.", + "examples": [ + "How bad is it.\nIt's really sucks.\nDamn, for God's sake, can it be more steady?\nShit, I just can't use this shit.\nI can't stand it anymore." + ], + "to": [ + "Agent:DullTownsHope" + ] + }, + "4. product related": { + "description": "The question is about the product usage, appearance and how it works.", + "examples": [ + "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?\nException: Can't connect to ES cluster\nHow to build the RAGFlow image from scratch" + ], + "to": [ + "Retrieval:ShyPumasJoke" + ] + } + }, + "llm_id": "deepseek-chat@DeepSeek", + "message_history_window_size": 1, + "outputs": { + "category_name": { + "type": "string" + } + }, + "query": "sys.query", + "temperature": "0.1" + } + }, + "upstream": [ + "begin" + ] + }, + "Message:BreezyDonutsHeal": { + "downstream": [], + "obj": { + "component_name": "Message", + "params": { + "content": [ + "Okay, I've already write this down. What else I can do for you?", + "Get it. What else I can do for you?", + "Thanks for your trust! Our expert will contact ASAP. So, anything else I can do for you?", + "Thanks! So, anything else I can do for you?" + ] + } + }, + "upstream": [ + "Categorize:DullFriendsThank" + ] + }, + "Message:DryBusesCarry": { + "downstream": [], + "obj": { + "component_name": "Message", + "params": { + "content": [ + "{VariableAggregator:FuzzyBerriesFlow@LLM_Response}" + ] + } + }, + "upstream": [ + "VariableAggregator:FuzzyBerriesFlow" + ] + }, + "Retrieval:ShyPumasJoke": { + "downstream": [ + "Agent:KhakiSunsJudge" + ], + "obj": { + "component_name": "Retrieval", + "params": { + "cross_languages": [], + "empty_response": "", + "kb_ids": [], + "keywords_similarity_weight": 0.7, + "outputs": { + "formalized_content": { + "type": "string", + "value": "" + } + }, + "query": "sys.query", + "rerank_id": "", + "similarity_threshold": 0.2, + "top_k": 1024, + "top_n": 8, + "use_kg": false + } + }, + "upstream": [ + "Categorize:DullFriendsThank" + ] + }, + "VariableAggregator:FuzzyBerriesFlow": { + "downstream": [ + "Message:DryBusesCarry" + ], + "obj": { + "component_name": "VariableAggregator", + "params": { + "groups": [ + { + "group_name": "LLM_Response", + "type": "string", + "variables": [ + { + "value": "Agent:TwelveOwlsWatch@content" + }, + { + "value": "Agent:DullTownsHope@content" + }, + { + "value": "Agent:KhakiSunsJudge@content" + } + ] + } + ], + "outputs": { + "LLM_Response": { + "type": "string" + } + } + } + }, + "upstream": [ + "Agent:DullTownsHope", + "Agent:TwelveOwlsWatch", + "Agent:KhakiSunsJudge" + ] + }, + "begin": { + "downstream": [ + "Categorize:DullFriendsThank" + ], + "obj": { + "component_name": "Begin", + "params": { + "enablePrologue": true, + "inputs": {}, + "mode": "conversational", + "prologue": "Hi! I'm an official AI customer service representative. How can I help you?" + } + }, + "upstream": [] + } }, + "globals": { + "sys.conversation_turns": 0, + "sys.files": [], + "sys.query": "", + "sys.user_id": "" + }, + "graph": { + "edges": [ + { + "data": { + "isHovered": false + }, + "id": "xy-edge__beginstart-Categorize:DullFriendsThankend", + "source": "begin", + "sourceHandle": "start", + "target": "Categorize:DullFriendsThank", + "targetHandle": "end" + }, + { + "data": { + "isHovered": false + }, + "id": "xy-edge__Categorize:DullFriendsThanke4d754a5-a33e-4096-8648-8688e5474a15-Message:BreezyDonutsHealend", + "source": "Categorize:DullFriendsThank", + "sourceHandle": "e4d754a5-a33e-4096-8648-8688e5474a15", + "target": "Message:BreezyDonutsHeal", + "targetHandle": "end" + }, + { + "data": { + "isHovered": false + }, + "id": "xy-edge__Categorize:DullFriendsThank8cbf6ea3-a176-490d-9f8c-86373c932583-Agent:TwelveOwlsWatchend", + "source": "Categorize:DullFriendsThank", + "sourceHandle": "8cbf6ea3-a176-490d-9f8c-86373c932583", + "target": "Agent:TwelveOwlsWatch", + "targetHandle": "end" + }, + { + "data": { + "isHovered": false + }, + "id": "xy-edge__Categorize:DullFriendsThankacc40a78-1b9e-4d2f-b5d6-64e01ab69269-Agent:DullTownsHopeend", + "source": "Categorize:DullFriendsThank", + "sourceHandle": "acc40a78-1b9e-4d2f-b5d6-64e01ab69269", + "target": "Agent:DullTownsHope", + "targetHandle": "end" + }, + { + "data": { + "isHovered": false + }, + "id": "xy-edge__Categorize:DullFriendsThankdfa5eead-9341-4f22-9236-068dbfb745e8-Retrieval:ShyPumasJokeend", + "source": "Categorize:DullFriendsThank", + "sourceHandle": "dfa5eead-9341-4f22-9236-068dbfb745e8", + "target": "Retrieval:ShyPumasJoke", + "targetHandle": "end" + }, + { + "data": { + "isHovered": false + }, + "id": "xy-edge__Retrieval:ShyPumasJokestart-Agent:KhakiSunsJudgeend", + "source": "Retrieval:ShyPumasJoke", + "sourceHandle": "start", + "target": "Agent:KhakiSunsJudge", + "targetHandle": "end" + }, + { + "data": { + "isHovered": false + }, + "id": "xy-edge__Agent:DullTownsHopestart-VariableAggregator:FuzzyBerriesFlowend", + "source": "Agent:DullTownsHope", + "sourceHandle": "start", + "target": "VariableAggregator:FuzzyBerriesFlow", + "targetHandle": "end" + }, + { + "id": "xy-edge__Agent:TwelveOwlsWatchstart-VariableAggregator:FuzzyBerriesFlowend", + "markerEnd": "logo", + "source": "Agent:TwelveOwlsWatch", + "sourceHandle": "start", + "target": "VariableAggregator:FuzzyBerriesFlow", + "targetHandle": "end", + "type": "buttonEdge", + "zIndex": 1001 + }, + { + "data": { + "isHovered": false + }, + "id": "xy-edge__Agent:KhakiSunsJudgestart-VariableAggregator:FuzzyBerriesFlowend", + "markerEnd": "logo", + "source": "Agent:KhakiSunsJudge", + "sourceHandle": "start", + "target": "VariableAggregator:FuzzyBerriesFlow", + "targetHandle": "end", + "type": "buttonEdge", + "zIndex": 1001 + }, + { + "id": "xy-edge__VariableAggregator:FuzzyBerriesFlowstart-Message:DryBusesCarryend", + "source": "VariableAggregator:FuzzyBerriesFlow", + "sourceHandle": "start", + "target": "Message:DryBusesCarry", + "targetHandle": "end" + } + ], + "nodes": [ + { + "data": { + "form": { + "enablePrologue": true, + "inputs": {}, + "mode": "conversational", + "prologue": "Hi! I'm an official AI customer service representative. How can I help you?" + }, + "label": "Begin", + "name": "begin" + }, + "id": "begin", + "measured": { + "height": 48, + "width": 200 + }, + "position": { + "x": 50, + "y": 200 + }, + "selected": false, + "sourcePosition": "left", + "targetPosition": "right", + "type": "beginNode" + }, + { + "data": { + "form": { + "frequencyPenaltyEnabled": false, + "frequency_penalty": 0.5, + "items": [ + { + "description": "This answer provide a specific contact information, like e-mail, phone number, wechat number, line number, twitter, discord, etc,.", + "examples": [ + { + "value": "My phone number is 203921\nkevinhu.hk@gmail.com\nThis is my discord number: johndowson_29384\n13212123432\n8379829" + } + ], + "name": "1. contact", + "uuid": "e4d754a5-a33e-4096-8648-8688e5474a15" + }, + { + "description": "The question is not about the product usage, appearance and how it works. Just casual chat.", + "examples": [ + { + "value": "How are you doing?\nWhat is your name?\nAre you a robot?\nWhat's the weather?\nWill it rain?" + } + ], + "name": "2. casual", + "uuid": "8cbf6ea3-a176-490d-9f8c-86373c932583" + }, + { + "description": "Complain even curse about the product or service you provide. But the comment is not specific enough.", + "examples": [ + { + "value": "How bad is it.\nIt's really sucks.\nDamn, for God's sake, can it be more steady?\nShit, I just can't use this shit.\nI can't stand it anymore." + } + ], + "name": "3. complain", + "uuid": "acc40a78-1b9e-4d2f-b5d6-64e01ab69269" + }, + { + "description": "The question is about the product usage, appearance and how it works.", + "examples": [ + { + "value": "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?\nException: Can't connect to ES cluster\nHow to build the RAGFlow image from scratch" + } + ], + "name": "4. product related", + "uuid": "dfa5eead-9341-4f22-9236-068dbfb745e8" + } + ], + "llm_id": "deepseek-chat@DeepSeek", + "maxTokensEnabled": false, + "max_tokens": 4096, + "message_history_window_size": 1, + "outputs": { + "category_name": { + "type": "string" + } + }, + "parameter": "Precise", + "presencePenaltyEnabled": false, + "presence_penalty": 0.5, + "query": "sys.query", + "temperature": "0.1", + "temperatureEnabled": true, + "topPEnabled": false, + "top_p": 0.75 + }, + "label": "Categorize", + "name": "Categorize" + }, + "dragging": false, + "id": "Categorize:DullFriendsThank", + "measured": { + "height": 218, + "width": 200 + }, + "position": { + "x": 377.1140727959881, + "y": 138.1799140251472 + }, + "selected": false, + "sourcePosition": "right", + "targetPosition": "left", + "type": "categorizeNode" + }, + { + "data": { + "form": { + "content": [ + "Okay, I've already write this down. What else I can do for you?", + "Get it. What else I can do for you?", + "Thanks for your trust! Our expert will contact ASAP. So, anything else I can do for you?", + "Thanks! So, anything else I can do for you?" + ] + }, + "label": "Message", + "name": "What else?" + }, + "dragging": false, + "id": "Message:BreezyDonutsHeal", + "measured": { + "height": 56, + "width": 200 + }, + "position": { + "x": 724.8348409169271, + "y": 60.09138437270154 + }, + "selected": false, + "sourcePosition": "right", + "targetPosition": "left", + "type": "messageNode" + }, + { + "data": { + "form": { + "delay_after_error": 1, + "description": "", + "exception_comment": "", + "exception_default_value": "", + "exception_goto": [], + "exception_method": null, + "frequencyPenaltyEnabled": false, + "frequency_penalty": 0.3, + "llm_id": "deepseek-chat@DeepSeek", + "maxTokensEnabled": false, + "max_retries": 3, + "max_rounds": 5, + "max_tokens": 4096, + "mcp": [], + "message_history_window_size": 12, + "outputs": { + "content": { + "type": "string", + "value": "" + } + }, + "parameter": "Balance", + "presencePenaltyEnabled": false, + "presence_penalty": 0.2, + "prompts": [ + { + "content": "The user query is {sys.query}", + "role": "user" + } + ], + "sys_prompt": "You are a friendly and casual conversational assistant. \n\nYour primary goal is to engage users in light and enjoyable daily conversation. \n\n- Keep a natural, relaxed, and positive tone. \n\n- Avoid sensitive, controversial, or negative topics. \n\n- You may gently guide the conversation by introducing related casual topics if the user shows interest. \n\n", + "temperature": 0.5, + "temperatureEnabled": true, + "tools": [], + "topPEnabled": false, + "top_p": 0.85, + "user_prompt": "", + "visual_files_var": "" + }, + "label": "Agent", + "name": "Causal chat" + }, + "dragging": false, + "id": "Agent:TwelveOwlsWatch", + "measured": { + "height": 84, + "width": 200 + }, + "position": { + "x": 720.4965892695689, + "y": 167.46311264481432 + }, + "selected": false, + "sourcePosition": "right", + "targetPosition": "left", + "type": "agentNode" + }, + { + "data": { + "form": { + "delay_after_error": 1, + "description": "", + "exception_comment": "", + "exception_default_value": "", + "exception_goto": [], + "exception_method": null, + "frequencyPenaltyEnabled": false, + "frequency_penalty": 0.3, + "llm_id": "deepseek-chat@DeepSeek", + "maxTokensEnabled": false, + "max_retries": 3, + "max_rounds": 5, + "max_tokens": 4096, + "mcp": [], + "message_history_window_size": 12, + "outputs": { + "content": { + "type": "string", + "value": "" + } + }, + "parameter": "Balance", + "presencePenaltyEnabled": false, + "presence_penalty": 0.2, + "prompts": [ + { + "content": "The user query is {sys.query}", + "role": "user" + } + ], + "sys_prompt": "You are an empathetic mood-soothing assistant. \n\nYour role is to comfort and encourage users when they feel upset or frustrated. \n\n- Use a warm, kind, and understanding tone. \n\n- Focus on showing empathy and emotional support rather than solving the problem directly. \n\n- Always encourage users with positive and reassuring statements. ", + "temperature": 0.5, + "temperatureEnabled": true, + "tools": [], + "topPEnabled": false, + "top_p": 0.85, + "user_prompt": "", + "visual_files_var": "" + }, + "label": "Agent", + "name": "Soothe mood" + }, + "dragging": false, + "id": "Agent:DullTownsHope", + "measured": { + "height": 84, + "width": 200 + }, + "position": { + "x": 722.665715093248, + "y": 281.3422183879642 + }, + "selected": false, + "sourcePosition": "right", + "targetPosition": "left", + "type": "agentNode" + }, + { + "data": { + "form": { + "cross_languages": [], + "empty_response": "", + "kb_ids": [], + "keywords_similarity_weight": 0.7, + "outputs": { + "formalized_content": { + "type": "string", + "value": "" + } + }, + "query": "sys.query", + "rerank_id": "", + "similarity_threshold": 0.2, + "top_k": 1024, + "top_n": 8, + "use_kg": false + }, + "label": "Retrieval", + "name": "Search product info" + }, + "dragging": false, + "id": "Retrieval:ShyPumasJoke", + "measured": { + "height": 50, + "width": 200 + }, + "position": { + "x": 645.6873721057459, + "y": 516.6923702571407 + }, + "selected": false, + "sourcePosition": "right", + "targetPosition": "left", + "type": "retrievalNode" + }, + { + "data": { + "form": { + "delay_after_error": 1, + "description": "", + "exception_comment": "", + "exception_default_value": "", + "exception_goto": [], + "exception_method": null, + "frequencyPenaltyEnabled": false, + "frequency_penalty": 0.7, + "llm_id": "deepseek-chat@DeepSeek", + "maxTokensEnabled": false, + "max_retries": 3, + "max_rounds": 5, + "max_tokens": 256, + "mcp": [], + "message_history_window_size": 12, + "outputs": { + "content": { + "type": "string", + "value": "" + } + }, + "presencePenaltyEnabled": false, + "presence_penalty": 0.4, + "prompts": [ + { + "content": "The user query is {sys.query}\n\nThe relevant document are {Retrieval:ShyPumasJoke@formalized_content}", + "role": "user" + } + ], + "sys_prompt": "You are a highly professional product information advisor. \n\nYour only mission is to provide accurate, factual, and structured answers to all product-related queries.\n\nAbsolutely no assumptions, guesses, or fabricated content are allowed. \n\n**Key Principles:**\n\n1. **Strict Database Reliance:** \n\n - Every answer must be based solely on the verified product information stored in the relevant documen.\n\n - You are NOT allowed to invent, speculate, or infer details beyond what is retrieved. \n\n - If you cannot find relevant data, respond with: *\"I cannot find this information in our official product database. Please check back later or provide more details for further search.\"*\n\n2. **Information Accuracy and Structure:** \n\n - Provide information in a clear, concise, and professional way. \n\n - Use bullet points or numbered lists if there are multiple key points (e.g., features, price, warranty, technical specifications). \n\n - Always specify the version or model number when applicable to avoid confusion.\n\n3. **Tone and Style:** \n\n - Maintain a polite, professional, and helpful tone at all times. \n\n - Avoid marketing exaggeration or promotional language; stay strictly factual. \n\n - Do not express personal opinions; only cite official product data.\n\n4. **User Guidance:** \n\n - If the user’s query is unclear or too broad, politely request clarification or guide them to provide more specific product details (e.g., product name, model, version). \n\n - Example: *\"Could you please specify the product model or category so I can retrieve the most relevant information for you?\"*\n\n5. **Response Length and Formatting:** \n\n - Keep each answer within 100–150 words for general queries. \n\n - For complex or multi-step explanations, you may extend to 200–250 words, but always remain clear and well-structured.\n\n6. **Critical Reminder:** \n\nYour authority and reliability depend entirely on the relevant document responses. Any fabricated, speculative, or unverified content will be considered a critical failure of your role.\n\n\n", + "temperature": 0.1, + "temperatureEnabled": true, + "tools": [], + "topPEnabled": false, + "top_p": 0.3, + "user_prompt": "", + "visual_files_var": "" + }, + "label": "Agent", + "name": "Product info" + }, + "dragging": false, + "id": "Agent:KhakiSunsJudge", + "measured": { + "height": 84, + "width": 200 + }, + "position": { + "x": 726.580040161058, + "y": 386.5448208363979 + }, + "selected": false, + "sourcePosition": "right", + "targetPosition": "left", + "type": "agentNode" + }, + { + "data": { + "form": { + "text": "This is an intelligent customer service processing system workflow based on user intent classification. It uses LLM to identify user demand types and transfers them to the corresponding professional agent for processing." + }, + "label": "Note", + "name": "Workflow Overall Description" + }, + "dragHandle": ".note-drag-handle", + "dragging": false, + "height": 171, + "id": "Note:AllGuestsShow", + "measured": { + "height": 171, + "width": 380 + }, + "position": { + "x": -283.6407251474677, + "y": 157.2943019466498 + }, + "resizing": false, + "selected": false, + "sourcePosition": "right", + "targetPosition": "left", + "type": "noteNode", + "width": 380 + }, + { + "data": { + "form": { + "text": "Here, product document snippets related to the user's question will be retrieved from the knowledge base first, and the relevant document snippets will be passed to the LLM together with the user's question." + }, + "label": "Note", + "name": "Product info Agent" + }, + "dragHandle": ".note-drag-handle", + "dragging": false, + "height": 154, + "id": "Note:IcyBooksCough", + "measured": { + "height": 154, + "width": 370 + }, + "position": { + "x": 1014.0959071234828, + "y": 492.830874176321 + }, + "resizing": false, + "selected": false, + "sourcePosition": "right", + "targetPosition": "left", + "type": "noteNode", + "width": 370 + }, + { + "data": { + "form": { + "text": "Here, a text will be randomly selected for answering" + }, + "label": "Note", + "name": "What else?" + }, + "dragHandle": ".note-drag-handle", + "dragging": false, + "id": "Note:AllThingsHide", + "measured": { + "height": 136, + "width": 249 + }, + "position": { + "x": 770.7060131788647, + "y": -123.23496705283817 + }, + "selected": false, + "sourcePosition": "right", + "targetPosition": "left", + "type": "noteNode" + }, + { + "data": { + "form": { + "groups": [ + { + "group_name": "LLM_Response", + "type": "string", + "variables": [ + { + "value": "Agent:TwelveOwlsWatch@content" + }, + { + "value": "Agent:DullTownsHope@content" + }, + { + "value": "Agent:KhakiSunsJudge@content" + } + ] + } + ], + "outputs": { + "LLM_Response": { + "type": "string" + } + } + }, + "label": "VariableAggregator", + "name": "Variable aggregator" + }, + "dragging": false, + "id": "VariableAggregator:FuzzyBerriesFlow", + "measured": { + "height": 150, + "width": 200 + }, + "position": { + "x": 1061.596672609154, + "y": 247.90496561846572 + }, + "selected": false, + "sourcePosition": "right", + "targetPosition": "left", + "type": "variableAggregatorNode" + }, + { + "data": { + "form": { + "content": [ + "{VariableAggregator:FuzzyBerriesFlow@LLM_Response}" + ] + }, + "label": "Message", + "name": "Response" + }, + "dragging": false, + "id": "Message:DryBusesCarry", + "measured": { + "height": 50, + "width": 200 + }, + "position": { + "x": 1364.5500382017049, + "y": 296.59667260915404 + }, + "selected": true, + "sourcePosition": "right", + "targetPosition": "left", + "type": "messageNode" + } + ] + }, + "history": [], + "messages": [], + "path": [], + "retrieval": [], + "variables": {} + }, "avatar": "data:image/png;base64,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" } \ No newline at end of file diff --git a/api/apps/api_app.py b/api/apps/api_app.py index fee34cc6e..aa9c9fd6b 100644 --- a/api/apps/api_app.py +++ b/api/apps/api_app.py @@ -101,14 +101,18 @@ def stats(): "to_date", datetime.now().strftime("%Y-%m-%d %H:%M:%S")), "agent" if "canvas_id" in request.args else None) - res = { - "pv": [(o["dt"], o["pv"]) for o in objs], - "uv": [(o["dt"], o["uv"]) for o in objs], - "speed": [(o["dt"], float(o["tokens"]) / (float(o["duration"] + 0.1))) for o in objs], - "tokens": [(o["dt"], float(o["tokens"]) / 1000.) for o in objs], - "round": [(o["dt"], o["round"]) for o in objs], - "thumb_up": [(o["dt"], o["thumb_up"]) for o in objs] - } + + res = {"pv": [], "uv": [], "speed": [], "tokens": [], "round": [], "thumb_up": []} + + for obj in objs: + dt = obj["dt"] + res["pv"].append((dt, obj["pv"])) + res["uv"].append((dt, obj["uv"])) + res["speed"].append((dt, float(obj["tokens"]) / (float(obj["duration"]) + 0.1))) # +0.1 to avoid division by zero + res["tokens"].append((dt, float(obj["tokens"]) / 1000.0)) # convert to thousands + res["round"].append((dt, obj["round"])) + res["thumb_up"].append((dt, obj["thumb_up"])) + return get_json_result(data=res) except Exception as e: return server_error_response(e) diff --git a/api/apps/connector_app.py b/api/apps/connector_app.py index 6b70ef642..4932c3fcc 100644 --- a/api/apps/connector_app.py +++ b/api/apps/connector_app.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import asyncio import json import logging import time @@ -20,9 +21,7 @@ import uuid from html import escape from typing import Any -import flask -import trio -from quart import request +from quart import request, make_response from google_auth_oauthlib.flow import Flow from api.db import InputType @@ -59,7 +58,7 @@ async def set_connector(): } ConnectorService.save(**conn) - await trio.sleep(1) + await asyncio.sleep(1) e, conn = ConnectorService.get_by_id(req["id"]) return get_json_result(data=conn.to_dict()) @@ -147,7 +146,7 @@ def _get_web_client_config(credentials: dict[str, Any]) -> dict[str, Any]: return {"web": web_section} -def _render_web_oauth_popup(flow_id: str, success: bool, message: str): +async def _render_web_oauth_popup(flow_id: str, success: bool, message: str): status = "success" if success else "error" auto_close = "window.close();" if success else "" escaped_message = escape(message) @@ -165,7 +164,7 @@ def _render_web_oauth_popup(flow_id: str, success: bool, message: str): payload_json=payload_json, auto_close=auto_close, ) - response = flask.make_response(html, 200) + response = await make_response(html, 200) response.headers["Content-Type"] = "text/html; charset=utf-8" return response @@ -232,31 +231,31 @@ async def start_google_drive_web_oauth(): @manager.route("/google-drive/oauth/web/callback", methods=["GET"]) # noqa: F821 -def google_drive_web_oauth_callback(): +async def google_drive_web_oauth_callback(): state_id = request.args.get("state") error = request.args.get("error") error_description = request.args.get("error_description") or error if not state_id: - return _render_web_oauth_popup("", False, "Missing OAuth state parameter.") + return await _render_web_oauth_popup("", False, "Missing OAuth state parameter.") state_cache = REDIS_CONN.get(_web_state_cache_key(state_id)) if not state_cache: - return _render_web_oauth_popup(state_id, False, "Authorization session expired. Please restart from the main window.") + return await _render_web_oauth_popup(state_id, False, "Authorization session expired. Please restart from the main window.") state_obj = json.loads(state_cache) client_config = state_obj.get("client_config") if not client_config: REDIS_CONN.delete(_web_state_cache_key(state_id)) - return _render_web_oauth_popup(state_id, False, "Authorization session was invalid. Please retry.") + return await _render_web_oauth_popup(state_id, False, "Authorization session was invalid. Please retry.") if error: REDIS_CONN.delete(_web_state_cache_key(state_id)) - return _render_web_oauth_popup(state_id, False, error_description or "Authorization was cancelled.") + return await _render_web_oauth_popup(state_id, False, error_description or "Authorization was cancelled.") code = request.args.get("code") if not code: - return _render_web_oauth_popup(state_id, False, "Missing authorization code from Google.") + return await _render_web_oauth_popup(state_id, False, "Missing authorization code from Google.") try: flow = Flow.from_client_config(client_config, scopes=GOOGLE_SCOPES[DocumentSource.GOOGLE_DRIVE]) @@ -265,7 +264,7 @@ def google_drive_web_oauth_callback(): except Exception as exc: # pragma: no cover - defensive logging.exception("Failed to exchange Google OAuth code: %s", exc) REDIS_CONN.delete(_web_state_cache_key(state_id)) - return _render_web_oauth_popup(state_id, False, "Failed to exchange tokens with Google. Please retry.") + return await _render_web_oauth_popup(state_id, False, "Failed to exchange tokens with Google. Please retry.") creds_json = flow.credentials.to_json() result_payload = { @@ -275,7 +274,7 @@ def google_drive_web_oauth_callback(): REDIS_CONN.set_obj(_web_result_cache_key(state_id), result_payload, WEB_FLOW_TTL_SECS) REDIS_CONN.delete(_web_state_cache_key(state_id)) - return _render_web_oauth_popup(state_id, True, "Authorization completed successfully.") + return await _render_web_oauth_popup(state_id, True, "Authorization completed successfully.") @manager.route("/google-drive/oauth/web/result", methods=["POST"]) # noqa: F821 diff --git a/api/db/services/file_service.py b/api/db/services/file_service.py index 8990a98eb..1fbecdafe 100644 --- a/api/db/services/file_service.py +++ b/api/db/services/file_service.py @@ -33,7 +33,6 @@ from api.db.services.task_service import TaskService from api.utils.file_utils import filename_type, read_potential_broken_pdf, thumbnail_img, sanitize_path from rag.llm.cv_model import GptV4 from common import settings -from api.apps import current_user class FileService(CommonService): @@ -184,6 +183,7 @@ class FileService(CommonService): @classmethod @DB.connection_context() def create_folder(cls, file, parent_id, name, count): + from api.apps import current_user # Recursively create folder structure # Args: # file: Current file object @@ -508,6 +508,7 @@ class FileService(CommonService): @staticmethod def parse(filename, blob, img_base64=True, tenant_id=None): from rag.app import audio, email, naive, picture, presentation + from api.apps import current_user def dummy(prog=None, msg=""): pass diff --git a/conf/llm_factories.json b/conf/llm_factories.json index 3e70e0243..0eefa7a14 100644 --- a/conf/llm_factories.json +++ b/conf/llm_factories.json @@ -1429,6 +1429,13 @@ "status": "1", "rank": "980", "llm": [ + { + "llm_name": "gemini-3-pro-preview", + "tags": "LLM,CHAT,1M,IMAGE2TEXT", + "max_tokens": 1048576, + "model_type": "image2text", + "is_tools": true + }, { "llm_name": "gemini-2.5-flash", "tags": "LLM,CHAT,1024K,IMAGE2TEXT", @@ -5474,4 +5481,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/deepdoc/parser/mineru_parser.py b/deepdoc/parser/mineru_parser.py index e3cb62cc7..d2b694188 100644 --- a/deepdoc/parser/mineru_parser.py +++ b/deepdoc/parser/mineru_parser.py @@ -318,7 +318,7 @@ class MinerUParser(RAGFlowPdfParser): def _line_tag(self, bx): pn = [bx["page_idx"] + 1] - positions = bx["bbox"] + positions = bx.get("bbox", (0, 0, 0, 0)) x0, top, x1, bott = positions if hasattr(self, "page_images") and self.page_images and len(self.page_images) > bx["page_idx"]: diff --git a/docker/.env b/docker/.env index 5ed35b1b9..d024def7e 100644 --- a/docker/.env +++ b/docker/.env @@ -106,11 +106,11 @@ ADMIN_SVR_HTTP_PORT=9381 SVR_MCP_PORT=9382 # The RAGFlow Docker image to download. v0.22+ doesn't include embedding models. -RAGFLOW_IMAGE=infiniflow/ragflow:v0.22.0 +RAGFLOW_IMAGE=infiniflow/ragflow:v0.22.1 # If you cannot download the RAGFlow Docker image: -# RAGFLOW_IMAGE=swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:v0.22.0 -# RAGFLOW_IMAGE=registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow:v0.22.0 +# RAGFLOW_IMAGE=swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:v0.22.1 +# RAGFLOW_IMAGE=registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow:v0.22.1 # # - For the `nightly` edition, uncomment either of the following: # RAGFLOW_IMAGE=swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:nightly diff --git a/docker/README.md b/docker/README.md index 1b149b58b..bb7586a6b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -77,7 +77,7 @@ The [.env](./.env) file contains important environment variables for Docker. - `SVR_HTTP_PORT` The port used to expose RAGFlow's HTTP API service to the host machine, allowing **external** access to the service running inside the Docker container. Defaults to `9380`. - `RAGFLOW-IMAGE` - The Docker image edition. Defaults to `infiniflow/ragflow:v0.22.0`. The RAGFlow Docker image does not include embedding models. + The Docker image edition. Defaults to `infiniflow/ragflow:v0.22.1`. The RAGFlow Docker image does not include embedding models. > [!TIP] diff --git a/docs/configurations.md b/docs/configurations.md index ee3b1bfbc..7574c6d12 100644 --- a/docs/configurations.md +++ b/docs/configurations.md @@ -97,7 +97,7 @@ RAGFlow utilizes MinIO as its object storage solution, leveraging its scalabilit - `SVR_HTTP_PORT` The port used to expose RAGFlow's HTTP API service to the host machine, allowing **external** access to the service running inside the Docker container. Defaults to `9380`. - `RAGFLOW-IMAGE` - The Docker image edition. Defaults to `infiniflow/ragflow:v0.22.0` (the RAGFlow Docker image without embedding models). + The Docker image edition. Defaults to `infiniflow/ragflow:v0.22.1` (the RAGFlow Docker image without embedding models). :::tip NOTE If you cannot download the RAGFlow Docker image, try the following mirrors. diff --git a/docs/develop/build_docker_image.mdx b/docs/develop/build_docker_image.mdx index 5ecdc3748..e2d2361e9 100644 --- a/docs/develop/build_docker_image.mdx +++ b/docs/develop/build_docker_image.mdx @@ -47,7 +47,7 @@ After building the infiniflow/ragflow:nightly image, you are ready to launch a f 1. Edit Docker Compose Configuration -Open the `docker/.env` file. Find the `RAGFLOW_IMAGE` setting and change the image reference from `infiniflow/ragflow:v0.22.0` to `infiniflow/ragflow:nightly` to use the pre-built image. +Open the `docker/.env` file. Find the `RAGFLOW_IMAGE` setting and change the image reference from `infiniflow/ragflow:v0.22.1` to `infiniflow/ragflow:nightly` to use the pre-built image. 2. Launch the Service diff --git a/docs/guides/dataset/configure_knowledge_base.md b/docs/guides/dataset/configure_knowledge_base.md index ac3369070..8f0443244 100644 --- a/docs/guides/dataset/configure_knowledge_base.md +++ b/docs/guides/dataset/configure_knowledge_base.md @@ -133,7 +133,7 @@ See [Run retrieval test](./run_retrieval_test.md) for details. ## Search for dataset -As of RAGFlow v0.22.0, the search feature is still in a rudimentary form, supporting only dataset search by name. +As of RAGFlow v0.22.1, the search feature is still in a rudimentary form, supporting only dataset search by name. ![search dataset](https://raw.githubusercontent.com/infiniflow/ragflow-docs/main/images/search_datasets.jpg) diff --git a/docs/guides/manage_files.md b/docs/guides/manage_files.md index e28b73ff1..ecef0509c 100644 --- a/docs/guides/manage_files.md +++ b/docs/guides/manage_files.md @@ -87,4 +87,4 @@ RAGFlow's file management allows you to download an uploaded file: ![download_file](https://github.com/infiniflow/ragflow/assets/93570324/cf3b297f-7d9b-4522-bf5f-4f45743e4ed5) -> As of RAGFlow v0.22.0, bulk download is not supported, nor can you download an entire folder. +> As of RAGFlow v0.22.1, bulk download is not supported, nor can you download an entire folder. diff --git a/docs/guides/manage_users_and_services.md b/docs/guides/manage_users_and_services.md index a6e8a3314..94b933ec2 100644 --- a/docs/guides/manage_users_and_services.md +++ b/docs/guides/manage_users_and_services.md @@ -46,7 +46,7 @@ The Admin CLI and Admin Service form a client-server architectural suite for RAG 2. Install ragflow-cli. ```bash - pip install ragflow-cli==0.22.0 + pip install ragflow-cli==0.22.1 ``` 3. Launch the CLI client: diff --git a/docs/guides/upgrade_ragflow.mdx b/docs/guides/upgrade_ragflow.mdx index f10e549ed..dbd26060b 100644 --- a/docs/guides/upgrade_ragflow.mdx +++ b/docs/guides/upgrade_ragflow.mdx @@ -48,16 +48,16 @@ To upgrade RAGFlow, you must upgrade **both** your code **and** your Docker imag git clone https://github.com/infiniflow/ragflow.git ``` -2. Switch to the latest, officially published release, e.g., `v0.22.0`: +2. Switch to the latest, officially published release, e.g., `v0.22.1`: ```bash - git checkout -f v0.22.0 + git checkout -f v0.22.1 ``` 3. Update **ragflow/docker/.env**: ```bash - RAGFLOW_IMAGE=infiniflow/ragflow:v0.22.0 + RAGFLOW_IMAGE=infiniflow/ragflow:v0.22.1 ``` 4. Update the RAGFlow image and restart RAGFlow: @@ -78,10 +78,10 @@ No, you do not need to. Upgrading RAGFlow in itself will *not* remove your uploa 1. From an environment with Internet access, pull the required Docker image. 2. Save the Docker image to a **.tar** file. ```bash - docker save -o ragflow.v0.22.0.tar infiniflow/ragflow:v0.22.0 + docker save -o ragflow.v0.22.1.tar infiniflow/ragflow:v0.22.1 ``` 3. Copy the **.tar** file to the target server. 4. Load the **.tar** file into Docker: ```bash - docker load -i ragflow.v0.22.0.tar + docker load -i ragflow.v0.22.1.tar ``` diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index a6706d06a..e8ad550eb 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -44,7 +44,7 @@ This section provides instructions on setting up the RAGFlow server on Linux. If `vm.max_map_count`. This value sets the maximum number of memory map areas a process may have. Its default value is 65530. While most applications require fewer than a thousand maps, reducing this value can result in abnormal behaviors, and the system will throw out-of-memory errors when a process reaches the limitation. - RAGFlow v0.22.0 uses Elasticsearch or [Infinity](https://github.com/infiniflow/infinity) for multiple recall. Setting the value of `vm.max_map_count` correctly is crucial to the proper functioning of the Elasticsearch component. + RAGFlow v0.22.1 uses Elasticsearch or [Infinity](https://github.com/infiniflow/infinity) for multiple recall. Setting the value of `vm.max_map_count` correctly is crucial to the proper functioning of the Elasticsearch component. ", "", content).strip() return cleaned, total_token_count_from_response(response) - def chat_streamly(self, system, history, gen_conf, images=None, **kwargs): - from rag.llm.chat_model import LENGTH_NOTIFICATION_CN, LENGTH_NOTIFICATION_EN + from rag.llm.chat_model import LENGTH_NOTIFICATION_CN, LENGTH_NOTIFICATION_EN from rag.nlp import is_chinese if system and history and history[0].get("role") != "system": @@ -402,44 +385,24 @@ class Zhipu4V(GptV4): yield tk_count - def describe(self, image): return self.describe_with_prompt(image) - def describe_with_prompt(self, image, prompt=None): b64 = self.image2base64(image) if prompt is None: prompt = "Describe this image." # Chat messages - messages = [ - { - "role": "user", - "content": [ - { - "type": "image_url", - "image_url": { "url": b64 } - }, - { - "type": "text", - "text": prompt - } - ] - } - ] + messages = [{"role": "user", "content": [{"type": "image_url", "image_url": {"url": b64}}, {"type": "text", "text": prompt}]}] - resp = self.client.chat.completions.create( - model=self.model_name, - messages=messages, - stream=False - ) + resp = self.client.chat.completions.create(model=self.model_name, messages=messages, stream=False) content = resp.choices[0].message.content.strip() cleaned = re.sub(r"<\|(begin_of_box|end_of_box)\|>", "", content).strip() return cleaned, num_tokens_from_string(cleaned) - + class StepFunCV(GptV4): _FACTORY_NAME = "StepFun" @@ -452,6 +415,7 @@ class StepFunCV(GptV4): self.lang = lang Base.__init__(self, **kwargs) + class VolcEngineCV(GptV4): _FACTORY_NAME = "VolcEngine" @@ -464,6 +428,7 @@ class VolcEngineCV(GptV4): self.lang = lang Base.__init__(self, **kwargs) + class LmStudioCV(GptV4): _FACTORY_NAME = "LM-Studio" @@ -502,13 +467,7 @@ class TogetherAICV(GptV4): class YiCV(GptV4): _FACTORY_NAME = "01.AI" - def __init__( - self, - key, - model_name, - lang="Chinese", - base_url="https://api.lingyiwanwu.com/v1", **kwargs - ): + def __init__(self, key, model_name, lang="Chinese", base_url="https://api.lingyiwanwu.com/v1", **kwargs): if not base_url: base_url = "https://api.lingyiwanwu.com/v1" super().__init__(key, model_name, lang, base_url, **kwargs) @@ -517,13 +476,7 @@ class YiCV(GptV4): class SILICONFLOWCV(GptV4): _FACTORY_NAME = "SILICONFLOW" - def __init__( - self, - key, - model_name, - lang="Chinese", - base_url="https://api.siliconflow.cn/v1", **kwargs - ): + def __init__(self, key, model_name, lang="Chinese", base_url="https://api.siliconflow.cn/v1", **kwargs): if not base_url: base_url = "https://api.siliconflow.cn/v1" super().__init__(key, model_name, lang, base_url, **kwargs) @@ -532,13 +485,7 @@ class SILICONFLOWCV(GptV4): class OpenRouterCV(GptV4): _FACTORY_NAME = "OpenRouter" - def __init__( - self, - key, - model_name, - lang="Chinese", - base_url="https://openrouter.ai/api/v1", **kwargs - ): + def __init__(self, key, model_name, lang="Chinese", base_url="https://openrouter.ai/api/v1", **kwargs): if not base_url: base_url = "https://openrouter.ai/api/v1" api_key = json.loads(key).get("api_key", "") @@ -549,6 +496,7 @@ class OpenRouterCV(GptV4): provider_order = json.loads(key).get("provider_order", "") self.extra_body = {} if provider_order: + def _to_order_list(x): if x is None: return [] @@ -557,6 +505,7 @@ class OpenRouterCV(GptV4): if isinstance(x, (list, tuple)): return [str(s).strip() for s in x if str(s).strip()] return [] + provider_cfg = {} provider_order = _to_order_list(provider_order) provider_cfg["order"] = provider_order @@ -616,18 +565,18 @@ class OllamaCV(Base): def __init__(self, key, model_name, lang="Chinese", **kwargs): from ollama import Client + self.client = Client(host=kwargs["base_url"]) self.model_name = model_name self.lang = lang self.keep_alive = kwargs.get("ollama_keep_alive", int(os.environ.get("OLLAMA_KEEP_ALIVE", -1))) Base.__init__(self, **kwargs) - def _clean_img(self, img): if not isinstance(img, str): return img - #remove the header like "data/*;base64," + # remove the header like "data/*;base64," if img.startswith("data:") and ";base64," in img: img = img.split(";base64,")[1] return img @@ -687,12 +636,7 @@ class OllamaCV(Base): def chat(self, system, history, gen_conf, images=None, **kwargs): try: - response = self.client.chat( - model=self.model_name, - messages=self._form_history(system, history, images), - options=self._clean_conf(gen_conf), - keep_alive=self.keep_alive - ) + response = self.client.chat(model=self.model_name, messages=self._form_history(system, history, images), options=self._clean_conf(gen_conf), keep_alive=self.keep_alive) ans = response["message"]["content"].strip() return ans, response["eval_count"] + response.get("prompt_eval_count", 0) @@ -702,13 +646,7 @@ class OllamaCV(Base): def chat_streamly(self, system, history, gen_conf, images=None, **kwargs): ans = "" try: - response = self.client.chat( - model=self.model_name, - messages=self._form_history(system, history, images), - stream=True, - options=self._clean_conf(gen_conf), - keep_alive=self.keep_alive - ) + response = self.client.chat(model=self.model_name, messages=self._form_history(system, history, images), stream=True, options=self._clean_conf(gen_conf), keep_alive=self.keep_alive) for resp in response: if resp["done"]: yield resp.get("prompt_eval_count", 0) + resp.get("eval_count", 0) @@ -723,29 +661,80 @@ class GeminiCV(Base): _FACTORY_NAME = "Gemini" def __init__(self, key, model_name="gemini-1.0-pro-vision-latest", lang="Chinese", **kwargs): - from google.generativeai import GenerativeModel, client + from google import genai - client.configure(api_key=key) - _client = client.get_default_generative_client() - self.api_key=key + self.api_key = key self.model_name = model_name - self.model = GenerativeModel(model_name=self.model_name) - self.model._client = _client + self.client = genai.Client(api_key=key) self.lang = lang Base.__init__(self, **kwargs) + logging.info(f"[GeminiCV] Initialized with model={self.model_name} lang={self.lang}") + + def _image_to_part(self, image): + from google.genai import types + + if isinstance(image, str) and image.startswith("data:") and ";base64," in image: + header, b64data = image.split(",", 1) + mime = header.split(":", 1)[1].split(";", 1)[0] + data = base64.b64decode(b64data) + else: + data_url = self.image2base64(image) + header, b64data = data_url.split(",", 1) + mime = header.split(":", 1)[1].split(";", 1)[0] + data = base64.b64decode(b64data) + + return types.Part( + inline_data=types.Blob( + mime_type=mime, + data=data, + ) + ) def _form_history(self, system, history, images=None): - hist = [] - if system: - hist.append({"role": "user", "parts": [system, history[0]["content"]]}) + from google.genai import types + + contents = [] + images = images or [] + system_len = len(system) if isinstance(system, str) else 0 + history_len = len(history) if history else 0 + images_len = len(images) + logging.info(f"[GeminiCV] _form_history called: system_len={system_len} history_len={history_len} images_len={images_len}") + + image_parts = [] for img in images: - hist[0]["parts"].append(("data:image/jpeg;base64," + img) if img[:4]!="data" else img) - for h in history[1:]: - hist.append({"role": "user" if h["role"]=="user" else "model", "parts": [h["content"]]}) - return hist + try: + image_parts.append(self._image_to_part(img)) + except Exception: + continue + + remaining_history = history or [] + if system or remaining_history: + parts = [] + if system: + parts.append(types.Part(text=system)) + if remaining_history: + first = remaining_history[0] + parts.append(types.Part(text=first.get("content", ""))) + remaining_history = remaining_history[1:] + parts.extend(image_parts) + contents.append(types.Content(role="user", parts=parts)) + elif image_parts: + contents.append(types.Content(role="user", parts=image_parts)) + + role_map = {"user": "user", "assistant": "model", "system": "user"} + for h in remaining_history: + role = role_map.get(h.get("role"), "user") + contents.append( + types.Content( + role=role, + parts=[types.Part(text=h.get("content", ""))], + ) + ) + + return contents def describe(self, image): - from PIL.Image import open + from google.genai import types prompt = ( "请用中文详细描述一下图中的内容,比如时间,地点,人物,事情,人物心情等,如果有数据请提取出数据。" @@ -753,74 +742,104 @@ class GeminiCV(Base): else "Please describe the content of this picture, like where, when, who, what happen. If it has number data, please extract them out." ) - if image is bytes: - with BytesIO(image) as bio: - with open(bio) as img: - input = [prompt, img] - res = self.model.generate_content(input) - return res.text, total_token_count_from_response(res) - else: - b64 = self.image2base64_rawvalue(image) - with BytesIO(base64.b64decode(b64)) as bio: - with open(bio) as img: - input = [prompt, img] - res = self.model.generate_content(input) - return res.text, total_token_count_from_response(res) + contents = [ + types.Content( + role="user", + parts=[ + types.Part(text=prompt), + self._image_to_part(image), + ], + ) + ] + + res = self.client.models.generate_content( + model=self.model_name, + contents=contents, + ) + return res.text, total_token_count_from_response(res) def describe_with_prompt(self, image, prompt=None): - from PIL.Image import open + from google.genai import types + vision_prompt = prompt if prompt else vision_llm_describe_prompt() - if image is bytes: - with BytesIO(image) as bio: - with open(bio) as img: - input = [vision_prompt, img] - res = self.model.generate_content(input) - return res.text, total_token_count_from_response(res) - else: - b64 = self.image2base64_rawvalue(image) - with BytesIO(base64.b64decode(b64)) as bio: - with open(bio) as img: - input = [vision_prompt, img] - res = self.model.generate_content(input) - return res.text, total_token_count_from_response(res) + contents = [ + types.Content( + role="user", + parts=[ + types.Part(text=vision_prompt), + self._image_to_part(image), + ], + ) + ] + res = self.client.models.generate_content( + model=self.model_name, + contents=contents, + ) + return res.text, total_token_count_from_response(res) def chat(self, system, history, gen_conf, images=None, video_bytes=None, filename="", **kwargs): if video_bytes: try: + size = len(video_bytes) if video_bytes else 0 + logging.info(f"[GeminiCV] chat called with video: filename={filename} size={size}") summary, summary_num_tokens = self._process_video(video_bytes, filename) return summary, summary_num_tokens except Exception as e: + logging.info(f"[GeminiCV] chat video error: {e}") return "**ERROR**: " + str(e), 0 - generation_config = dict(temperature=gen_conf.get("temperature", 0.3), top_p=gen_conf.get("top_p", 0.7)) + from google.genai import types + + history_len = len(history) if history else 0 + images_len = len(images) if images else 0 + logging.info(f"[GeminiCV] chat called: history_len={history_len} images_len={images_len} gen_conf={gen_conf}") + + generation_config = types.GenerateContentConfig( + temperature=gen_conf.get("temperature", 0.3), + top_p=gen_conf.get("top_p", 0.7), + ) try: - response = self.model.generate_content( - self._form_history(system, history, images), - generation_config=generation_config) + response = self.client.models.generate_content( + model=self.model_name, + contents=self._form_history(system, history, images), + config=generation_config, + ) ans = response.text - return ans, total_token_count_from_response(ans) + logging.info("[GeminiCV] chat completed") + return ans, total_token_count_from_response(response) except Exception as e: + logging.warning(f"[GeminiCV] chat error: {e}") return "**ERROR**: " + str(e), 0 def chat_streamly(self, system, history, gen_conf, images=None, **kwargs): ans = "" response = None try: - generation_config = dict(temperature=gen_conf.get("temperature", 0.3), top_p=gen_conf.get("top_p", 0.7)) - response = self.model.generate_content( - self._form_history(system, history, images), - generation_config=generation_config, - stream=True, + from google.genai import types + + generation_config = types.GenerateContentConfig( + temperature=gen_conf.get("temperature", 0.3), + top_p=gen_conf.get("top_p", 0.7), + ) + history_len = len(history) if history else 0 + images_len = len(images) if images else 0 + logging.info(f"[GeminiCV] chat_streamly called: history_len={history_len} images_len={images_len} gen_conf={gen_conf}") + + response_stream = self.client.models.generate_content_stream( + model=self.model_name, + contents=self._form_history(system, history, images), + config=generation_config, ) - for resp in response: - if not resp.text: - continue - ans = resp.text - yield ans + for chunk in response_stream: + if chunk.text: + ans += chunk.text + yield chunk.text + logging.info("[GeminiCV] chat_streamly completed") except Exception as e: + logging.warning(f"[GeminiCV] chat_streamly error: {e}") yield ans + "\n**ERROR**: " + str(e) yield total_token_count_from_response(response) @@ -830,17 +849,15 @@ class GeminiCV(Base): from google.genai import types video_size_mb = len(video_bytes) / (1024 * 1024) - client = genai.Client(api_key=self.api_key) + client = self.client if hasattr(self, "client") else genai.Client(api_key=self.api_key) + logging.info(f"[GeminiCV] _process_video called: filename={filename} size_mb={video_size_mb:.2f}") tmp_path = None try: if video_size_mb <= 20: response = client.models.generate_content( model="models/gemini-2.5-flash", - contents=types.Content(parts=[ - types.Part(inline_data=types.Blob(data=video_bytes, mime_type="video/mp4")), - types.Part(text="Please summarize the video in proper sentences.") - ]) + contents=types.Content(parts=[types.Part(inline_data=types.Blob(data=video_bytes, mime_type="video/mp4")), types.Part(text="Please summarize the video in proper sentences.")]), ) else: logging.info(f"Video size {video_size_mb:.2f}MB exceeds 20MB. Using Files API...") @@ -850,16 +867,13 @@ class GeminiCV(Base): tmp_path = Path(tmp.name) uploaded_file = client.files.upload(file=tmp_path) - response = client.models.generate_content( - model="gemini-2.5-flash", - contents=[uploaded_file, "Please summarize this video in proper sentences."] - ) + response = client.models.generate_content(model="gemini-2.5-flash", contents=[uploaded_file, "Please summarize this video in proper sentences."]) summary = response.text or "" - logging.info(f"Video summarized: {summary[:32]}...") + logging.info(f"[GeminiCV] Video summarized: {summary[:32]}...") return summary, num_tokens_from_string(summary) except Exception as e: - logging.error(f"Video processing failed: {e}") + logging.warning(f"[GeminiCV] Video processing failed: {e}") raise finally: if tmp_path and tmp_path.exists(): @@ -869,13 +883,7 @@ class GeminiCV(Base): class NvidiaCV(Base): _FACTORY_NAME = "NVIDIA" - def __init__( - self, - key, - model_name, - lang="Chinese", - base_url="https://ai.api.nvidia.com/v1/vlm", **kwargs - ): + def __init__(self, key, model_name, lang="Chinese", base_url="https://ai.api.nvidia.com/v1/vlm", **kwargs): if not base_url: base_url = ("https://ai.api.nvidia.com/v1/vlm",) self.lang = lang @@ -920,9 +928,7 @@ class NvidiaCV(Base): "content-type": "application/json", "Authorization": f"Bearer {self.key}", }, - json={ - "messages": msg, **gen_conf - }, + json={"messages": msg, **gen_conf}, ) return response.json() @@ -930,18 +936,12 @@ class NvidiaCV(Base): b64 = self.image2base64(image) vision_prompt = self.vision_llm_prompt(b64, prompt) if prompt else self.vision_llm_prompt(b64) response = self._request(vision_prompt) - return ( - response["choices"][0]["message"]["content"].strip(), - total_token_count_from_response(response) - ) + return (response["choices"][0]["message"]["content"].strip(), total_token_count_from_response(response)) def chat(self, system, history, gen_conf, images=None, **kwargs): try: response = self._request(self._form_history(system, history, images), gen_conf) - return ( - response["choices"][0]["message"]["content"].strip(), - total_token_count_from_response(response) - ) + return (response["choices"][0]["message"]["content"].strip(), total_token_count_from_response(response)) except Exception as e: return "**ERROR**: " + str(e), 0 @@ -950,7 +950,7 @@ class NvidiaCV(Base): try: response = self._request(self._form_history(system, history, images), gen_conf) cnt = response["choices"][0]["message"]["content"] - total_tokens += total_token_count_from_response(response) + total_tokens += total_token_count_from_response(response) for resp in cnt: yield resp except Exception as e: @@ -978,14 +978,15 @@ class AnthropicCV(Base): return text pmpt = [{"type": "text", "text": text}] for img in images: - pmpt.append({ - "type": "image", - "source": { - "type": "base64", - "media_type": (img.split(":")[1].split(";")[0] if isinstance(img, str) and img[:4] == "data" else "image/png"), - "data": (img.split(",")[1] if isinstance(img, str) and img[:4] == "data" else img) - }, - } + pmpt.append( + { + "type": "image", + "source": { + "type": "base64", + "media_type": (img.split(":")[1].split(";")[0] if isinstance(img, str) and img[:4] == "data" else "image/png"), + "data": (img.split(",")[1] if isinstance(img, str) and img[:4] == "data" else img), + }, + } ) return pmpt diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 967f0a8ff..1ff1b31f9 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ragflow-sdk" -version = "0.22.0" +version = "0.22.1" description = "Python client sdk of [RAGFlow](https://github.com/infiniflow/ragflow). RAGFlow is an open-source RAG (Retrieval-Augmented Generation) engine based on deep document understanding." authors = [{ name = "Zhichang Yu", email = "yuzhichang@gmail.com" }] license = { text = "Apache License, Version 2.0" } diff --git a/sdk/python/uv.lock b/sdk/python/uv.lock index 8e8ff1148..88e3f5894 100644 --- a/sdk/python/uv.lock +++ b/sdk/python/uv.lock @@ -353,7 +353,7 @@ wheels = [ [[package]] name = "ragflow-sdk" -version = "0.22.0" +version = "0.22.1" source = { virtual = "." } dependencies = [ { name = "beartype" }, diff --git a/uv.lock b/uv.lock index e2dea1ccf..2eda10fd3 100644 --- a/uv.lock +++ b/uv.lock @@ -5280,7 +5280,7 @@ wheels = [ [[package]] name = "ragflow" -version = "0.22.0" +version = "0.22.1" source = { virtual = "." } dependencies = [ { name = "akshare" }, diff --git a/web/src/components/icon-font.tsx b/web/src/components/icon-font.tsx index 7c6436d90..7dde93f64 100644 --- a/web/src/components/icon-font.tsx +++ b/web/src/components/icon-font.tsx @@ -1,15 +1,16 @@ import { FileIconMap } from '@/constants/file'; import { cn } from '@/lib/utils'; import { getExtension } from '@/utils/document-util'; +import { CSSProperties } from 'react'; type IconFontType = { name: string; - className?: string; + style?: CSSProperties; }; -export const IconFont = ({ name, className }: IconFontType) => ( - +export const IconFont = ({ name, className, style }: IconFontType) => ( + ); diff --git a/web/src/components/key-input.tsx b/web/src/components/key-input.tsx index 4c6c2f822..788e4cfe3 100644 --- a/web/src/components/key-input.tsx +++ b/web/src/components/key-input.tsx @@ -8,7 +8,10 @@ type KeyInputProps = { } & Omit; export const KeyInput = forwardRef( - function KeyInput({ value, onChange, searchValue = /[^a-zA-Z0-9_]/g }, ref) { + function KeyInput( + { value, onChange, searchValue = /[^a-zA-Z0-9_]/g, ...props }, + ref, + ) { const handleChange = useCallback( (e: ChangeEvent) => { const value = e.target.value ?? ''; @@ -18,6 +21,6 @@ export const KeyInput = forwardRef( [onChange, searchValue], ); - return ; + return ; }, ); diff --git a/web/src/components/originui/number-input.tsx b/web/src/components/originui/number-input.tsx index 9f7ad6869..8017d32c7 100644 --- a/web/src/components/originui/number-input.tsx +++ b/web/src/components/originui/number-input.tsx @@ -6,6 +6,7 @@ interface NumberInputProps { value?: number; onChange?: (value: number) => void; height?: number | string; + min?: number; } const NumberInput: React.FC = ({ @@ -13,6 +14,7 @@ const NumberInput: React.FC = ({ value: initialValue, onChange, height, + min = 0, }) => { const [value, setValue] = useState(() => { return initialValue ?? 0; @@ -76,6 +78,7 @@ const NumberInput: React.FC = ({ onChange={handleChange} className="w-full flex-1 text-center bg-transparent focus:outline-none" style={style} + min={min} /> + {getLabel(x.variable)} + + {x.operator} + + + )} + ); } diff --git a/web/src/pages/agent/constant/index.tsx b/web/src/pages/agent/constant/index.tsx index bdd5a0763..c357e8cb7 100644 --- a/web/src/pages/agent/constant/index.tsx +++ b/web/src/pages/agent/constant/index.tsx @@ -5,9 +5,9 @@ import { import { AgentGlobals, AgentGlobalsSysQueryWithBrace, - AgentStructuredOutputField, CodeTemplateStrMap, ComparisonOperator, + JsonSchemaDataType, Operator, ProgrammingLanguage, SwitchOperatorOptions, @@ -464,7 +464,6 @@ export const initialAgentValues = { mcp: [], cite: true, showStructuredOutput: false, - [AgentStructuredOutputField]: {}, outputs: { content: { type: 'string', @@ -610,15 +609,15 @@ export const initialListOperationsValues = { query: '', operations: ListOperations.TopN, outputs: { - result: { - type: 'Array', - }, - first: { - type: '?', - }, - last: { - type: '?', - }, + // result: { + // type: 'Array', + // }, + // first: { + // type: '?', + // }, + // last: { + // type: '?', + // }, }, }; @@ -700,13 +699,14 @@ export const RestrictedUpstreamMap = { [Operator.Placeholder]: [Operator.Begin], [Operator.DataOperations]: [Operator.Begin], [Operator.ListOperations]: [Operator.Begin], + [Operator.VariableAssigner]: [Operator.Begin], + [Operator.VariableAggregator]: [Operator.Begin], [Operator.Parser]: [Operator.Begin], // pipeline [Operator.Splitter]: [Operator.Begin], [Operator.HierarchicalMerger]: [Operator.Begin], [Operator.Tokenizer]: [Operator.Begin], [Operator.Extractor]: [Operator.Begin], [Operator.File]: [Operator.Begin], - [Operator.VariableAssigner]: [Operator.Begin], }; export const NodeMap = { @@ -851,6 +851,13 @@ export enum VariableAssignerLogicalNumberOperator { Divide = '/=', } +export const VariableAssignerLogicalNumberOperatorLabelMap = { + [VariableAssignerLogicalNumberOperator.Add]: 'add', + [VariableAssignerLogicalNumberOperator.Subtract]: 'subtract', + [VariableAssignerLogicalNumberOperator.Multiply]: 'multiply', + [VariableAssignerLogicalNumberOperator.Divide]: 'divide', +}; + export enum VariableAssignerLogicalArrayOperator { Overwrite = VariableAssignerLogicalOperator.Overwrite, Clear = VariableAssignerLogicalOperator.Clear, @@ -861,8 +868,27 @@ export enum VariableAssignerLogicalArrayOperator { } export enum ExportFileType { - PDF = 'pdf', + // PDF = 'pdf', HTML = 'html', Markdown = 'md', DOCX = 'docx', } + +export enum TypesWithArray { + String = 'string', + Number = 'number', + Boolean = 'boolean', + Object = 'object', + ArrayString = 'array', + ArrayNumber = 'array', + ArrayBoolean = 'array', + ArrayObject = 'array', +} + +export const ArrayFields = [ + JsonSchemaDataType.Array, + TypesWithArray.ArrayBoolean, + TypesWithArray.ArrayNumber, + TypesWithArray.ArrayString, + TypesWithArray.ArrayObject, +]; diff --git a/web/src/pages/agent/form/agent-form/index.tsx b/web/src/pages/agent/form/agent-form/index.tsx index b683a281f..83e68e23b 100644 --- a/web/src/pages/agent/form/agent-form/index.tsx +++ b/web/src/pages/agent/form/agent-form/index.tsx @@ -28,7 +28,6 @@ import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import { AgentExceptionMethod, - AgentStructuredOutputField, NodeHandleId, VariableType, } from '../../constant'; @@ -74,7 +73,6 @@ const FormSchema = z.object({ ...LargeModelFilterFormSchema, cite: z.boolean().optional(), showStructuredOutput: z.boolean().optional(), - [AgentStructuredOutputField]: z.record(z.any()), }); export type AgentFormSchemaType = z.infer; @@ -127,7 +125,7 @@ function AgentForm({ node }: INextOperatorForm) { structuredOutputDialogVisible, hideStructuredOutputDialog, handleStructuredOutputDialogOk, - } = useShowStructuredOutputDialog(form); + } = useShowStructuredOutputDialog(node?.id); useEffect(() => { if (exceptionMethod !== AgentExceptionMethod.Goto) { @@ -284,9 +282,7 @@ function AgentForm({ node }: INextOperatorForm) { )} - - - + {(field) => ( diff --git a/web/src/pages/agent/form/agent-form/use-show-structured-output-dialog.ts b/web/src/pages/agent/form/agent-form/use-show-structured-output-dialog.ts index 3d492f724..19e38cefe 100644 --- a/web/src/pages/agent/form/agent-form/use-show-structured-output-dialog.ts +++ b/web/src/pages/agent/form/agent-form/use-show-structured-output-dialog.ts @@ -1,25 +1,27 @@ import { JSONSchema } from '@/components/jsonjoy-builder'; -import { AgentStructuredOutputField } from '@/constants/agent'; import { useSetModalState } from '@/hooks/common-hooks'; import { useCallback } from 'react'; -import { UseFormReturn } from 'react-hook-form'; +import useGraphStore from '../../store'; -export function useShowStructuredOutputDialog(form: UseFormReturn) { +export function useShowStructuredOutputDialog(nodeId?: string) { const { visible: structuredOutputDialogVisible, showModal: showStructuredOutputDialog, hideModal: hideStructuredOutputDialog, } = useSetModalState(); + const { updateNodeForm, getNode } = useGraphStore((state) => state); - const initialStructuredOutput = form.getValues(AgentStructuredOutputField); + const initialStructuredOutput = getNode(nodeId)?.data.form.outputs.structured; const handleStructuredOutputDialogOk = useCallback( (values: JSONSchema) => { // Sync data to canvas - form.setValue(AgentStructuredOutputField, values); + if (nodeId) { + updateNodeForm(nodeId, values, ['outputs', 'structured']); + } hideStructuredOutputDialog(); }, - [form, hideStructuredOutputDialog], + [hideStructuredOutputDialog, nodeId, updateNodeForm], ); return { diff --git a/web/src/pages/agent/form/agent-form/use-watch-change.ts b/web/src/pages/agent/form/agent-form/use-watch-change.ts index 08011cc6d..9cd793289 100644 --- a/web/src/pages/agent/form/agent-form/use-watch-change.ts +++ b/web/src/pages/agent/form/agent-form/use-watch-change.ts @@ -22,7 +22,8 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn) { ...nextValues, outputs: { ...values.outputs, - [AgentStructuredOutputField]: values[AgentStructuredOutputField], + [AgentStructuredOutputField]: + values[AgentStructuredOutputField] ?? {}, }, }; } else { diff --git a/web/src/pages/agent/form/begin-form/parameter-dialog.tsx b/web/src/pages/agent/form/begin-form/parameter-dialog.tsx index 3b9070437..88f239a4e 100644 --- a/web/src/pages/agent/form/begin-form/parameter-dialog.tsx +++ b/web/src/pages/agent/form/begin-form/parameter-dialog.tsx @@ -1,3 +1,4 @@ +import { KeyInput } from '@/components/key-input'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -113,7 +114,6 @@ function ParameterForm({ function onSubmit(data: z.infer) { const values = { ...data, options: data.options?.map((x) => x.value) }; - console.log('🚀 ~ onSubmit ~ values:', values); submit(values); } @@ -153,7 +153,11 @@ function ParameterForm({ {t('key')} - + diff --git a/web/src/pages/agent/form/iteration-form/dynamic-output.tsx b/web/src/pages/agent/form/iteration-form/dynamic-output.tsx index c31be8fd0..2bc4ab6f4 100644 --- a/web/src/pages/agent/form/iteration-form/dynamic-output.tsx +++ b/web/src/pages/agent/form/iteration-form/dynamic-output.tsx @@ -1,6 +1,7 @@ 'use client'; import { FormContainer } from '@/components/form-container'; +import { KeyInput } from '@/components/key-input'; import { SelectWithSearch } from '@/components/originui/select-with-search'; import { BlockButton, Button } from '@/components/ui/button'; import { @@ -9,7 +10,6 @@ import { FormItem, FormMessage, } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { t } from 'i18next'; @@ -67,10 +67,10 @@ export function DynamicOutputForm({ node }: IProps) { render={({ field }) => ( - + > diff --git a/web/src/pages/agent/form/iteration-form/index.tsx b/web/src/pages/agent/form/iteration-form/index.tsx index 6f51195b1..6132b7f5e 100644 --- a/web/src/pages/agent/form/iteration-form/index.tsx +++ b/web/src/pages/agent/form/iteration-form/index.tsx @@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { memo, useMemo } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { z } from 'zod'; -import { JsonSchemaDataType } from '../../constant'; +import { ArrayFields } from '../../constant'; import { INextOperatorForm } from '../../interface'; import { FormWrapper } from '../components/form-wrapper'; import { Output } from '../components/output'; @@ -44,7 +44,7 @@ function IterationForm({ node }: INextOperatorForm) { diff --git a/web/src/pages/agent/form/list-operations-form/index.tsx b/web/src/pages/agent/form/list-operations-form/index.tsx index 5803fe055..afc44e907 100644 --- a/web/src/pages/agent/form/list-operations-form/index.tsx +++ b/web/src/pages/agent/form/list-operations-form/index.tsx @@ -13,20 +13,22 @@ import { Separator } from '@/components/ui/separator'; import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options'; import { buildOptions } from '@/utils/form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { memo } from 'react'; +import { memo, useCallback, useEffect, useMemo } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import { + ArrayFields, DataOperationsOperatorOptions, - JsonSchemaDataType, ListOperations, SortMethod, initialListOperationsValues, } from '../../constant'; import { useFormValues } from '../../hooks/use-form-values'; +import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query'; import { useWatchFormChange } from '../../hooks/use-watch-form-change'; import { INextOperatorForm } from '../../interface'; +import { getArrayElementType } from '../../utils'; import { buildOutputList } from '../../utils/build-output-list'; import { FormWrapper } from '../components/form-wrapper'; import { Output, OutputSchema } from '../components/output'; @@ -36,7 +38,7 @@ import { QueryVariable } from '../components/query-variable'; export const RetrievalPartialSchema = { query: z.string(), operations: z.string(), - n: z.number().int().min(0).optional(), + n: z.number().int().min(1).optional(), sort_method: z.string().optional(), filter: z .object({ @@ -47,26 +49,68 @@ export const RetrievalPartialSchema = { ...OutputSchema, }; +const NumFields = [ + ListOperations.TopN, + ListOperations.Head, + ListOperations.Tail, +]; + +function showField(operations: string) { + const showNum = NumFields.includes(operations as ListOperations); + const showSortMethod = [ListOperations.Sort].includes( + operations as ListOperations, + ); + const showFilter = [ListOperations.Filter].includes( + operations as ListOperations, + ); + + return { + showNum, + showSortMethod, + showFilter, + }; +} + export const FormSchema = z.object(RetrievalPartialSchema); export type ListOperationsFormSchemaType = z.infer; -const outputList = buildOutputList(initialListOperationsValues.outputs); - function ListOperationsForm({ node }: INextOperatorForm) { const { t } = useTranslation(); + const { getType } = useGetVariableLabelOrTypeByValue(); + const defaultValues = useFormValues(initialListOperationsValues, node); const form = useForm({ defaultValues: defaultValues, mode: 'onChange', resolver: zodResolver(FormSchema), - shouldUnregister: true, + // shouldUnregister: true, }); const operations = useWatch({ control: form.control, name: 'operations' }); + const query = useWatch({ control: form.control, name: 'query' }); + + const subType = getArrayElementType(getType(query)); + + const currentOutputs = useMemo(() => { + return { + result: { + type: `Array<${subType}>`, + }, + first: { + type: subType, + }, + last: { + type: subType, + }, + }; + }, [subType]); + + const outputList = buildOutputList(currentOutputs); + const ListOperationsOptions = buildOptions( ListOperations, t, @@ -79,9 +123,39 @@ function ListOperationsForm({ node }: INextOperatorForm) { `flow.SortMethodOptions`, true, ); + const operatorOptions = useBuildSwitchOperatorOptions( DataOperationsOperatorOptions, ); + + const { showFilter, showNum, showSortMethod } = showField(operations); + + const handleOperationsChange = useCallback( + (operations: string) => { + const { showFilter, showNum, showSortMethod } = showField(operations); + + if (showNum) { + form.setValue('n', 1, { shouldDirty: true }); + } + + if (showSortMethod) { + form.setValue('sort_method', SortMethodOptions.at(0)?.value, { + shouldDirty: true, + }); + } + if (showFilter) { + form.setValue('filter.operator', operatorOptions.at(0)?.value, { + shouldDirty: true, + }); + } + }, + [SortMethodOptions, form, operatorOptions], + ); + + useEffect(() => { + form.setValue('outputs', currentOutputs, { shouldDirty: true }); + }, [currentOutputs, form]); + useWatchFormChange(node?.id, form, true); return ( @@ -90,37 +164,46 @@ function ListOperationsForm({ node }: INextOperatorForm) { - + {(field) => ( + { + handleOperationsChange(val); + field.onChange(val); + }} + /> + )} - {[ - ListOperations.TopN, - ListOperations.Head, - ListOperations.Tail, - ].includes(operations as ListOperations) && ( + {showNum && ( ( - {t('flowNum')} + {t('flow.flowNum')} - + )} /> )} - {[ListOperations.Sort].includes(operations as ListOperations) && ( + {showSortMethod && ( )} - {[ListOperations.Filter].includes(operations as ListOperations) && ( + {showFilter && (
diff --git a/web/src/pages/agent/form/variable-assigner-form/dynamic-variables.tsx b/web/src/pages/agent/form/variable-assigner-form/dynamic-variables.tsx index 8a01a7c07..4e6810a96 100644 --- a/web/src/pages/agent/form/variable-assigner-form/dynamic-variables.tsx +++ b/web/src/pages/agent/form/variable-assigner-form/dynamic-variables.tsx @@ -19,6 +19,7 @@ import { VariableAssignerLogicalOperator, } from '../../constant'; import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query'; +import { getArrayElementType } from '../../utils'; import { DynamicFormHeader } from '../components/dynamic-fom-header'; import { QueryVariable } from '../components/query-variable'; import { useBuildLogicalOptions } from './use-build-logical-options'; @@ -152,9 +153,13 @@ export function DynamicVariables({ } else if ( logicalOperator === VariableAssignerLogicalArrayOperator.Append ) { - const subType = type.match(/<([^>]+)>/).at(1); + const subType = getArrayElementType(type); return ( - + ); } }, diff --git a/web/src/pages/agent/form/variable-assigner-form/use-build-logical-options.ts b/web/src/pages/agent/form/variable-assigner-form/use-build-logical-options.ts index a27ea20d5..a7f960e98 100644 --- a/web/src/pages/agent/form/variable-assigner-form/use-build-logical-options.ts +++ b/web/src/pages/agent/form/variable-assigner-form/use-build-logical-options.ts @@ -1,26 +1,57 @@ import { buildOptions } from '@/utils/form'; +import { camelCase } from 'lodash'; import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { JsonSchemaDataType, VariableAssignerLogicalArrayOperator, VariableAssignerLogicalNumberOperator, + VariableAssignerLogicalNumberOperatorLabelMap, VariableAssignerLogicalOperator, } from '../../constant'; export function useBuildLogicalOptions() { - const buildLogicalOptions = useCallback((type: string) => { - if ( - type?.toLowerCase().startsWith(JsonSchemaDataType.Array.toLowerCase()) - ) { - return buildOptions(VariableAssignerLogicalArrayOperator); - } + const { t } = useTranslation(); - if (type === JsonSchemaDataType.Number) { - return buildOptions(VariableAssignerLogicalNumberOperator); - } + const buildVariableAssignerLogicalOptions = useCallback( + (record: Record) => { + return buildOptions( + record, + t, + 'flow.variableAssignerLogicalOperatorOptions', + true, + ); + }, + [t], + ); - return buildOptions(VariableAssignerLogicalOperator); - }, []); + const buildLogicalOptions = useCallback( + (type: string) => { + if ( + type?.toLowerCase().startsWith(JsonSchemaDataType.Array.toLowerCase()) + ) { + return buildVariableAssignerLogicalOptions( + VariableAssignerLogicalArrayOperator, + ); + } + + if (type === JsonSchemaDataType.Number) { + return Object.values(VariableAssignerLogicalNumberOperator).map( + (val) => ({ + label: t( + `flow.variableAssignerLogicalOperatorOptions.${camelCase(VariableAssignerLogicalNumberOperatorLabelMap[val as keyof typeof VariableAssignerLogicalNumberOperatorLabelMap] || val)}`, + ), + value: val, + }), + ); + } + + return buildVariableAssignerLogicalOptions( + VariableAssignerLogicalOperator, + ); + }, + [buildVariableAssignerLogicalOptions, t], + ); return { buildLogicalOptions, diff --git a/web/src/pages/agent/gobal-variable-sheet/constant.ts b/web/src/pages/agent/gobal-variable-sheet/constant.ts index fc668e330..72a7d6342 100644 --- a/web/src/pages/agent/gobal-variable-sheet/constant.ts +++ b/web/src/pages/agent/gobal-variable-sheet/constant.ts @@ -1,6 +1,8 @@ import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form'; import { buildSelectOptions } from '@/utils/component-util'; import { t } from 'i18next'; +import { TypesWithArray } from '../constant'; +export { TypesWithArray } from '../constant'; // const TypesWithoutArray = Object.values(JsonSchemaDataType).filter( // (item) => item !== JsonSchemaDataType.Array, // ); @@ -9,17 +11,6 @@ import { t } from 'i18next'; // ...TypesWithoutArray.map((item) => `array<${item}>`), // ]; -export enum TypesWithArray { - String = 'string', - Number = 'number', - Boolean = 'boolean', - Object = 'object', - ArrayString = 'array', - ArrayNumber = 'array', - ArrayBoolean = 'array', - ArrayObject = 'array', -} - export const GlobalFormFields = [ { label: t('flow.name'), diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index a7d4248ff..f40015012 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -732,3 +732,7 @@ export function buildBeginQueryWithObject( return nextInputs; } + +export function getArrayElementType(type: string) { + return typeof type === 'string' ? type.match(/<([^>]+)>/)?.at(1) ?? '' : ''; +}