Compare commits

...

30 Commits

Author SHA1 Message Date
e6cb74b27f Fix (next search): Optimize the search problem interface and related functions #3221 (#9569)
### What problem does this PR solve?

Fix (next search): Optimize the search problem interface and related
functions #3221

-Add search_id to the retrievval_test interface
-Optimize handleSearchStrChange and handleSearch callbacks to determine
whether to enable AI search based on search configuration

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-19 19:22:07 +08:00
00f54c207e Fix: Reset all data except the first one on the chat page shared with others #3221 (#9567)
### What problem does this PR solve?

Fix: Reset all data except the first one on the chat page shared with
others #3221

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-19 19:04:40 +08:00
d0dc56166c Fix: no effect on retrieval_test in term of metadata filter. (#9566)
### What problem does this PR solve?


### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-19 18:57:35 +08:00
e15e39f183 Fix: Fixed an issue where renaming a chat would create a new chat #3221 (#9563)
### What problem does this PR solve?

Fix: Fixed an issue where renaming a chat would create a new chat #3221
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-19 18:33:55 +08:00
33f3e05b75 Refa: create new name for duplicated dialog name (#9558)
### What problem does this PR solve?

 Create new name for duplicated dialog name.

### Type of change

- [x] Refactoring
2025-08-19 18:14:04 +08:00
b8bfbac2e5 Feat: Switch the root route to the new page #3221 (#9560)
### What problem does this PR solve?

Feat: Switch the root route to the new page #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-08-19 17:41:03 +08:00
d5729e598f Docs: Updated workarounds for uploading file to an agent (#9561)
### What problem does this PR solve?


### Type of change


- [x] Documentation Update
2025-08-19 17:40:39 +08:00
f2c5ad170d Fix(search): Search application list supports renaming function #3221 (#9555)
### What problem does this PR solve?

Fix (search): Search application list supports renaming function #3221

-Update the search application list page and add a renaming operation
entry
-Modify the search application details interface to support obtaining
detailed information
-Optimize search settings page layout and style

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-19 17:35:32 +08:00
0aa3c4cdae Docs: Update version references to v0.20.2 in READMEs and docs (#9559)
### What problem does this PR solve?

- Update version tags in README files (including translations) from
v0.20.1 to v0.20.2
- Modify Docker image references and documentation to reflect new
version
- Update version badges and image descriptions
- Maintain consistency across all language variants of README files

### Type of change

- [x] Documentation Update
2025-08-19 17:26:49 +08:00
f123587538 Feat: add meta filter to search app. (#9554)
### What problem does this PR solve?


### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-08-19 17:25:44 +08:00
a41a646909 Fix: Fixed the issue where clicking the SQL tool test button did not request the interface #9541 (#9542)
### What problem does this PR solve?

Fix: Fixed the issue where clicking the SQL tool test button did not
request the interface #9541
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-19 16:41:32 +08:00
787e0c6786 Refa: OpenAI whisper-1 (#9552)
### What problem does this PR solve?

Refactor OpenAI to enable audio parsing.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
2025-08-19 16:41:18 +08:00
05ee1be1e9 Docs: Updated v0.20.2 release notes (#9553)
### What problem does this PR solve?

### Type of change


- [x] Documentation Update
2025-08-19 16:03:42 +08:00
a0d630365c Refactor:Improve VoyageRerank not texts handling (#9539)
### What problem does this PR solve?

Improve VoyageRerank not texts handling

### Type of change

- [x] Refactoring
2025-08-19 10:31:04 +08:00
b5b8032a56 Feat: Support metadata auto filer for Search. (#9524)
### What problem does this PR solve?

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-08-19 10:27:24 +08:00
ccb9f0b0d7 Feature (agent): Allow Retrieval kb_ids param use kb_id,and allow list kb_name or kb_id (#9531)
### What problem does this PR solve?

Allow Retrieval kb_ids param use kb_id,and allow list kb_name or kb_id。
- Add judgment on whether the knowledge base name is a list and support
batch queries
-When the knowledge base name does not exist, try using the ID for
querying
-If both query methods fail, throw an exception

### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2025-08-19 09:42:39 +08:00
a0ab619aeb Fix: ensure update_progress loop always waits between iterations (#9528)
Move stop_event.wait(6) into finally block so that even when an
exception occurs, the loop still sleeps before retrying. This prevents
busy looping and excessive error logs when Redis connection fails.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-19 09:42:15 +08:00
32349481ef Feat: Allow agent operators to select speech-to-text models #3221 (#9534)
### What problem does this PR solve?

Feat: Allow agent operators to select speech-to-text models #3221
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-08-19 09:40:01 +08:00
2b9ed935f3 feat(search): Optimized search functionality and user interface #3221 (#9535)
### What problem does this PR solve?

feat(search): Optimized search functionality and user interface #3221
### Type of change
- Added similarity threshold adjustment function
- Optimized mind map display logic
- Adjusted search settings interface layout
- Fixed related search and document viewing functions
- Optimized time display and node selection logic

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-19 09:39:48 +08:00
188c0f614b Refa: refine search app (#9536)
### What problem does this PR solve?

Refine search app.

### Type of change

- [x] Refactoring
2025-08-19 09:33:33 +08:00
dad97869b6 Fix: search service reference (#9533)
### What problem does this PR solve?

- Update search_app.py to use SearchService instead of
KnowledgebaseService for duplicate

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-18 19:02:10 +08:00
57c8a37285 Feat: add dialog chatbots info (#9530)
### What problem does this PR solve?

Add dialog chatbots info.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-08-18 19:01:45 +08:00
9d0fed601d Feat: Displays the embedded page of the chat module #3221 (#9532)
### What problem does this PR solve?

Feat: Displays the embedded page of the chat module #3221
Feat: Let the agen operator support the selection of tts model #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-08-18 18:02:13 +08:00
fe32952825 Fix: Gemini parameters error (#9520)
### What problem does this PR solve?

Fix Gemini parameters error.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
2025-08-18 14:51:10 +08:00
5808aef28c Fix (search): Optimize the search page functionality and UI #3221 (#9525)
### What problem does this PR solve?

Fix (search): Optimize the search page functionality and UI #3221 

- Add a search list component
- Implement search settings
- Optimize search result display
- Add related search functionality
- Adjust the search input box style
- Unify internationalized text

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-18 14:50:29 +08:00
ca720bd811 Fix: save team's canvas issue. (#9518)
### What problem does this PR solve?


### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-18 13:05:29 +08:00
ba11312766 Feat: embedded search (#9501)
### What problem does this PR solve?

Add embedded search functionality.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
2025-08-18 12:05:11 +08:00
c8bbf7452d Env: Update dependencies for proxy support (#9519)
### What problem does this PR solve?

- Update httpx dependency to include socks support in pyproject.toml
- Update lockfile with new socksio dependency

### Type of change

- [x] Update dependencies for proxy support
2025-08-18 12:04:16 +08:00
b08650bc4c Feat: Fixed the chat model setting echo issue (#9521)
### What problem does this PR solve?

Feat: Fixed the chat model setting echo issue

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-08-18 12:03:33 +08:00
fb77f9917b Refactor: Use Input Length In DefaultRerank (#9516)
### What problem does this PR solve?

1. Use input length to prepare res
2. Adjust torch_empty_cache code location

### Type of change

- [x] Refactoring
- [x] Performance Improvement
2025-08-18 10:00:27 +08:00
146 changed files with 2869 additions and 1309 deletions

View File

@ -22,7 +22,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.1">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.2">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@ -190,7 +190,7 @@ 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.20.1-slim` edition of the RAGFlow Docker image. See the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.20.1-slim`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server. For example: set `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1` for the full edition `v0.20.1`.
> The command below downloads the `v0.20.2-slim` edition of the RAGFlow Docker image. See the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.20.2-slim`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server. For example: set `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2` for the full edition `v0.20.2`.
```bash
$ cd ragflow/docker
@ -203,8 +203,8 @@ releases! 🌟
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
|-------------------|-----------------|-----------------------|--------------------------|
| v0.20.1 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.1-slim | &approx;2 | ❌ | Stable release |
| v0.20.2 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.2-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;9 | :heavy_check_mark: | _Unstable_ nightly build |
| nightly-slim | &approx;2 | ❌ | _Unstable_ nightly build |

View File

@ -22,7 +22,7 @@
<img alt="Lencana Daring" src="https://img.shields.io/badge/Online-Demo-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.1">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.2">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Rilis%20Terbaru" alt="Rilis Terbaru">
@ -181,7 +181,7 @@ 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.20.1-slim dari gambar Docker RAGFlow. Silakan merujuk ke tabel berikut untuk deskripsi berbagai edisi RAGFlow. Untuk mengunduh edisi RAGFlow yang berbeda dari v0.20.1-slim, perbarui variabel RAGFLOW_IMAGE di docker/.env sebelum menggunakan docker compose untuk memulai server. Misalnya, atur RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1 untuk edisi lengkap v0.20.1.
> Perintah di bawah ini mengunduh edisi v0.20.2-slim dari gambar Docker RAGFlow. Silakan merujuk ke tabel berikut untuk deskripsi berbagai edisi RAGFlow. Untuk mengunduh edisi RAGFlow yang berbeda dari v0.20.2-slim, perbarui variabel RAGFLOW_IMAGE di docker/.env sebelum menggunakan docker compose untuk memulai server. Misalnya, atur RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2 untuk edisi lengkap v0.20.2.
```bash
$ cd ragflow/docker
@ -194,8 +194,8 @@ $ docker compose -f docker-compose.yml up -d
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | ------------------------ |
| v0.20.1 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.1-slim | &approx;2 | ❌ | Stable release |
| v0.20.2 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.2-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;9 | :heavy_check_mark: | _Unstable_ nightly build |
| nightly-slim | &approx;2 | ❌ | _Unstable_ nightly build |

View File

@ -22,7 +22,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.1">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.2">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@ -160,7 +160,7 @@
> 現在、公式に提供されているすべての Docker イメージは x86 アーキテクチャ向けにビルドされており、ARM64 用の Docker イメージは提供されていません。
> ARM64 アーキテクチャのオペレーティングシステムを使用している場合は、[このドキュメント](https://ragflow.io/docs/dev/build_docker_image)を参照して Docker イメージを自分でビルドしてください。
> 以下のコマンドは、RAGFlow Docker イメージの v0.20.1-slim エディションをダウンロードします。異なる RAGFlow エディションの説明については、以下の表を参照してください。v0.20.1-slim とは異なるエディションをダウンロードするには、docker/.env ファイルの RAGFLOW_IMAGE 変数を適宜更新し、docker compose を使用してサーバーを起動してください。例えば、完全版 v0.20.1 をダウンロードするには、RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1 と設定します。
> 以下のコマンドは、RAGFlow Docker イメージの v0.20.2-slim エディションをダウンロードします。異なる RAGFlow エディションの説明については、以下の表を参照してください。v0.20.2-slim とは異なるエディションをダウンロードするには、docker/.env ファイルの RAGFLOW_IMAGE 変数を適宜更新し、docker compose を使用してサーバーを起動してください。例えば、完全版 v0.20.2 をダウンロードするには、RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2 と設定します。
```bash
$ cd ragflow/docker
@ -173,8 +173,8 @@
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | ------------------------ |
| v0.20.1 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.1-slim | &approx;2 | ❌ | Stable release |
| v0.20.2 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.2-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;9 | :heavy_check_mark: | _Unstable_ nightly build |
| nightly-slim | &approx;2 | ❌ | _Unstable_ nightly build |

View File

@ -22,7 +22,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.1">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.2">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@ -160,7 +160,7 @@
> 모든 Docker 이미지는 x86 플랫폼을 위해 빌드되었습니다. 우리는 현재 ARM64 플랫폼을 위한 Docker 이미지를 제공하지 않습니다.
> ARM64 플랫폼을 사용 중이라면, [시스템과 호환되는 Docker 이미지를 빌드하려면 이 가이드를 사용해 주세요](https://ragflow.io/docs/dev/build_docker_image).
> 아래 명령어는 RAGFlow Docker 이미지의 v0.20.1-slim 버전을 다운로드합니다. 다양한 RAGFlow 버전에 대한 설명은 다음 표를 참조하십시오. v0.20.1-slim과 다른 RAGFlow 버전을 다운로드하려면, docker/.env 파일에서 RAGFLOW_IMAGE 변수를 적절히 업데이트한 후 docker compose를 사용하여 서버를 시작하십시오. 예를 들어, 전체 버전인 v0.20.1을 다운로드하려면 RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1로 설정합니다.
> 아래 명령어는 RAGFlow Docker 이미지의 v0.20.2-slim 버전을 다운로드합니다. 다양한 RAGFlow 버전에 대한 설명은 다음 표를 참조하십시오. v0.20.2-slim과 다른 RAGFlow 버전을 다운로드하려면, docker/.env 파일에서 RAGFLOW_IMAGE 변수를 적절히 업데이트한 후 docker compose를 사용하여 서버를 시작하십시오. 예를 들어, 전체 버전인 v0.20.2을 다운로드하려면 RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2로 설정합니다.
```bash
$ cd ragflow/docker
@ -173,8 +173,8 @@
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | ------------------------ |
| v0.20.1 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.1-slim | &approx;2 | ❌ | Stable release |
| v0.20.2 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.2-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;9 | :heavy_check_mark: | _Unstable_ nightly build |
| nightly-slim | &approx;2 | ❌ | _Unstable_ nightly build |

View File

@ -22,7 +22,7 @@
<img alt="Badge Estático" src="https://img.shields.io/badge/Online-Demo-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.1">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.2">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Última%20Relese" alt="Última Versão">
@ -180,7 +180,7 @@ 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.20.1-slim` 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.20.1-slim`, atualize a variável `RAGFLOW_IMAGE` conforme necessário no **docker/.env** antes de usar `docker compose` para iniciar o servidor. Por exemplo: defina `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1` para a edição completa `v0.20.1`.
> O comando abaixo baixa a edição `v0.20.2-slim` 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.20.2-slim`, atualize a variável `RAGFLOW_IMAGE` conforme necessário no **docker/.env** antes de usar `docker compose` para iniciar o servidor. Por exemplo: defina `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2` para a edição completa `v0.20.2`.
```bash
$ cd ragflow/docker
@ -193,8 +193,8 @@ Experimente nossa demo em [https://demo.ragflow.io](https://demo.ragflow.io).
| Tag da imagem RAGFlow | Tamanho da imagem (GB) | Possui modelos de incorporação? | Estável? |
| --------------------- | ---------------------- | ------------------------------- | ------------------------ |
| v0.20.1 | ~9 | :heavy_check_mark: | Lançamento estável |
| v0.20.1-slim | ~2 | ❌ | Lançamento estável |
| v0.20.2 | ~9 | :heavy_check_mark: | Lançamento estável |
| v0.20.2-slim | ~2 | ❌ | Lançamento estável |
| nightly | ~9 | :heavy_check_mark: | _Instável_ build noturno |
| nightly-slim | ~2 | ❌ | _Instável_ build noturno |

View File

@ -22,7 +22,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.1">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.2">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@ -183,7 +183,7 @@
> 所有 Docker 映像檔都是為 x86 平台建置的。目前,我們不提供 ARM64 平台的 Docker 映像檔。
> 如果您使用的是 ARM64 平台,請使用 [這份指南](https://ragflow.io/docs/dev/build_docker_image) 來建置適合您系統的 Docker 映像檔。
> 執行以下指令會自動下載 RAGFlow slim Docker 映像 `v0.20.1-slim`。請參考下表查看不同 Docker 發行版的說明。如需下載不同於 `v0.20.1-slim` 的 Docker 映像,請在執行 `docker compose` 啟動服務之前先更新 **docker/.env** 檔案內的 `RAGFLOW_IMAGE` 變數。例如,你可以透過設定 `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1` 來下載 RAGFlow 鏡像的 `v0.20.1` 完整發行版。
> 執行以下指令會自動下載 RAGFlow slim Docker 映像 `v0.20.2-slim`。請參考下表查看不同 Docker 發行版的說明。如需下載不同於 `v0.20.2-slim` 的 Docker 映像,請在執行 `docker compose` 啟動服務之前先更新 **docker/.env** 檔案內的 `RAGFLOW_IMAGE` 變數。例如,你可以透過設定 `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2` 來下載 RAGFlow 鏡像的 `v0.20.2` 完整發行版。
```bash
$ cd ragflow/docker
@ -196,8 +196,8 @@
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | ------------------------ |
| v0.20.1 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.1-slim | &approx;2 | ❌ | Stable release |
| v0.20.2 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.2-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;9 | :heavy_check_mark: | _Unstable_ nightly build |
| nightly-slim | &approx;2 | ❌ | _Unstable_ nightly build |

View File

@ -22,7 +22,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Online-Demo-4e6b99">
</a>
<a href="https://hub.docker.com/r/infiniflow/ragflow" target="_blank">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.1">
<img src="https://img.shields.io/docker/pulls/infiniflow/ragflow?label=Docker%20Pulls&color=0db7ed&logo=docker&logoColor=white&style=flat-square" alt="docker pull infiniflow/ragflow:v0.20.2">
</a>
<a href="https://github.com/infiniflow/ragflow/releases/latest">
<img src="https://img.shields.io/github/v/release/infiniflow/ragflow?color=blue&label=Latest%20Release" alt="Latest Release">
@ -183,7 +183,7 @@
> 请注意,目前官方提供的所有 Docker 镜像均基于 x86 架构构建,并不提供基于 ARM64 的 Docker 镜像。
> 如果你的操作系统是 ARM64 架构,请参考[这篇文档](https://ragflow.io/docs/dev/build_docker_image)自行构建 Docker 镜像。
> 运行以下命令会自动下载 RAGFlow slim Docker 镜像 `v0.20.1-slim`。请参考下表查看不同 Docker 发行版的描述。如需下载不同于 `v0.20.1-slim` 的 Docker 镜像,请在运行 `docker compose` 启动服务之前先更新 **docker/.env** 文件内的 `RAGFLOW_IMAGE` 变量。比如,你可以通过设置 `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1` 来下载 RAGFlow 镜像的 `v0.20.1` 完整发行版。
> 运行以下命令会自动下载 RAGFlow slim Docker 镜像 `v0.20.2-slim`。请参考下表查看不同 Docker 发行版的描述。如需下载不同于 `v0.20.2-slim` 的 Docker 镜像,请在运行 `docker compose` 启动服务之前先更新 **docker/.env** 文件内的 `RAGFLOW_IMAGE` 变量。比如,你可以通过设置 `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2` 来下载 RAGFlow 镜像的 `v0.20.2` 完整发行版。
```bash
$ cd ragflow/docker
@ -196,8 +196,8 @@
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | ------------------------ |
| v0.20.1 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.1-slim | &approx;2 | ❌ | Stable release |
| v0.20.2 | &approx;9 | :heavy_check_mark: | Stable release |
| v0.20.2-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;9 | :heavy_check_mark: | _Unstable_ nightly build |
| nightly-slim | &approx;2 | ❌ | _Unstable_ nightly build |

View File

@ -484,7 +484,7 @@ class Canvas:
threads.append(exe.submit(FileService.parse, file["name"], FileService.get_blob(file["created_by"], file["id"]), True, file["created_by"]))
return [th.result() for th in threads]
def tool_use_callback(self, agent_id: str, func_name: str, params: dict, result: Any):
def tool_use_callback(self, agent_id: str, func_name: str, params: dict, result: Any, elapsed_time=None):
agent_ids = agent_id.split("-->")
agent_name = self.get_component_name(agent_ids[0])
path = agent_name if len(agent_ids) < 2 else agent_name+"-->"+"-->".join(agent_ids[1:])
@ -493,16 +493,16 @@ class Canvas:
if bin:
obj = json.loads(bin.encode("utf-8"))
if obj[-1]["component_id"] == agent_ids[0]:
obj[-1]["trace"].append({"path": path, "tool_name": func_name, "arguments": params, "result": result})
obj[-1]["trace"].append({"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time})
else:
obj.append({
"component_id": agent_ids[0],
"trace": [{"path": path, "tool_name": func_name, "arguments": params, "result": result}]
"trace": [{"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time}]
})
else:
obj = [{
"component_id": agent_ids[0],
"trace": [{"path": path, "tool_name": func_name, "arguments": params, "result": result}]
"trace": [{"path": path, "tool_name": func_name, "arguments": params, "result": result, "elapsed_time": elapsed_time}]
}]
REDIS_CONN.set_obj(f"{self.task_id}-{self.message_id}-logs", obj, 60*10)
except Exception as e:

View File

@ -22,7 +22,7 @@ from functools import partial
from typing import Any
import json_repair
from timeit import default_timer as timer
from agent.tools.base import LLMToolPluginCallSession, ToolParamBase, ToolBase, ToolMeta
from api.db.services.llm_service import LLMBundle
from api.db.services.tenant_llm_service import TenantLLMService
@ -215,8 +215,9 @@ class Agent(LLM, ToolBase):
hist = deepcopy(history)
last_calling = ""
if len(hist) > 3:
st = timer()
user_request = full_question(messages=history, chat_mdl=self.chat_mdl)
self.callback("Multi-turn conversation optimization", {}, user_request)
self.callback("Multi-turn conversation optimization", {}, user_request, elapsed_time=timer()-st)
else:
user_request = history[-1]["content"]
@ -263,12 +264,13 @@ class Agent(LLM, ToolBase):
if not need2cite or cited:
return
st = timer()
txt = ""
for delta_ans in self._gen_citations(entire_txt):
yield delta_ans, 0
txt += delta_ans
self.callback("gen_citations", {}, txt)
self.callback("gen_citations", {}, txt, elapsed_time=timer()-st)
def append_user_content(hist, content):
if hist[-1]["role"] == "user":
@ -276,8 +278,9 @@ class Agent(LLM, ToolBase):
else:
hist.append({"role": "user", "content": content})
st = timer()
task_desc = analyze_task(self.chat_mdl, prompt, user_request, tool_metas)
self.callback("analyze_task", {}, task_desc)
self.callback("analyze_task", {}, task_desc, elapsed_time=timer()-st)
for _ in range(self._param.max_rounds + 1):
response, tk = next_step(self.chat_mdl, hist, tool_metas, task_desc)
# self.callback("next_step", {}, str(response)[:256]+"...")
@ -303,9 +306,10 @@ class Agent(LLM, ToolBase):
thr.append(executor.submit(use_tool, name, args))
st = timer()
reflection = reflect(self.chat_mdl, hist, [th.result() for th in thr])
append_user_content(hist, reflection)
self.callback("reflection", {}, str(reflection))
self.callback("reflection", {}, str(reflection), elapsed_time=timer()-st)
except Exception as e:
logging.exception(msg=f"Wrong JSON argument format in LLM ReAct response: {e}")

View File

@ -479,7 +479,7 @@ class ComponentBase(ABC):
def get_input_elements_from_text(self, txt: str) -> dict[str, dict[str, str]]:
res = {}
for r in re.finditer(self.variable_ref_patt, txt, flags=re.IGNORECASE):
for r in re.finditer(self.variable_ref_patt, txt, flags=re.IGNORECASE|re.DOTALL):
exp = r.group(1)
cpn_id, var_nm = exp.split("@") if exp.find("@")>0 else ("", exp)
res[exp] = {

View File

@ -54,6 +54,8 @@ class Message(ComponentBase):
if k in kwargs:
continue
v = v["value"]
if not v:
v = ""
ans = ""
if isinstance(v, partial):
for t in v():
@ -94,6 +96,8 @@ class Message(ComponentBase):
continue
v = self._canvas.get_variable_value(exp)
if not v:
v = ""
if isinstance(v, partial):
cnt = ""
for t in v():

View File

@ -24,6 +24,7 @@ from api.utils import hash_str2int
from rag.llm.chat_model import ToolCallSession
from rag.prompts.prompts import kb_prompt
from rag.utils.mcp_tool_call_conn import MCPToolCallSession
from timeit import default_timer as timer
class ToolParameter(TypedDict):
@ -49,12 +50,13 @@ class LLMToolPluginCallSession(ToolCallSession):
def tool_call(self, name: str, arguments: dict[str, Any]) -> Any:
assert name in self.tools_map, f"LLM tool {name} does not exist"
st = timer()
if isinstance(self.tools_map[name], MCPToolCallSession):
resp = self.tools_map[name].tool_call(name, arguments, 60)
else:
resp = self.tools_map[name].invoke(**arguments)
self.callback(name, arguments, resp)
self.callback(name, arguments, resp, elapsed_time=timer()-st)
return resp
def get_tool_obj(self, name):

View File

@ -67,9 +67,17 @@ class CodeExecParam(ToolParamBase):
"description": """
This tool has a sandbox that can execute code written in 'Python'/'Javascript'. It recieves a piece of code and return a Json string.
Here's a code example for Python(`main` function MUST be included):
def main(arg1: str, arg2: str) -> dict:
def main() -> dict:
\"\"\"
Generate Fibonacci numbers within 100.
\"\"\"
def fibonacci_recursive(n):
if n <= 1:
return n
else:
return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
return {
"result": arg1 + arg2,
"result": fibonacci_recursive(100),
}
Here's a code example for Javascript(`main` function MUST be included and exported):

View File

@ -79,6 +79,17 @@ class ExeSQL(ToolBase, ABC):
@timeout(os.environ.get("COMPONENT_EXEC_TIMEOUT", 60))
def _invoke(self, **kwargs):
def convert_decimals(obj):
from decimal import Decimal
if isinstance(obj, Decimal):
return float(obj) # 或 str(obj)
elif isinstance(obj, dict):
return {k: convert_decimals(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [convert_decimals(item) for item in obj]
return obj
sql = kwargs.get("sql")
if not sql:
raise Exception("SQL for `ExeSQL` MUST not be empty.")
@ -122,7 +133,11 @@ class ExeSQL(ToolBase, ABC):
single_res = pd.DataFrame([i for i in cursor.fetchmany(self._param.max_records)])
single_res.columns = [i[0] for i in cursor.description]
sql_res.append(single_res.to_dict(orient='records'))
for col in single_res.columns:
if pd.api.types.is_datetime64_any_dtype(single_res[col]):
single_res[col] = single_res[col].dt.strftime('%Y-%m-%d')
sql_res.append(convert_decimals(single_res.to_dict(orient='records')))
formalized_content.append(single_res.to_markdown(index=False, floatfmt=".6f"))
self.set_output("json", sql_res)
@ -130,4 +145,4 @@ class ExeSQL(ToolBase, ABC):
return self.output("formalized_content")
def thoughts(self) -> str:
return "Query sent—waiting for the data."
return "Query sent—waiting for the data."

View File

@ -86,10 +86,16 @@ class Retrieval(ToolBase, ABC):
kb_ids.append(id)
continue
kb_nm = self._canvas.get_variable_value(id)
e, kb = KnowledgebaseService.get_by_name(kb_nm, self._canvas._tenant_id)
if not e:
raise Exception(f"Dataset({kb_nm}) does not exist.")
kb_ids.append(kb.id)
# if kb_nm is a list
kb_nm_list = kb_nm if isinstance(kb_nm, list) else [kb_nm]
for nm_or_id in kb_nm_list:
e, kb = KnowledgebaseService.get_by_name(nm_or_id,
self._canvas._tenant_id)
if not e:
e, kb = KnowledgebaseService.get_by_id(nm_or_id)
if not e:
raise Exception(f"Dataset({nm_or_id}) does not exist.")
kb_ids.append(kb.id)
filtered_kb_ids: list[str] = list(set([kb_id for kb_id in kb_ids if kb_id]))

View File

@ -74,11 +74,11 @@ def rm():
@login_required
def save():
req = request.json
req["user_id"] = current_user.id
if not isinstance(req["dsl"], str):
req["dsl"] = json.dumps(req["dsl"], ensure_ascii=False)
req["dsl"] = json.loads(req["dsl"])
if "id" not in req:
req["user_id"] = current_user.id
if UserCanvasService.query(user_id=current_user.id, title=req["title"].strip()):
return get_data_error_result(message=f"{req['title'].strip()} already exists.")
req["id"] = get_uuid()
@ -115,6 +115,12 @@ def getsse(canvas_id):
if not objs:
return get_data_error_result(message='Authentication error: API key is invalid!"')
tenant_id = objs[0].tenant_id
if not UserCanvasService.query(user_id=tenant_id, id=canvas_id):
return get_json_result(
data=False,
message='Only owner of canvas authorized for this operation.',
code=RetCode.OPERATING_ERROR
)
e, c = UserCanvasService.get_by_id(canvas_id)
if not e or c.user_id != tenant_id:
return get_data_error_result(message="canvas not found.")

View File

@ -23,15 +23,18 @@ from flask_login import current_user, login_required
from api import settings
from api.db import LLMType, ParserType
from api.db.services.dialog_service import meta_filter
from api.db.services.document_service import DocumentService
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.db.services.llm_service import LLMBundle
from api.db.services.search_service import SearchService
from api.db.services.user_service import UserTenantService
from api.utils.api_utils import get_data_error_result, get_json_result, server_error_response, validate_request
from rag.app.qa import beAdoc, rmPrefix
from rag.app.tag import label_question
from rag.nlp import rag_tokenizer, search
from rag.prompts import cross_languages, keyword_extraction
from rag.prompts.prompts import gen_meta_filter
from rag.settings import PAGERANK_FLD
from rag.utils import rmSpace
@ -288,13 +291,26 @@ def retrieval_test():
if isinstance(kb_ids, str):
kb_ids = [kb_ids]
doc_ids = req.get("doc_ids", [])
similarity_threshold = float(req.get("similarity_threshold", 0.0))
vector_similarity_weight = float(req.get("vector_similarity_weight", 0.3))
use_kg = req.get("use_kg", False)
top = int(req.get("top_k", 1024))
langs = req.get("cross_languages", [])
tenant_ids = []
if req.get("search_id", ""):
search_config = SearchService.get_detail(req.get("search_id", "")).get("search_config", {})
meta_data_filter = search_config.get("meta_data_filter", {})
metas = DocumentService.get_meta_by_kbs(kb_ids)
if meta_data_filter.get("method") == "auto":
chat_mdl = LLMBundle(current_user.id, LLMType.CHAT, llm_name=search_config.get("chat_id", ""))
filters = gen_meta_filter(chat_mdl, metas, question)
doc_ids.extend(meta_filter(metas, filters))
if not doc_ids:
doc_ids = None
elif meta_data_filter.get("method") == "manual":
doc_ids.extend(meta_filter(metas, meta_data_filter["manual"]))
if not doc_ids:
doc_ids = None
try:
tenants = UserTenantService.query(user_id=current_user.id)
for kb_id in kb_ids:
@ -327,7 +343,9 @@ def retrieval_test():
labels = label_question(question, [kb])
ranks = settings.retrievaler.retrieval(question, embd_mdl, tenant_ids, kb_ids, page, size,
similarity_threshold, vector_similarity_weight, top,
float(req.get("similarity_threshold", 0.0)),
float(req.get("vector_similarity_weight", 0.3)),
top,
doc_ids, rerank_mdl=rerank_mdl, highlight=req.get("highlight"),
rank_feature=labels
)

View File

@ -17,23 +17,19 @@ import json
import re
import traceback
from copy import deepcopy
import trio
from flask import Response, request
from flask_login import current_user, login_required
from api import settings
from api.db import LLMType
from api.db.db_models import APIToken
from api.db.services.conversation_service import ConversationService, structure_answer
from api.db.services.dialog_service import DialogService, ask, chat
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.db.services.dialog_service import DialogService, ask, chat, gen_mindmap
from api.db.services.llm_service import LLMBundle
from api.db.services.search_service import SearchService
from api.db.services.tenant_llm_service import TenantLLMService
from api.db.services.user_service import TenantService, UserTenantService
from api.utils.api_utils import get_data_error_result, get_json_result, server_error_response, validate_request
from graphrag.general.mind_map_extractor import MindMapExtractor
from rag.app.tag import label_question
from rag.prompts.prompt_template import load_prompt
from rag.prompts.prompts import chunks_format
@ -343,10 +339,18 @@ def ask_about():
req = request.json
uid = current_user.id
search_id = req.get("search_id", "")
search_app = None
search_config = {}
if search_id:
search_app = SearchService.get_detail(search_id)
if search_app:
search_config = search_app.get("search_config", {})
def stream():
nonlocal req, uid
try:
for ans in ask(req["question"], req["kb_ids"], uid):
for ans in ask(req["question"], req["kb_ids"], uid, search_config=search_config):
yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
except Exception as e:
yield "data:" + json.dumps({"code": 500, "message": str(e), "data": {"answer": "**ERROR**: " + str(e), "reference": []}}, ensure_ascii=False) + "\n\n"
@ -365,18 +369,14 @@ def ask_about():
@validate_request("question", "kb_ids")
def mindmap():
req = request.json
kb_ids = req["kb_ids"]
e, kb = KnowledgebaseService.get_by_id(kb_ids[0])
if not e:
return get_data_error_result(message="Knowledgebase not found!")
search_id = req.get("search_id", "")
search_app = SearchService.get_detail(search_id) if search_id else {}
search_config = search_app.get("search_config", {}) if search_app else {}
kb_ids = search_config.get("kb_ids", [])
kb_ids.extend(req["kb_ids"])
kb_ids = list(set(kb_ids))
embd_mdl = LLMBundle(kb.tenant_id, LLMType.EMBEDDING, llm_name=kb.embd_id)
chat_mdl = LLMBundle(current_user.id, LLMType.CHAT)
question = req["question"]
ranks = settings.retrievaler.retrieval(question, embd_mdl, kb.tenant_id, kb_ids, 1, 12, 0.3, 0.3, aggs=False, rank_feature=label_question(question, [kb]))
mindmap = MindMapExtractor(chat_mdl)
mind_map = trio.run(mindmap, [c["content_with_weight"] for c in ranks["chunks"]])
mind_map = mind_map.output
mind_map = gen_mindmap(req["question"], kb_ids, search_app.get("tenant_id", current_user.id), search_config)
if "error" in mind_map:
return server_error_response(Exception(mind_map["error"]))
return get_json_result(data=mind_map)
@ -387,41 +387,20 @@ def mindmap():
@validate_request("question")
def related_questions():
req = request.json
search_id = req.get("search_id", "")
search_config = {}
if search_id:
if search_app := SearchService.get_detail(search_id):
search_config = search_app.get("search_config", {})
question = req["question"]
chat_mdl = LLMBundle(current_user.id, LLMType.CHAT)
prompt = """
Role: You are an AI language model assistant tasked with generating 5-10 related questions based on a users original query. These questions should help expand the search query scope and improve search relevance.
Instructions:
Input: You are provided with a users question.
Output: Generate 5-10 alternative questions that are related to the original user question. These alternatives should help retrieve a broader range of relevant documents from a vector database.
Context: Focus on rephrasing the original question in different ways, making sure the alternative questions are diverse but still connected to the topic of the original query. Do not create overly obscure, irrelevant, or unrelated questions.
Fallback: If you cannot generate any relevant alternatives, do not return any questions.
Guidance:
1. Each alternative should be unique but still relevant to the original query.
2. Keep the phrasing clear, concise, and easy to understand.
3. Avoid overly technical jargon or specialized terms unless directly relevant.
4. Ensure that each question contributes towards improving search results by broadening the search angle, not narrowing it.
chat_id = search_config.get("chat_id", "")
chat_mdl = LLMBundle(current_user.id, LLMType.CHAT, chat_id)
Example:
Original Question: What are the benefits of electric vehicles?
Alternative Questions:
1. How do electric vehicles impact the environment?
2. What are the advantages of owning an electric car?
3. What is the cost-effectiveness of electric vehicles?
4. How do electric vehicles compare to traditional cars in terms of fuel efficiency?
5. What are the environmental benefits of switching to electric cars?
6. How do electric vehicles help reduce carbon emissions?
7. Why are electric vehicles becoming more popular?
8. What are the long-term savings of using electric vehicles?
9. How do electric vehicles contribute to sustainability?
10. What are the key benefits of electric vehicles for consumers?
Reason:
Rephrasing the original query into multiple alternative questions helps the user explore different aspects of their search topic, improving the quality of search results.
These questions guide the search engine to provide a more comprehensive set of relevant documents.
"""
gen_conf = search_config.get("llm_setting", {"temperature": 0.9})
prompt = load_prompt("related_question")
ans = chat_mdl.chat(
prompt,
[
@ -433,6 +412,6 @@ Related search terms:
""",
}
],
{"temperature": 0.9},
gen_conf,
)
return get_json_result(data=[re.sub(r"^[0-9]\. ", "", a) for a in ans.split("\n") if re.match(r"^[0-9]\. ", a)])

View File

@ -16,6 +16,7 @@
from flask import request
from flask_login import login_required, current_user
from api.db.services import duplicate_name
from api.db.services.dialog_service import DialogService
from api.db import StatusEnum
from api.db.services.tenant_llm_service import TenantLLMService
@ -41,6 +42,15 @@ def set_dialog():
return get_data_error_result(message="Dialog name can't be empty.")
if len(name.encode("utf-8")) > 255:
return get_data_error_result(message=f"Dialog name length is {len(name)} which is larger than 255")
if is_create and DialogService.query(tenant_id=current_user.id, name=name.strip()):
name = name.strip()
name = duplicate_name(
DialogService.query,
name=name,
tenant_id=current_user.id,
status=StatusEnum.VALID.value)
description = req.get("description", "A helpful dialog")
icon = req.get("icon", "")
top_n = req.get("top_n", 6)

View File

@ -19,6 +19,7 @@ import time
import tiktoken
from flask import Response, jsonify, request
from agent.canvas import Canvas
from api import settings
from api.db import LLMType, StatusEnum
from api.db.db_models import APIToken
from api.db.services.api_service import API4ConversationService
@ -26,12 +27,17 @@ from api.db.services.canvas_service import UserCanvasService, completionOpenAI
from api.db.services.canvas_service import completion as agent_completion
from api.db.services.conversation_service import ConversationService, iframe_completion
from api.db.services.conversation_service import completion as rag_completion
from api.db.services.dialog_service import DialogService, ask, chat
from api.db.services.dialog_service import DialogService, ask, chat, gen_mindmap
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.db.services.llm_service import LLMBundle
from api.db.services.search_service import SearchService
from api.db.services.user_service import UserTenantService
from api.utils import get_uuid
from api.utils.api_utils import check_duplicate_ids, get_data_openai, get_error_data_result, get_result, token_required, validate_request
from api.utils.api_utils import check_duplicate_ids, get_data_openai, get_error_data_result, get_json_result, get_result, server_error_response, token_required, validate_request
from rag.app.tag import label_question
from rag.prompts import chunks_format
from rag.prompts.prompt_template import load_prompt
from rag.prompts.prompts import cross_languages, keyword_extraction
@manager.route("/chats/<chat_id>/sessions", methods=["POST"]) # noqa: F821
@ -808,6 +814,29 @@ def chatbot_completions(dialog_id):
return get_result(data=answer)
@manager.route("/chatbots/<dialog_id>/info", methods=["GET"]) # noqa: F821
def chatbots_inputs(dialog_id):
token = request.headers.get("Authorization").split()
if len(token) != 2:
return get_error_data_result(message='Authorization is not valid!"')
token = token[1]
objs = APIToken.query(beta=token)
if not objs:
return get_error_data_result(message='Authentication error: API key is invalid!"')
e, dialog = DialogService.get_by_id(dialog_id)
if not e:
return get_error_data_result(f"Can't find dialog by ID: {dialog_id}")
return get_result(
data={
"title": dialog.name,
"avatar": dialog.icon,
"prologue": dialog.prompt_config.get("prologue", ""),
}
)
@manager.route("/agentbots/<agent_id>/completions", methods=["POST"]) # noqa: F821
def agent_bot_completions(agent_id):
req = request.json
@ -855,3 +884,225 @@ def begin_inputs(agent_id):
"prologue": canvas.get_prologue()
}
)
@manager.route("/searchbots/ask", methods=["POST"]) # noqa: F821
@validate_request("question", "kb_ids")
def ask_about_embedded():
token = request.headers.get("Authorization").split()
if len(token) != 2:
return get_error_data_result(message='Authorization is not valid!"')
token = token[1]
objs = APIToken.query(beta=token)
if not objs:
return get_error_data_result(message='Authentication error: API key is invalid!"')
req = request.json
uid = objs[0].tenant_id
search_id = req.get("search_id", "")
search_config = {}
if search_id:
if search_app := SearchService.get_detail(search_id):
search_config = search_app.get("search_config", {})
def stream():
nonlocal req, uid
try:
for ans in ask(req["question"], req["kb_ids"], uid, search_config):
yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
except Exception as e:
yield "data:" + json.dumps({"code": 500, "message": str(e), "data": {"answer": "**ERROR**: " + str(e), "reference": []}}, ensure_ascii=False) + "\n\n"
yield "data:" + json.dumps({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
resp = Response(stream(), mimetype="text/event-stream")
resp.headers.add_header("Cache-control", "no-cache")
resp.headers.add_header("Connection", "keep-alive")
resp.headers.add_header("X-Accel-Buffering", "no")
resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
return resp
@manager.route("/searchbots/retrieval_test", methods=['POST']) # noqa: F821
@validate_request("kb_id", "question")
def retrieval_test_embedded():
token = request.headers.get("Authorization").split()
if len(token) != 2:
return get_error_data_result(message='Authorization is not valid!"')
token = token[1]
objs = APIToken.query(beta=token)
if not objs:
return get_error_data_result(message='Authentication error: API key is invalid!"')
req = request.json
page = int(req.get("page", 1))
size = int(req.get("size", 30))
question = req["question"]
kb_ids = req["kb_id"]
if isinstance(kb_ids, str):
kb_ids = [kb_ids]
doc_ids = req.get("doc_ids", [])
similarity_threshold = float(req.get("similarity_threshold", 0.0))
vector_similarity_weight = float(req.get("vector_similarity_weight", 0.3))
use_kg = req.get("use_kg", False)
top = int(req.get("top_k", 1024))
langs = req.get("cross_languages", [])
tenant_ids = []
tenant_id = objs[0].tenant_id
if not tenant_id:
return get_error_data_result(message="permission denined.")
try:
tenants = UserTenantService.query(user_id=tenant_id)
for kb_id in kb_ids:
for tenant in tenants:
if KnowledgebaseService.query(
tenant_id=tenant.tenant_id, id=kb_id):
tenant_ids.append(tenant.tenant_id)
break
else:
return get_json_result(
data=False, message='Only owner of knowledgebase authorized for this operation.',
code=settings.RetCode.OPERATING_ERROR)
e, kb = KnowledgebaseService.get_by_id(kb_ids[0])
if not e:
return get_error_data_result(message="Knowledgebase not found!")
if langs:
question = cross_languages(kb.tenant_id, None, question, langs)
embd_mdl = LLMBundle(kb.tenant_id, LLMType.EMBEDDING.value, llm_name=kb.embd_id)
rerank_mdl = None
if req.get("rerank_id"):
rerank_mdl = LLMBundle(kb.tenant_id, LLMType.RERANK.value, llm_name=req["rerank_id"])
if req.get("keyword", False):
chat_mdl = LLMBundle(kb.tenant_id, LLMType.CHAT)
question += keyword_extraction(chat_mdl, question)
labels = label_question(question, [kb])
ranks = settings.retrievaler.retrieval(question, embd_mdl, tenant_ids, kb_ids, page, size,
similarity_threshold, vector_similarity_weight, top,
doc_ids, rerank_mdl=rerank_mdl, highlight=req.get("highlight"),
rank_feature=labels
)
if use_kg:
ck = settings.kg_retrievaler.retrieval(question,
tenant_ids,
kb_ids,
embd_mdl,
LLMBundle(kb.tenant_id, LLMType.CHAT))
if ck["content_with_weight"]:
ranks["chunks"].insert(0, ck)
for c in ranks["chunks"]:
c.pop("vector", None)
ranks["labels"] = labels
return get_json_result(data=ranks)
except Exception as e:
if str(e).find("not_found") > 0:
return get_json_result(data=False, message='No chunk found! Check the chunk status please!',
code=settings.RetCode.DATA_ERROR)
return server_error_response(e)
@manager.route("/searchbots/related_questions", methods=["POST"]) # noqa: F821
@validate_request("question")
def related_questions_embedded():
token = request.headers.get("Authorization").split()
if len(token) != 2:
return get_error_data_result(message='Authorization is not valid!"')
token = token[1]
objs = APIToken.query(beta=token)
if not objs:
return get_error_data_result(message='Authentication error: API key is invalid!"')
req = request.json
tenant_id = objs[0].tenant_id
if not tenant_id:
return get_error_data_result(message="permission denined.")
search_id = req.get("search_id", "")
search_config = {}
if search_id:
if search_app := SearchService.get_detail(search_id):
search_config = search_app.get("search_config", {})
question = req["question"]
chat_id = search_config.get("chat_id", "")
chat_mdl = LLMBundle(tenant_id, LLMType.CHAT, chat_id)
gen_conf = search_config.get("llm_setting", {"temperature": 0.9})
prompt = load_prompt("related_question")
ans = chat_mdl.chat(
prompt,
[
{
"role": "user",
"content": f"""
Keywords: {question}
Related search terms:
""",
}
],
gen_conf,
)
return get_json_result(data=[re.sub(r"^[0-9]\. ", "", a) for a in ans.split("\n") if re.match(r"^[0-9]\. ", a)])
@manager.route("/searchbots/detail", methods=["GET"]) # noqa: F821
def detail_share_embedded():
token = request.headers.get("Authorization").split()
if len(token) != 2:
return get_error_data_result(message='Authorization is not valid!"')
token = token[1]
objs = APIToken.query(beta=token)
if not objs:
return get_error_data_result(message='Authentication error: API key is invalid!"')
search_id = request.args["search_id"]
tenant_id = objs[0].tenant_id
if not tenant_id:
return get_error_data_result(message="permission denined.")
try:
tenants = UserTenantService.query(user_id=tenant_id)
for tenant in tenants:
if SearchService.query(tenant_id=tenant.tenant_id, id=search_id):
break
else:
return get_json_result(data=False, message="Has no permission for this operation.", code=settings.RetCode.OPERATING_ERROR)
search = SearchService.get_detail(search_id)
if not search:
return get_error_data_result(message="Can't find this Search App!")
return get_json_result(data=search)
except Exception as e:
return server_error_response(e)
@manager.route("/searchbots/mindmap", methods=["POST"]) # noqa: F821
@validate_request("question", "kb_ids")
def mindmap():
token = request.headers.get("Authorization").split()
if len(token) != 2:
return get_error_data_result(message='Authorization is not valid!"')
token = token[1]
objs = APIToken.query(beta=token)
if not objs:
return get_error_data_result(message='Authentication error: API key is invalid!"')
tenant_id = objs[0].tenant_id
req = request.json
search_id = req.get("search_id", "")
search_app = SearchService.get_detail(search_id) if search_id else {}
mind_map = gen_mindmap(req["question"], req["kb_ids"], tenant_id, search_app.get("search_config", {}))
if "error" in mind_map:
return server_error_response(Exception(mind_map["error"]))
return get_json_result(data=mind_map)

View File

@ -22,7 +22,6 @@ from api.constants import DATASET_NAME_LIMIT
from api.db import StatusEnum
from api.db.db_models import DB
from api.db.services import duplicate_name
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.db.services.search_service import SearchService
from api.db.services.user_service import TenantService, UserTenantService
from api.utils import get_uuid
@ -47,7 +46,7 @@ def create():
return get_data_error_result(message="Authorizationd identity.")
search_name = search_name.strip()
search_name = duplicate_name(KnowledgebaseService.query, name=search_name, tenant_id=current_user.id, status=StatusEnum.VALID.value)
search_name = duplicate_name(SearchService.query, name=search_name, tenant_id=current_user.id, status=StatusEnum.VALID.value)
req["id"] = get_uuid()
req["name"] = search_name

View File

@ -872,7 +872,7 @@ class Search(DataBaseModel):
default={
"kb_ids": [],
"doc_ids": [],
"similarity_threshold": 0.0,
"similarity_threshold": 0.2,
"vector_similarity_weight": 0.3,
"use_kg": False,
# rerank settings

View File

@ -22,6 +22,7 @@ from datetime import datetime
from functools import partial
from timeit import default_timer as timer
import trio
from langfuse import Langfuse
from peewee import fn
@ -36,11 +37,12 @@ from api.db.services.langfuse_service import TenantLangfuseService
from api.db.services.llm_service import LLMBundle
from api.db.services.tenant_llm_service import TenantLLMService
from api.utils import current_timestamp, datetime_format
from graphrag.general.mind_map_extractor import MindMapExtractor
from rag.app.resume import forbidden_select_fields4resume
from rag.app.tag import label_question
from rag.nlp.search import index_name
from rag.prompts import chunks_format, citation_prompt, cross_languages, full_question, kb_prompt, keyword_extraction, message_fit_in
from rag.prompts.prompts import gen_meta_filter
from rag.prompts.prompts import gen_meta_filter, PROMPT_JINJA_ENV, ASK_SUMMARY
from rag.utils import num_tokens_from_string, rmSpace
from rag.utils.tavily_conn import Tavily
@ -687,7 +689,14 @@ def tts(tts_mdl, text):
return binascii.hexlify(bin).decode("utf-8")
def ask(question, kb_ids, tenant_id, chat_llm_name=None):
def ask(question, kb_ids, tenant_id, chat_llm_name=None, search_config={}):
doc_ids = search_config.get("doc_ids", [])
rerank_mdl = None
kb_ids = search_config.get("kb_ids", kb_ids)
chat_llm_name = search_config.get("chat_id", chat_llm_name)
rerank_id = search_config.get("rerank_id", "")
meta_data_filter = search_config.get("meta_data_filter")
kbs = KnowledgebaseService.get_by_ids(kb_ids)
embedding_list = list(set([kb.embd_id for kb in kbs]))
@ -696,30 +705,46 @@ def ask(question, kb_ids, tenant_id, chat_llm_name=None):
embd_mdl = LLMBundle(tenant_id, LLMType.EMBEDDING, embedding_list[0])
chat_mdl = LLMBundle(tenant_id, LLMType.CHAT, chat_llm_name)
if rerank_id:
rerank_mdl = LLMBundle(tenant_id, LLMType.RERANK, rerank_id)
max_tokens = chat_mdl.max_length
tenant_ids = list(set([kb.tenant_id for kb in kbs]))
kbinfos = retriever.retrieval(question, embd_mdl, tenant_ids, kb_ids, 1, 12, 0.1, 0.3, aggs=False, rank_feature=label_question(question, kbs))
if meta_data_filter:
metas = DocumentService.get_meta_by_kbs(kb_ids)
if meta_data_filter.get("method") == "auto":
filters = gen_meta_filter(chat_mdl, metas, question)
doc_ids.extend(meta_filter(metas, filters))
if not doc_ids:
doc_ids = None
elif meta_data_filter.get("method") == "manual":
doc_ids.extend(meta_filter(metas, meta_data_filter["manual"]))
if not doc_ids:
doc_ids = None
kbinfos = retriever.retrieval(
question = question,
embd_mdl=embd_mdl,
tenant_ids=tenant_ids,
kb_ids=kb_ids,
page=1,
page_size=12,
similarity_threshold=search_config.get("similarity_threshold", 0.1),
vector_similarity_weight=search_config.get("vector_similarity_weight", 0.3),
top=search_config.get("top_k", 1024),
doc_ids=doc_ids,
aggs=False,
rerank_mdl=rerank_mdl,
rank_feature=label_question(question, kbs)
)
knowledges = kb_prompt(kbinfos, max_tokens)
prompt = """
Role: You're a smart assistant. Your name is Miss R.
Task: Summarize the information from knowledge bases and answer user's question.
Requirements and restriction:
- DO NOT make things up, especially for numbers.
- If the information from knowledge is irrelevant with user's question, JUST SAY: Sorry, no relevant information provided.
- Answer with markdown format text.
- Answer in language of user's question.
- DO NOT make things up, especially for numbers.
sys_prompt = PROMPT_JINJA_ENV.from_string(ASK_SUMMARY).render(knowledge="\n".join(knowledges))
### Information from knowledge bases
%s
The above is information from knowledge bases.
""" % "\n".join(knowledges)
msg = [{"role": "user", "content": question}]
def decorate_answer(answer):
nonlocal knowledges, kbinfos, prompt
nonlocal knowledges, kbinfos, sys_prompt
answer, idx = retriever.insert_citations(answer, [ck["content_ltks"] for ck in kbinfos["chunks"]], [ck["vector"] for ck in kbinfos["chunks"]], embd_mdl, tkweight=0.7, vtweight=0.3)
idx = set([kbinfos["chunks"][int(i)]["doc_id"] for i in idx])
recall_docs = [d for d in kbinfos["doc_aggs"] if d["doc_id"] in idx]
@ -737,7 +762,55 @@ def ask(question, kb_ids, tenant_id, chat_llm_name=None):
return {"answer": answer, "reference": refs}
answer = ""
for ans in chat_mdl.chat_streamly(prompt, msg, {"temperature": 0.1}):
for ans in chat_mdl.chat_streamly(sys_prompt, msg, {"temperature": 0.1}):
answer = ans
yield {"answer": answer, "reference": {}}
yield decorate_answer(answer)
def gen_mindmap(question, kb_ids, tenant_id, search_config={}):
meta_data_filter = search_config.get("meta_data_filter", {})
doc_ids = search_config.get("doc_ids", [])
rerank_id = search_config.get("rerank_id", "")
rerank_mdl = None
kbs = KnowledgebaseService.get_by_ids(kb_ids)
if not kbs:
return {"error": "No KB selected"}
embedding_list = list(set([kb.embd_id for kb in kbs]))
tenant_ids = list(set([kb.tenant_id for kb in kbs]))
embd_mdl = LLMBundle(tenant_id, LLMType.EMBEDDING, llm_name=embedding_list[0])
chat_mdl = LLMBundle(tenant_id, LLMType.CHAT, llm_name=search_config.get("chat_id", ""))
if rerank_id:
rerank_mdl = LLMBundle(tenant_id, LLMType.RERANK, rerank_id)
if meta_data_filter:
metas = DocumentService.get_meta_by_kbs(kb_ids)
if meta_data_filter.get("method") == "auto":
filters = gen_meta_filter(chat_mdl, metas, question)
doc_ids.extend(meta_filter(metas, filters))
if not doc_ids:
doc_ids = None
elif meta_data_filter.get("method") == "manual":
doc_ids.extend(meta_filter(metas, meta_data_filter["manual"]))
if not doc_ids:
doc_ids = None
ranks = settings.retrievaler.retrieval(
question=question,
embd_mdl=embd_mdl,
tenant_ids=tenant_ids,
kb_ids=kb_ids,
page=1,
page_size=12,
similarity_threshold=search_config.get("similarity_threshold", 0.2),
vector_similarity_weight=search_config.get("vector_similarity_weight", 0.3),
top=search_config.get("top_k", 1024),
doc_ids=doc_ids,
aggs=False,
rerank_mdl=rerank_mdl,
rank_feature=label_question(question, kbs),
)
mindmap = MindMapExtractor(chat_mdl)
mind_map = trio.run(mindmap, [c["content_with_weight"] for c in ranks["chunks"]])
return mind_map.output

View File

@ -71,6 +71,8 @@ class SearchService(CommonService):
.first()
.to_dict()
)
if not search:
return {}
return search
@classmethod

View File

@ -59,11 +59,14 @@ def update_progress():
if redis_lock.acquire():
DocumentService.update_progress()
redis_lock.release()
stop_event.wait(6)
except Exception:
logging.exception("update_progress exception")
finally:
redis_lock.release()
try:
redis_lock.release()
except Exception:
logging.exception("update_progress exception")
stop_event.wait(6)
def signal_handler(sig, frame):
logging.info("Received interrupt signal, shutting down...")

View File

@ -505,6 +505,24 @@
"tags": "RE-RANK,4k",
"max_tokens": 4000,
"model_type": "rerank"
},
{
"llm_name": "qwen-audio-asr",
"tags": "SPEECH2TEXT,8k",
"max_tokens": 8000,
"model_type": "speech2text"
},
{
"llm_name": "qwen-audio-asr-latest",
"tags": "SPEECH2TEXT,8k",
"max_tokens": 8000,
"model_type": "speech2text"
},
{
"llm_name": "qwen-audio-asr-1204",
"tags": "SPEECH2TEXT,8k",
"max_tokens": 8000,
"model_type": "speech2text"
}
]
},
@ -1146,60 +1164,35 @@
"llm_name": "gemini-2.5-flash",
"tags": "LLM,CHAT,1024K,IMAGE2TEXT",
"max_tokens": 1048576,
"model_type": "image2text",
"model_type": "chat",
"is_tools": true
},
{
"llm_name": "gemini-2.5-pro",
"tags": "LLM,CHAT,IMAGE2TEXT,1024K",
"max_tokens": 1048576,
"model_type": "image2text",
"model_type": "chat",
"is_tools": true
},
{
"llm_name": "gemini-2.5-flash-preview-05-20",
"llm_name": "gemini-2.5-flash-lite",
"tags": "LLM,CHAT,1024K,IMAGE2TEXT",
"max_tokens": 1048576,
"model_type": "image2text",
"model_type": "chat",
"is_tools": true
},
{
"llm_name": "gemini-2.0-flash-001",
"tags": "LLM,CHAT,1024K",
"max_tokens": 1048576,
"model_type": "image2text",
"is_tools": true
},
{
"llm_name": "gemini-2.0-flash-thinking-exp-01-21",
"llm_name": "gemini-2.0-flash",
"tags": "LLM,CHAT,1024K",
"max_tokens": 1048576,
"model_type": "chat",
"is_tools": true
},
{
"llm_name": "gemini-1.5-flash",
"tags": "LLM,IMAGE2TEXT,1024K",
"llm_name": "gemini-2.0-flash-lite",
"tags": "LLM,CHAT,1024K",
"max_tokens": 1048576,
"model_type": "image2text"
},
{
"llm_name": "gemini-2.5-pro-preview-05-06",
"tags": "LLM,IMAGE2TEXT,1024K",
"max_tokens": 1048576,
"model_type": "image2text"
},
{
"llm_name": "gemini-1.5-pro",
"tags": "LLM,IMAGE2TEXT,2048K",
"max_tokens": 2097152,
"model_type": "image2text"
},
{
"llm_name": "gemini-1.5-flash-8b",
"tags": "LLM,IMAGE2TEXT,1024K",
"max_tokens": 1048576,
"model_type": "image2text",
"model_type": "chat",
"is_tools": true
},
{

View File

@ -94,7 +94,7 @@ SVR_HTTP_PORT=9380
# The RAGFlow Docker image to download.
# Defaults to the v0.20.1-slim edition, which is the RAGFlow Docker image without embedding models.
RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1-slim
RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2-slim
#
# To download the RAGFlow Docker image with embedding models, uncomment the following line instead:
# RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1

View File

@ -79,8 +79,8 @@ The [.env](./.env) file contains important environment variables for Docker.
- `RAGFLOW-IMAGE`
The Docker image edition. Available editions:
- `infiniflow/ragflow:v0.20.1-slim` (default): The RAGFlow Docker image without embedding models.
- `infiniflow/ragflow:v0.20.1`: The RAGFlow Docker image with embedding models including:
- `infiniflow/ragflow:v0.20.2-slim` (default): The RAGFlow Docker image without embedding models.
- `infiniflow/ragflow:v0.20.2`: The RAGFlow Docker image with embedding models including:
- Built-in embedding models:
- `BAAI/bge-large-zh-v1.5`
- `maidalun1020/bce-embedding-base_v1`

View File

@ -6,3 +6,7 @@ proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffer_size 1024k;
proxy_buffers 16 1024k;
proxy_busy_buffers_size 2048k;
proxy_temp_file_write_size 2048k;

View File

@ -99,8 +99,8 @@ RAGFlow utilizes MinIO as its object storage solution, leveraging its scalabilit
- `RAGFLOW-IMAGE`
The Docker image edition. Available editions:
- `infiniflow/ragflow:v0.20.1-slim` (default): The RAGFlow Docker image without embedding models.
- `infiniflow/ragflow:v0.20.1`: The RAGFlow Docker image with embedding models including:
- `infiniflow/ragflow:v0.20.2-slim` (default): The RAGFlow Docker image without embedding models.
- `infiniflow/ragflow:v0.20.2`: The RAGFlow Docker image with embedding models including:
- Built-in embedding models:
- `BAAI/bge-large-zh-v1.5`
- `maidalun1020/bce-embedding-base_v1`

View File

@ -77,7 +77,7 @@ After building the infiniflow/ragflow:nightly-slim image, you are ready to launc
1. Edit Docker Compose Configuration
Open the `docker/.env` file. Find the `RAGFLOW_IMAGE` setting and change the image reference from `infiniflow/ragflow:v0.20.1-slim` to `infiniflow/ragflow:nightly-slim` 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.20.2-slim` to `infiniflow/ragflow:nightly-slim` to use the pre-built image.
2. Launch the Service

View File

@ -30,17 +30,17 @@ The "garbage in garbage out" status quo remains unchanged despite the fact that
Each RAGFlow release is available in two editions:
- **Slim edition**: excludes built-in embedding models and is identified by a **-slim** suffix added to the version name. Example: `infiniflow/ragflow:v0.20.1-slim`
- **Full edition**: includes built-in embedding models and has no suffix added to the version name. Example: `infiniflow/ragflow:v0.20.1`
- **Slim edition**: excludes built-in embedding models and is identified by a **-slim** suffix added to the version name. Example: `infiniflow/ragflow:v0.20.2-slim`
- **Full edition**: includes built-in embedding models and has no suffix added to the version name. Example: `infiniflow/ragflow:v0.20.2`
---
### Which embedding models can be deployed locally?
RAGFlow offers two Docker image editions, `v0.20.1-slim` and `v0.20.1`:
RAGFlow offers two Docker image editions, `v0.20.2-slim` and `v0.20.2`:
- `infiniflow/ragflow:v0.20.1-slim` (default): The RAGFlow Docker image without embedding models.
- `infiniflow/ragflow:v0.20.1`: The RAGFlow Docker image with embedding models including:
- `infiniflow/ragflow:v0.20.2-slim` (default): The RAGFlow Docker image without embedding models.
- `infiniflow/ragflow:v0.20.2`: The RAGFlow Docker image with embedding models including:
- Built-in embedding models:
- `BAAI/bge-large-zh-v1.5`
- `maidalun1020/bce-embedding-base_v1`

View File

@ -9,7 +9,7 @@ The component equipped with reasoning, tool usage, and multi-agent collaboration
---
An **Agent** component fine-tunes the LLM and sets its prompt. From v0.20.1 onwards, an **Agent** component is able to work independently and with the following capabilities:
An **Agent** component fine-tunes the LLM and sets its prompt. From v0.20.2 onwards, an **Agent** component is able to work independently and with the following capabilities:
- Autonomous reasoning with reflection and adjustment based on environmental feedback.
- Use of tools or subagents to complete tasks.

View File

@ -9,7 +9,7 @@ A component that retrieves information from specified datasets.
## Scenarios
A **Retrieval** component is essential in most RAG scenarios, where information is extracted from designated knowledge bases before being sent to the LLM for content generation. As of v0.20.1, a **Retrieval** component can operate either as a workflow component or as a tool of an **Agent**, enabling the Agent to control its invocation and search queries.
A **Retrieval** component is essential in most RAG scenarios, where information is extracted from designated knowledge bases before being sent to the LLM for content generation. As of v0.20.2, a **Retrieval** component can operate either as a workflow component or as a tool of an **Agent**, enabling the Agent to control its invocation and search queries.
## Configurations

View File

@ -48,7 +48,7 @@ You start an AI conversation by creating an assistant.
- If no target language is selected, the system will search only in the language of your query, which may cause relevant information in other languages to be missed.
- **Variable** refers to the variables (keys) to be used in the system prompt. `{knowledge}` is a reserved variable. Click **Add** to add more variables for the system prompt.
- If you are uncertain about the logic behind **Variable**, leave it *as-is*.
- As of v0.20.1, if you add custom variables here, the only way you can pass in their values is to call:
- As of v0.20.2, if you add custom variables here, the only way you can pass in their values is to call:
- HTTP method [Converse with chat assistant](../../references/http_api_reference.md#converse-with-chat-assistant), or
- Python method [Converse with chat assistant](../../references/python_api_reference.md#converse-with-chat-assistant).

View File

@ -128,7 +128,7 @@ See [Run retrieval test](./run_retrieval_test.md) for details.
## Search for knowledge base
As of RAGFlow v0.20.1, the search feature is still in a rudimentary form, supporting only knowledge base search by name.
As of RAGFlow v0.20.2, the search feature is still in a rudimentary form, supporting only knowledge base search by name.
![search knowledge base](https://github.com/infiniflow/ragflow/assets/93570324/836ae94c-2438-42be-879e-c7ad2a59693e)

View File

@ -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.20.1, bulk download is not supported, nor can you download an entire folder.
> As of RAGFlow v0.20.2, bulk download is not supported, nor can you download an entire folder.

View File

@ -18,7 +18,7 @@ RAGFlow ships with a built-in [Langfuse](https://langfuse.com) integration so th
Langfuse stores traces, spans and prompt payloads in a purpose-built observability backend and offers filtering and visualisations on top.
:::info NOTE
• RAGFlow **≥ 0.20.1** (contains the Langfuse connector)
• RAGFlow **≥ 0.20.2** (contains the Langfuse connector)
• A Langfuse workspace (cloud or self-hosted) with a _Project Public Key_ and _Secret Key_
:::

View File

@ -66,10 +66,10 @@ 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.20.1`:
2. Switch to the latest, officially published release, e.g., `v0.20.2`:
```bash
git checkout -f v0.20.1
git checkout -f v0.20.2
```
3. Update **ragflow/docker/.env**:
@ -83,14 +83,14 @@ To upgrade RAGFlow, you must upgrade **both** your code **and** your Docker imag
<TabItem value="slim">
```bash
RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1-slim
RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2-slim
```
</TabItem>
<TabItem value="full">
```bash
RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1
RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2
```
</TabItem>
@ -114,10 +114,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.20.1.tar infiniflow/ragflow:v0.20.1
docker save -o ragflow.v0.20.2.tar infiniflow/ragflow:v0.20.2
```
3. Copy the **.tar** file to the target server.
4. Load the **.tar** file into Docker:
```bash
docker load -i ragflow.v0.20.1.tar
docker load -i ragflow.v0.20.2.tar
```

View File

@ -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.20.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.
RAGFlow v0.20.2 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.
<Tabs
defaultValue="linux"
@ -184,13 +184,13 @@ This section provides instructions on setting up the RAGFlow server on Linux. If
```bash
$ git clone https://github.com/infiniflow/ragflow.git
$ cd ragflow/docker
$ git checkout -f v0.20.1
$ git checkout -f v0.20.2
```
3. Use the pre-built Docker images and start up the server:
:::tip NOTE
The command below downloads the `v0.20.1-slim` edition of the RAGFlow Docker image. Refer to the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.20.1-slim`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server. For example: set `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.1` for the full edition `v0.20.1`.
The command below downloads the `v0.20.2-slim` edition of the RAGFlow Docker image. Refer to the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.20.2-slim`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server. For example: set `RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.2` for the full edition `v0.20.2`.
:::
```bash
@ -207,8 +207,8 @@ This section provides instructions on setting up the RAGFlow server on Linux. If
| RAGFlow image tag | Image size (GB) | Has embedding models and Python packages? | Stable? |
| ------------------- | --------------- | ----------------------------------------- | ------------------------ |
| `v0.20.1` | &approx;9 | :heavy_check_mark: | Stable release |
| `v0.20.1-slim` | &approx;2 | ❌ | Stable release |
| `v0.20.2` | &approx;9 | :heavy_check_mark: | Stable release |
| `v0.20.2-slim` | &approx;2 | ❌ | Stable release |
| `nightly` | &approx;9 | :heavy_check_mark: | *Unstable* nightly build |
| `nightly-slim` | &approx;2 | ❌ | *Unstable* nightly build |
@ -217,7 +217,7 @@ This section provides instructions on setting up the RAGFlow server on Linux. If
```
:::danger IMPORTANT
The embedding models included in `v0.20.1` and `nightly` are:
The embedding models included in `v0.20.2` and `nightly` are:
- BAAI/bge-large-zh-v1.5
- maidalun1020/bce-embedding-base_v1

View File

@ -19,7 +19,7 @@ import TOCInline from '@theme/TOCInline';
### Cross-language search
Cross-language search (also known as cross-lingual retrieval) is a feature introduced in version 0.20.1. It enables users to submit queries in one language (for example, English) and retrieve relevant documents written in other languages such as Chinese or Spanish. This feature is enabled by the systems default chat model, which translates queries to ensure accurate matching of semantic meaning across languages.
Cross-language search (also known as cross-lingual retrieval) is a feature introduced in version 0.20.2. It enables users to submit queries in one language (for example, English) and retrieve relevant documents written in other languages such as Chinese or Spanish. This feature is enabled by the systems default chat model, which translates queries to ensure accurate matching of semantic meaning across languages.
By enabling cross-language search, users can effortlessly access a broader range of information regardless of language barriers, significantly enhancing the systems usability and inclusiveness.

View File

@ -2656,7 +2656,7 @@ Creates a session with an agent.
- Body:
- the required parameters:`str`
- other parameters:
The parameters specified in the **Begin** component.
The variables specified in the **Begin** component.
##### Request example
@ -3000,13 +3000,19 @@ curl --request POST \
- `"session_id"`: (*Body Parameter*)
The ID of the session. If it is not provided, a new session will be generated.
- `"inputs"`: (*Body Parameter*)
Parameters specified in the **Begin** component.
Variables specified in the **Begin** component.
- `"user_id"`: (*Body parameter*), `string`
The optional user-defined ID. Valid *only* when no `session_id` is provided.
:::tip NOTE
For now, this method does *not* support a file type input/variable. As a workaround, use the following to upload a file to an agent:
`http://{address}/v1/canvas/upload/{agent_id}`
*You will get a corresponding file ID from its response body.*
:::
#### Response
success without `session_id` provided and with no parameters specified in the **Begin** component:
success without `session_id` provided and with no variables specified in the **Begin** component:
Stream:
@ -3074,7 +3080,7 @@ Non-stream:
}
```
Success without `session_id` provided and with parameters specified in the **Begin** component:
Success without `session_id` provided and with variables specified in the **Begin** component:
Stream:
@ -3163,7 +3169,7 @@ Non-stream:
}
```
Success with parameters specified in the **Begin** component:
Success with variables specified in the **Begin** component:
Stream:

View File

@ -22,13 +22,14 @@ The embedding models included in a full edition are:
These two embedding models are optimized specifically for English and Chinese, so performance may be compromised if you use them to embed documents in other languages.
:::
## v0.20.2 (Ongoing🔨)
## v0.20.2
Released on August ??, 2025.
Released on August 19, 2025.
### Improvements
- Revamps the user interface for the **Datasets**, **Chat**, and **Search** pages.
- Search and Chat: Introduces document-level metadata filtering, allowing automatic or manual filtering during chats or searches.
- Search: Supports creating search apps tailored to various business scenarios
- Chat: Supports comparing answer performance of up to three chat model settings on a single **Chat** page.
- Agent:
@ -42,6 +43,7 @@ Released on August ??, 2025.
### Fixed issues
- The timeout mechanism introduced in v0.20.0 caused tasks like GraphRAG to halt.
- Predefined opening greeting in the **Agent** component was missing during conversations.
- An automatic line break issue in the prompt editor.
- A memory leak issue caused by PyPDF. [#9469](https://github.com/infiniflow/ragflow/pull/9469)

View File

@ -57,7 +57,7 @@ async def run_graphrag(
):
chunks.append(d["content_with_weight"])
with trio.fail_after(len(chunks)*60):
with trio.fail_after(max(120, len(chunks)*120)):
subgraph = await generate_subgraph(
LightKGExt
if "method" not in row["kb_parser_config"].get("graphrag", {}) or row["kb_parser_config"]["graphrag"]["method"] != "general"

View File

@ -56,7 +56,7 @@ env:
ragflow:
image:
repository: infiniflow/ragflow
tag: v0.20.1-slim
tag: v0.20.2-slim
pullPolicy: IfNotPresent
pullSecrets: []
# Optional service configuration overrides

View File

@ -1,6 +1,6 @@
[project]
name = "ragflow"
version = "0.20.1"
version = "0.20.2"
description = "[RAGFlow](https://ragflow.io/) is an open-source RAG (Retrieval-Augmented Generation) engine based on deep document understanding. It offers a streamlined RAG workflow for businesses of any scale, combining LLM (Large Language Models) to provide truthful question-answering capabilities, backed by well-founded citations from various complex formatted data."
authors = [{ name = "Zhichang Yu", email = "yuzhichang@gmail.com" }]
license-files = ["LICENSE"]
@ -43,7 +43,7 @@ dependencies = [
"groq==0.9.0",
"hanziconv==0.3.2",
"html-text==0.6.2",
"httpx==0.27.2",
"httpx[socks]==0.27.2",
"huggingface-hub>=0.25.0,<0.26.0",
"infinity-sdk==0.6.0-dev4",
"infinity-emb>=0.0.66,<0.0.67",

View File

@ -14,31 +14,48 @@
# limitations under the License.
#
import os
import re
import tempfile
from api.db import LLMType
from rag.nlp import rag_tokenizer
from api.db.services.llm_service import LLMBundle
from rag.nlp import tokenize
from rag.nlp import rag_tokenizer, tokenize
def chunk(filename, binary, tenant_id, lang, callback=None, **kwargs):
doc = {
"docnm_kwd": filename,
"title_tks": rag_tokenizer.tokenize(re.sub(r"\.[a-zA-Z]+$", "", filename))
}
doc = {"docnm_kwd": filename, "title_tks": rag_tokenizer.tokenize(re.sub(r"\.[a-zA-Z]+$", "", filename))}
doc["title_sm_tks"] = rag_tokenizer.fine_grained_tokenize(doc["title_tks"])
# is it English
eng = lang.lower() == "english" # is_english(sections)
try:
_, ext = os.path.splitext(filename)
if not ext:
raise RuntimeError("No extension detected.")
if ext not in [".da", ".wave", ".wav", ".mp3", ".wav", ".aac", ".flac", ".ogg", ".aiff", ".au", ".midi", ".wma", ".realaudio", ".vqf", ".oggvorbis", ".aac", ".ape"]:
raise RuntimeError(f"Extension {ext} is not supported yet.")
tmp_path = ""
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmpf:
tmpf.write(binary)
tmpf.flush()
tmp_path = os.path.abspath(tmpf.name)
callback(0.1, "USE Sequence2Txt LLM to transcription the audio")
seq2txt_mdl = LLMBundle(tenant_id, LLMType.SPEECH2TEXT, lang=lang)
ans = seq2txt_mdl.transcription(binary)
ans = seq2txt_mdl.transcription(tmp_path)
callback(0.8, "Sequence2Txt LLM respond: %s ..." % ans[:32])
tokenize(doc, ans, eng)
return [doc]
except Exception as e:
callback(prog=-1, msg=str(e))
finally:
if tmp_path and os.path.exists(tmp_path):
try:
os.unlink(tmp_path)
except Exception:
pass
return []

View File

@ -539,24 +539,24 @@ class GeminiCV(Base):
return res.text, res.usage_metadata.total_token_count
def chat(self, system, history, gen_conf, images=[]):
from transformers import GenerationConfig
generation_config = dict(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=GenerationConfig(temperature=gen_conf.get("temperature", 0.3), top_p=gen_conf.get("top_p", 0.7)))
generation_config=generation_config)
ans = response.text
return ans, response.usage_metadata.total_token_count
except Exception as e:
return "**ERROR**: " + str(e), 0
def chat_streamly(self, system, history, gen_conf, images=[]):
from transformers import GenerationConfig
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=GenerationConfig(temperature=gen_conf.get("temperature", 0.3), top_p=gen_conf.get("top_p", 0.7)),
generation_config=generation_config,
stream=True,
)
@ -572,7 +572,7 @@ class GeminiCV(Base):
yield response.usage_metadata.total_token_count
else:
yield 0
class NvidiaCV(Base):
_FACTORY_NAME = "NVIDIA"

View File

@ -100,7 +100,7 @@ class DefaultRerank(Base):
old_dynamic_batch_size = self._dynamic_batch_size
if max_batch_size is not None:
self._dynamic_batch_size = max_batch_size
res = np.array([], dtype=float)
res = np.array(len(pairs), dtype=float)
i = 0
while i < len(pairs):
cur_i = i
@ -111,7 +111,7 @@ class DefaultRerank(Base):
try:
# call subclass implemented batch processing calculation
batch_scores = self._compute_batch_scores(pairs[i : i + current_batch])
res = np.append(res, batch_scores)
res[i : i + current_batch] = batch_scores
i += current_batch
self._dynamic_batch_size = min(self._dynamic_batch_size * 2, 8)
break
@ -125,8 +125,8 @@ class DefaultRerank(Base):
raise
if retry_count >= max_retries:
raise RuntimeError("max retry times, still cannot process batch, please check your GPU memory")
self.torch_empty_cache()
self.torch_empty_cache()
self._dynamic_batch_size = old_dynamic_batch_size
return np.array(res)
@ -482,9 +482,10 @@ class VoyageRerank(Base):
self.model_name = model_name
def similarity(self, query: str, texts: list):
rank = np.zeros(len(texts), dtype=float)
if not texts:
return rank, 0
return np.array([]), 0
rank = np.zeros(len(texts), dtype=float)
res = self.client.rerank(query=query, documents=texts, model=self.model_name, top_k=len(texts))
try:
for r in res.results:

View File

@ -35,8 +35,9 @@ class Base(ABC):
"""
pass
def transcription(self, audio, **kwargs):
transcription = self.client.audio.transcriptions.create(model=self.model_name, file=audio, response_format="text")
def transcription(self, audio_path, **kwargs):
audio_file = open(audio_path, "rb")
transcription = self.client.audio.transcriptions.create(model=self.model_name, file=audio_file)
return transcription.text.strip(), num_tokens_from_string(transcription.text.strip())
def audio2base64(self, audio):
@ -50,7 +51,7 @@ class Base(ABC):
class GPTSeq2txt(Base):
_FACTORY_NAME = "OpenAI"
def __init__(self, key, model_name="whisper-1", base_url="https://api.openai.com/v1"):
def __init__(self, key, model_name="whisper-1", base_url="https://api.openai.com/v1", **kwargs):
if not base_url:
base_url = "https://api.openai.com/v1"
self.client = OpenAI(api_key=key, base_url=base_url)
@ -60,27 +61,38 @@ class GPTSeq2txt(Base):
class QWenSeq2txt(Base):
_FACTORY_NAME = "Tongyi-Qianwen"
def __init__(self, key, model_name="paraformer-realtime-8k-v1", **kwargs):
def __init__(self, key, model_name="qwen-audio-asr", **kwargs):
import dashscope
dashscope.api_key = key
self.model_name = model_name
def transcription(self, audio, format):
from http import HTTPStatus
def transcription(self, audio_path):
if "paraformer" in self.model_name or "sensevoice" in self.model_name:
return f"**ERROR**: model {self.model_name} is not suppported yet.", 0
from dashscope.audio.asr import Recognition
from dashscope import MultiModalConversation
recognition = Recognition(model=self.model_name, format=format, sample_rate=16000, callback=None)
result = recognition.call(audio)
audio_path = f"file://{audio_path}"
messages = [
{
"role": "user",
"content": [{"audio": audio_path}],
}
]
ans = ""
if result.status_code == HTTPStatus.OK:
for sentence in result.get_sentence():
ans += sentence.text.decode("utf-8") + "\n"
return ans, num_tokens_from_string(ans)
return "**ERROR**: " + result.message, 0
response = None
full_content = ""
try:
response = MultiModalConversation.call(model="qwen-audio-asr", messages=messages, result_format="message", stream=True)
for response in response:
try:
full_content += response["output"]["choices"][0]["message"].content[0]["text"]
except Exception:
pass
return full_content, num_tokens_from_string(full_content)
except Exception as e:
return "**ERROR**: " + str(e), 0
class AzureSeq2txt(Base):
@ -212,6 +224,7 @@ class GiteeSeq2txt(Base):
self.client = OpenAI(api_key=key, base_url=base_url)
self.model_name = model_name
class DeepInfraSeq2txt(Base):
_FACTORY_NAME = "DeepInfra"

View File

@ -611,10 +611,6 @@ def naive_merge_with_images(texts, images, chunk_token_num=128, delimiter="\n。
if re.match(f"^{dels}$", sub_sec):
continue
add_chunk(sub_sec, image)
for img in images:
if isinstance(img, Image.Image):
img.close()
return cks, result_images

View File

@ -0,0 +1,14 @@
Role: You're a smart assistant. Your name is Miss R.
Task: Summarize the information from knowledge bases and answer user's question.
Requirements and restriction:
- DO NOT make things up, especially for numbers.
- If the information from knowledge is irrelevant with user's question, JUST SAY: Sorry, no relevant information provided.
- Answer with markdown format text.
- Answer in language of user's question.
- DO NOT make things up, especially for numbers.
### Information from knowledge bases
{{ knowledge }}
The above is information from knowledge bases.

View File

@ -150,6 +150,7 @@ REFLECT = load_prompt("reflect")
SUMMARY4MEMORY = load_prompt("summary4memory")
RANK_MEMORY = load_prompt("rank_memory")
META_FILTER = load_prompt("meta_filter")
ASK_SUMMARY = load_prompt("ask_summary")
PROMPT_JINJA_ENV = jinja2.Environment(autoescape=False, trim_blocks=True, lstrip_blocks=True)

View File

@ -0,0 +1,55 @@
# Role
You are an AI language model assistant tasked with generating **5-10 related questions** based on a users original query.
These questions should help **expand the search query scope** and **improve search relevance**.
---
## Instructions
**Input:**
You are provided with a **users question**.
**Output:**
Generate **5-10 alternative questions** that are **related** to the original user question.
These alternatives should help retrieve a **broader range of relevant documents** from a vector database.
**Context:**
Focus on **rephrasing** the original question in different ways, ensuring the alternative questions are **diverse but still connected** to the topic of the original query.
Do **not** create overly obscure, irrelevant, or unrelated questions.
**Fallback:**
If you cannot generate any relevant alternatives, do **not** return any questions.
---
## Guidance
1. Each alternative should be **unique** but still **relevant** to the original query.
2. Keep the phrasing **clear, concise, and easy to understand**.
3. Avoid overly technical jargon or specialized terms **unless directly relevant**.
4. Ensure that each question **broadens** the search angle, **not narrows** it.
---
## Example
**Original Question:**
> What are the benefits of electric vehicles?
**Alternative Questions:**
1. How do electric vehicles impact the environment?
2. What are the advantages of owning an electric car?
3. What is the cost-effectiveness of electric vehicles?
4. How do electric vehicles compare to traditional cars in terms of fuel efficiency?
5. What are the environmental benefits of switching to electric cars?
6. How do electric vehicles help reduce carbon emissions?
7. Why are electric vehicles becoming more popular?
8. What are the long-term savings of using electric vehicles?
9. How do electric vehicles contribute to sustainability?
10. What are the key benefits of electric vehicles for consumers?
---
## Reason
Rephrasing the original query into multiple alternative questions helps the user explore **different aspects** of their search topic, improving the **quality of search results**.
These questions guide the search engine to provide a **more comprehensive set** of relevant documents.

View File

@ -302,7 +302,7 @@ async def build_chunks(task, progress_callback):
# If the image is in RGBA mode, convert it to RGB mode before saving it in JPEG format.
if d["image"].mode in ("RGBA", "P"):
converted_image = d["image"].convert("RGB")
d["image"].close() # Close original image
#d["image"].close() # Close original image
d["image"] = converted_image
try:
d["image"].save(output_buffer, format='JPEG')

View File

@ -1,6 +1,6 @@
[project]
name = "ragflow-sdk"
version = "0.20.1"
version = "0.20.2"
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" }

2
sdk/python/uv.lock generated
View File

@ -342,7 +342,7 @@ wheels = [
[[package]]
name = "ragflow-sdk"
version = "0.20.1"
version = "0.20.2"
source = { virtual = "." }
dependencies = [
{ name = "beartype" },

20
uv.lock generated
View File

@ -2422,6 +2422,11 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0" },
]
[package.optional-dependencies]
socks = [
{ name = "socksio" },
]
[[package]]
name = "httpx-sse"
version = "0.4.1"
@ -5263,7 +5268,7 @@ wheels = [
[[package]]
name = "ragflow"
version = "0.20.1"
version = "0.20.2"
source = { virtual = "." }
dependencies = [
{ name = "akshare" },
@ -5308,7 +5313,7 @@ dependencies = [
{ name = "groq" },
{ name = "hanziconv" },
{ name = "html-text" },
{ name = "httpx" },
{ name = "httpx", extra = ["socks"] },
{ name = "huggingface-hub" },
{ name = "infinity-emb" },
{ name = "infinity-sdk" },
@ -5463,7 +5468,7 @@ requires-dist = [
{ name = "groq", specifier = "==0.9.0" },
{ name = "hanziconv", specifier = "==0.3.2" },
{ name = "html-text", specifier = "==0.6.2" },
{ name = "httpx", specifier = "==0.27.2" },
{ name = "httpx", extras = ["socks"], specifier = "==0.27.2" },
{ name = "huggingface-hub", specifier = ">=0.25.0,<0.26.0" },
{ name = "infinity-emb", specifier = ">=0.0.66,<0.0.67" },
{ name = "infinity-sdk", specifier = "==0.6.0.dev4" },
@ -6216,6 +6221,15 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" },
]
[[package]]
name = "socksio"
version = "1.0.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3" },
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"

View File

@ -317,7 +317,11 @@ export function ChunkMethodDialog({
</FormContainer>
)}
{showGraphRagItems(selectedTag as DocumentParserType) &&
useGraphRag && <UseGraphRagFormField></UseGraphRagFormField>}
useGraphRag && (
<FormContainer>
<UseGraphRagFormField></UseGraphRagFormField>
</FormContainer>
)}
{showEntityTypes && <EntityTypesFormField></EntityTypesFormField>}
</form>
</Form>

View File

@ -50,10 +50,10 @@ export function DelimiterFormField() {
}
return (
<FormItem className=" items-center space-y-0 ">
<div className="flex items-center">
<div className="flex items-center gap-1">
<FormLabel
tooltip={t('knowledgeDetails.delimiterTip')}
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
className="text-sm text-muted-foreground whitespace-break-spaces w-1/4"
>
{t('knowledgeDetails.delimiter')}
</FormLabel>

View File

@ -0,0 +1,48 @@
import { useFetchAppConf } from '@/hooks/logic-hooks';
import { RefreshCcw } from 'lucide-react';
import { PropsWithChildren } from 'react';
import { RAGFlowAvatar } from './ragflow-avatar';
import { Button } from './ui/button';
type EmbedContainerProps = {
title: string;
avatar?: string;
handleReset?(): void;
} & PropsWithChildren;
export function EmbedContainer({
title,
avatar,
children,
handleReset,
}: EmbedContainerProps) {
const appConf = useFetchAppConf();
return (
<section className="h-[100vh] flex justify-center items-center">
<div className="w-40 flex gap-2 absolute left-3 top-12 items-center">
<img src="/logo.svg" alt="" />
<span className="text-2xl font-bold">{appConf.appName}</span>
</div>
<div className=" w-[80vw] border rounded-lg">
<div className="flex justify-between items-center border-b p-3">
<div className="flex gap-2 items-center">
<RAGFlowAvatar avatar={avatar} name={title} isPerson />
<div className="text-xl text-foreground">{title}</div>
</div>
<Button
variant={'secondary'}
className="text-sm text-foreground cursor-pointer"
onClick={handleReset}
>
<div className="flex gap-1 items-center">
<RefreshCcw size={14} />
<span className="text-lg ">Reset</span>
</div>
</Button>
</div>
{children}
</div>
</section>
);
}

View File

@ -23,6 +23,7 @@ import {
} from '@/constants/common';
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { Routes } from '@/routes';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useCallback, useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
@ -68,7 +69,7 @@ function EmbedDialog({
const generateIframeSrc = useCallback(() => {
const { visibleAvatar, locale } = values;
let src = `${location.origin}/next-chat/share?shared_id=${token}&from=${from}&auth=${beta}`;
let src = `${location.origin}${from === SharedFrom.Agent ? Routes.AgentShare : Routes.ChatShare}?shared_id=${token}&from=${from}&auth=${beta}`;
if (visibleAvatar) {
src += '&visible_avatar=1';
}

View File

@ -0,0 +1,87 @@
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { useFetchManualSystemTokenList } from '@/hooks/user-setting-hooks';
import { useCallback } from 'react';
import message from '../ui/message';
export const useShowTokenEmptyError = () => {
const { t } = useTranslate('chat');
const showTokenEmptyError = useCallback(() => {
message.error(t('tokenError'));
}, [t]);
return { showTokenEmptyError };
};
export const useShowBetaEmptyError = () => {
const { t } = useTranslate('chat');
const showBetaEmptyError = useCallback(() => {
message.error(t('betaError'));
}, [t]);
return { showBetaEmptyError };
};
export const useFetchTokenListBeforeOtherStep = () => {
const { showTokenEmptyError } = useShowTokenEmptyError();
const { showBetaEmptyError } = useShowBetaEmptyError();
const { data: tokenList, fetchSystemTokenList } =
useFetchManualSystemTokenList();
let token = '',
beta = '';
if (Array.isArray(tokenList) && tokenList.length > 0) {
token = tokenList[0].token;
beta = tokenList[0].beta;
}
token =
Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : '';
const handleOperate = useCallback(async () => {
const ret = await fetchSystemTokenList();
const list = ret;
if (Array.isArray(list) && list.length > 0) {
if (!list[0].beta) {
showBetaEmptyError();
return false;
}
return list[0]?.token;
} else {
showTokenEmptyError();
return false;
}
}, [fetchSystemTokenList, showBetaEmptyError, showTokenEmptyError]);
return {
token,
beta,
handleOperate,
};
};
export const useShowEmbedModal = () => {
const {
visible: embedVisible,
hideModal: hideEmbedModal,
showModal: showEmbedModal,
} = useSetModalState();
const { handleOperate, token, beta } = useFetchTokenListBeforeOtherStep();
const handleShowEmbedModal = useCallback(async () => {
const succeed = await handleOperate();
if (succeed) {
showEmbedModal();
}
}, [handleOperate, showEmbedModal]);
return {
showEmbedModal: handleShowEmbedModal,
hideEmbedModal,
embedVisible,
embedToken: token,
beta,
};
};

View File

@ -25,10 +25,10 @@ export function ExcelToHtmlFormField() {
return (
<FormItem defaultChecked={false} className=" items-center space-y-0 ">
<div className="flex items-center">
<div className="flex items-center gap-1">
<FormLabel
tooltip={t('html4excelTip')}
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
className="text-sm text-muted-foreground whitespace-break-spaces w-1/4"
>
{t('html4excel')}
</FormLabel>

View File

@ -0,0 +1,54 @@
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Card, CardContent } from '@/components/ui/card';
import { formatDate } from '@/utils/date';
interface IProps {
data: {
name: string;
description?: string;
avatar?: string;
update_time?: string | number;
};
onClick?: () => void;
moreDropdown: React.ReactNode;
}
export function HomeCard({ data, onClick, moreDropdown }: IProps) {
return (
<Card
className="bg-bg-card border-colors-outline-neutral-standard"
onClick={() => {
// navigateToSearch(data?.id);
onClick?.();
}}
>
<CardContent className="p-4 flex gap-2 items-start group h-full">
<div className="flex justify-between mb-4">
<RAGFlowAvatar
className="w-[32px] h-[32px]"
avatar={data.avatar}
name={data.name}
/>
</div>
<div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]">
<section className="flex justify-between">
<div className="text-[20px] font-bold w-80% leading-5">
{data.name}
</div>
{moreDropdown}
</section>
<section className="flex flex-col gap-1 mt-1">
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
{data.description}
</div>
<div>
<p className="text-sm opacity-80">
{formatDate(data.update_time)}
</p>
</div>
</section>
</div>
</CardContent>
</Card>
);
}

View File

@ -16,7 +16,7 @@ import { Funnel } from 'lucide-react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { NextLLMSelect } from './llm-select/next';
import { NextInnerLLMSelectProps, NextLLMSelect } from './llm-select/next';
import { Button } from './ui/button';
const ModelTypes = [
@ -38,7 +38,13 @@ export const LargeModelFilterFormSchema = {
llm_filter: z.string().optional(),
};
export function LargeModelFormField() {
type LargeModelFormFieldProps = Pick<
NextInnerLLMSelectProps,
'showSpeech2TextModel'
>;
export function LargeModelFormField({
showSpeech2TextModel: showTTSModel,
}: LargeModelFormFieldProps) {
const form = useFormContext();
const { t } = useTranslation();
const filter = useWatch({ control: form.control, name: 'llm_filter' });
@ -85,7 +91,11 @@ export function LargeModelFormField() {
/>
<FormControl>
<NextLLMSelect {...field} filter={filter} />
<NextLLMSelect
{...field}
filter={filter}
showSpeech2TextModel={showTTSModel}
/>
</FormControl>
</section>

View File

@ -1,29 +1,41 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import * as SelectPrimitive from '@radix-ui/react-select';
import { forwardRef, memo, useState } from 'react';
import { forwardRef, memo, useMemo, useState } from 'react';
import { LlmSettingFieldItems } from '../llm-setting-items/next';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { Select, SelectTrigger, SelectValue } from '../ui/select';
interface IProps {
export interface NextInnerLLMSelectProps {
id?: string;
value?: string;
onInitialValue?: (value: string, option: any) => void;
onChange?: (value: string) => void;
disabled?: boolean;
filter?: string;
showSpeech2TextModel?: boolean;
}
const NextInnerLLMSelect = forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
IProps
>(({ value, disabled, filter }, ref) => {
NextInnerLLMSelectProps
>(({ value, disabled, filter, showSpeech2TextModel = false }, ref) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const modelTypes =
filter === 'all' || filter === undefined
? [LlmModelType.Chat, LlmModelType.Image2text]
: [filter as LlmModelType];
const ttsModel = useMemo(() => {
return showSpeech2TextModel ? [LlmModelType.Speech2text] : [];
}, [showSpeech2TextModel]);
const modelTypes = useMemo(() => {
if (filter === LlmModelType.Chat) {
return [LlmModelType.Chat];
} else if (filter === LlmModelType.Image2text) {
return [LlmModelType.Image2text, ...ttsModel];
} else {
return [LlmModelType.Chat, LlmModelType.Image2text, ...ttsModel];
}
}, [filter, ttsModel]);
const modelOptions = useComposeLlmOptionsByModelTypes(modelTypes);
return (

View File

@ -28,20 +28,32 @@ interface LlmSettingFieldItemsProps {
options?: any[];
}
export const LlmSettingSchema = {
export const LLMIdFormField = {
llm_id: z.string(),
temperature: z.coerce.number().optional(),
top_p: z.number().optional(),
presence_penalty: z.coerce.number().optional(),
frequency_penalty: z.coerce.number().optional(),
};
export const LlmSettingEnabledSchema = {
temperatureEnabled: z.boolean().optional(),
topPEnabled: z.boolean().optional(),
presencePenaltyEnabled: z.boolean().optional(),
frequencyPenaltyEnabled: z.boolean().optional(),
maxTokensEnabled: z.boolean().optional(),
};
export const LlmSettingFieldSchema = {
temperature: z.coerce.number().optional(),
top_p: z.number().optional(),
presence_penalty: z.coerce.number().optional(),
frequency_penalty: z.coerce.number().optional(),
max_tokens: z.number().optional(),
};
export const LlmSettingSchema = {
...LLMIdFormField,
...LlmSettingFieldSchema,
...LlmSettingEnabledSchema,
};
export function LlmSettingFieldItems({
prefix,
options,

View File

@ -1,6 +1,7 @@
import { settledModelVariableMap } from '@/constants/knowledge';
import { AgentFormContext } from '@/pages/agent/context';
import useGraphStore from '@/pages/agent/store';
import { setChatVariableEnabledFieldValuePage } from '@/utils/chat';
import { useCallback, useContext } from 'react';
import { useFormContext } from 'react-hook-form';
@ -11,6 +12,20 @@ export function useHandleFreedomChange(
const node = useContext(AgentFormContext);
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
const setLLMParameters = useCallback(
(values: Record<string, any>, withPrefix: boolean) => {
for (const key in values) {
if (Object.prototype.hasOwnProperty.call(values, key)) {
const realKey = getFieldWithPrefix(key);
const element = values[key as keyof typeof values];
form.setValue(withPrefix ? realKey : key, element);
}
}
},
[form, getFieldWithPrefix],
);
const handleChange = useCallback(
(parameter: string) => {
const currentValues = { ...form.getValues() };
@ -25,16 +40,12 @@ export function useHandleFreedomChange(
updateNodeForm(node?.id, nextValues);
}
for (const key in values) {
if (Object.prototype.hasOwnProperty.call(values, key)) {
const realKey = getFieldWithPrefix(key);
const element = values[key as keyof typeof values];
const variableCheckBoxFieldMap = setChatVariableEnabledFieldValuePage();
form.setValue(realKey, element);
}
}
setLLMParameters(values, true);
setLLMParameters(variableCheckBoxFieldMap, false);
},
[form, getFieldWithPrefix, node?.id, updateNodeForm],
[form, node?.id, setLLMParameters, updateNodeForm],
);
return handleChange;

View File

@ -14,6 +14,7 @@ import {
} from '@/components/file-upload';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { CircleStop, Paperclip, Send, Upload, X } from 'lucide-react';
import * as React from 'react';
import { toast } from 'sonner';
@ -135,7 +136,11 @@ export function NextMessageInput({
disabled={isUploading || disabled || sendLoading}
onKeyDown={handleKeyDown}
/>
<div className="flex items-center justify-between gap-1.5">
<div
className={cn('flex items-center justify-between gap-1.5', {
'justify-end': !showUploadIcon,
})}
>
{showUploadIcon && (
<FileUploadTrigger asChild>
<Button

View File

@ -24,7 +24,7 @@
.messageText {
.chunkText();
.messageTextBase();
background-color: #e6f4ff;
// background-color: #e6f4ff;
word-break: break-word;
}
.messageTextDark {

View File

@ -9,6 +9,7 @@ import {
useFetchDocumentThumbnailsByIds,
} from '@/hooks/document-hooks';
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { cn } from '@/lib/utils';
import { IMessage } from '@/pages/chat/interface';
import MarkdownContent from '@/pages/chat/markdown-content';
import { Avatar, Flex, Space } from 'antd';
@ -129,13 +130,14 @@ const MessageItem = ({
{/* <b>{isAssistant ? '' : nickname}</b> */}
</Space>
<div
className={
className={cn(
isAssistant
? theme === 'dark'
? styles.messageTextDark
: styles.messageText
: styles.messageUserText
}
: styles.messageUserText,
{ '!bg-bg-card': !isAssistant },
)}
>
<MarkdownContent
loading={loading}

View File

@ -0,0 +1,72 @@
import { DatasetMetadata } from '@/constants/chat';
import { useTranslate } from '@/hooks/common-hooks';
import { useFormContext, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { SelectWithSearch } from '../originui/select-with-search';
import { RAGFlowFormItem } from '../ragflow-form';
import { MetadataFilterConditions } from './metadata-filter-conditions';
type MetadataFilterProps = {
prefix?: string;
};
export const MetadataFilterSchema = {
meta_data_filter: z
.object({
method: z.string().optional(),
manual: z
.array(
z.object({
key: z.string(),
op: z.string(),
value: z.string(),
}),
)
.optional(),
})
.optional(),
};
export function MetadataFilter({ prefix = '' }: MetadataFilterProps) {
const { t } = useTranslate('chat');
const form = useFormContext();
const methodName = prefix + 'meta_data_filter.method';
const kbIds: string[] = useWatch({
control: form.control,
name: prefix + 'kb_ids',
});
const metadata = useWatch({
control: form.control,
name: methodName,
});
const hasKnowledge = Array.isArray(kbIds) && kbIds.length > 0;
const MetadataOptions = Object.values(DatasetMetadata).map((x) => {
return {
value: x,
label: t(`meta.${x}`),
};
});
return (
<>
{hasKnowledge && (
<RAGFlowFormItem
label={t('metadata')}
name={methodName}
tooltip={t('metadataTip')}
>
<SelectWithSearch options={MetadataOptions} />
</RAGFlowFormItem>
)}
{hasKnowledge && metadata === DatasetMetadata.Manual && (
<MetadataFilterConditions
kbIds={kbIds}
prefix={prefix}
></MetadataFilterConditions>
)}
</>
);
}

View File

@ -0,0 +1,135 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request';
import { SwitchOperatorOptions } from '@/pages/agent/constant';
import { useBuildSwitchOperatorOptions } from '@/pages/agent/form/switch-form';
import { Plus, X } from 'lucide-react';
import { useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
export function MetadataFilterConditions({
kbIds,
prefix = '',
}: {
kbIds: string[];
prefix?: string;
}) {
const { t } = useTranslation();
const form = useFormContext();
const name = prefix + 'meta_data_filter.manual';
const metadata = useFetchKnowledgeMetadata(kbIds);
const switchOperatorOptions = useBuildSwitchOperatorOptions();
const { fields, remove, append } = useFieldArray({
name,
control: form.control,
});
const add = useCallback(
(key: string) => () => {
append({
key,
value: '',
op: SwitchOperatorOptions[0].value,
});
},
[append],
);
return (
<section className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<FormLabel>{t('chat.conditions')}</FormLabel>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant={'ghost'} type="button">
<Plus />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{Object.keys(metadata.data).map((key, idx) => {
return (
<DropdownMenuItem key={idx} onClick={add(key)}>
{key}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-5">
{fields.map((field, index) => {
const typeField = `${name}.${index}.key`;
return (
<div key={field.id} className="flex w-full items-center gap-2">
<FormField
control={form.control}
name={typeField}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<Input
{...field}
placeholder={t('common.pleaseInput')}
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
name={`${name}.${index}.op`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<SelectWithSearch
{...field}
options={switchOperatorOptions}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
name={`${name}.${index}.value`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<Input placeholder={t('common.pleaseInput')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X className="text-text-sub-title-invert " />
</Button>
</div>
);
})}
</div>
</section>
);
}

View File

@ -59,10 +59,10 @@ export function UseGraphRagFormField() {
name="parser_config.graphrag.use_graphrag"
render={({ field }) => (
<FormItem defaultChecked={false} className=" items-center space-y-0 ">
<div className="flex items-center">
<div className="flex items-center gap-1">
<FormLabel
tooltip={t('useGraphRagTip')}
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
className="text-sm text-muted-foreground whitespace-break-spaces w-1/4"
>
{t('useGraphRag')}
</FormLabel>

View File

@ -86,10 +86,10 @@ const RaptorFormFields = () => {
defaultChecked={false}
className="items-center space-y-0 "
>
<div className="flex items-center">
<div className="flex items-center gap-1">
<FormLabel
tooltip={t('useRaptorTip')}
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
className="text-sm text-muted-foreground w-1/4 whitespace-break-spaces"
>
{t('useRaptor')}
</FormLabel>

View File

@ -3,6 +3,13 @@ import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { forwardRef, memo, useEffect, useRef, useState } from 'react';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
const PREDEFINED_COLORS = [
{ from: '#4F6DEE', to: '#67BDF9' },
{ from: '#38A04D', to: '#93DCA2' },
{ from: '#C35F2B', to: '#EDB395' },
{ from: '#633897', to: '#CBA1FF' },
];
const getStringHash = (str: string): number => {
const normalized = str.trim().toLowerCase();
let hash = 104729;
@ -17,16 +24,12 @@ const getStringHash = (str: string): number => {
return Math.abs(hash);
};
// Generate a hash function with a fixed color
const getColorForName = (name: string): { from: string; to: string } => {
const hash = getStringHash(name);
const hue = hash % 360;
return {
to: `hsl(${hue}, 70%, 80%)`,
from: `hsl(${hue}, 60%, 30%)`,
};
const index = hash % PREDEFINED_COLORS.length;
return PREDEFINED_COLORS[index];
};
export const RAGFlowAvatar = memo(
forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
@ -43,13 +46,13 @@ export const RAGFlowAvatar = memo(
if (parts.length === 1) {
return parts[0][0].toUpperCase();
}
return parts[0][0].toUpperCase() + parts[1][0].toUpperCase();
return parts[0][0].toUpperCase();
};
const initials = getInitials(name);
const { from, to } = name
? getColorForName(name)
: { from: 'hsl(0, 0%, 80%)', to: 'hsl(0, 0%, 30%)' };
: { from: 'hsl(0, 0%, 30%)', to: 'hsl(0, 0%, 80%)' };
const fallbackRef = useRef<HTMLElement>(null);
const [fontSize, setFontSize] = useState('0.875rem');
@ -98,7 +101,7 @@ export const RAGFlowAvatar = memo(
'bg-gradient-to-b',
`from-[${from}] to-[${to}]`,
'flex items-center justify-center',
'text-white font-bold',
'text-white ',
{ 'rounded-md': !isPerson },
)}
style={{

View File

@ -49,12 +49,12 @@ export function SliderInputFormField({
defaultValue={defaultValue || 0}
render={({ field }) => (
<FormItem
className={cn({ 'flex items-center space-y-0': isHorizontal })}
className={cn({ 'flex items-center gap-1 space-y-0': isHorizontal })}
>
<FormLabel
tooltip={tooltip}
className={cn({
'text-sm text-muted-foreground whitespace-nowrap w-1/4':
'text-sm text-muted-foreground whitespace-break-spaces w-1/4':
isHorizontal,
})}
>

View File

@ -32,3 +32,9 @@ export enum ChatSearchParams {
}
export const EmptyConversationId = 'empty';
export enum DatasetMetadata {
Disabled = 'disabled',
Automatic = 'automatic',
Manual = 'manual',
}

View File

@ -369,22 +369,28 @@ export const useScrollToBottom = (
return () => container.removeEventListener('scroll', handleScroll);
}, [containerRef, checkIfUserAtBottom]);
// Imperative scroll function
const scrollToBottom = useCallback(() => {
if (containerRef?.current) {
const container = containerRef.current;
container.scrollTo({
top: container.scrollHeight - container.clientHeight,
behavior: 'smooth',
});
}
}, [containerRef]);
useEffect(() => {
if (!messages) return;
if (!containerRef?.current) return;
requestAnimationFrame(() => {
setTimeout(() => {
if (isAtBottomRef.current) {
ref.current?.scrollIntoView({ behavior: 'smooth' });
scrollToBottom();
}
}, 30);
}, 100);
});
}, [messages, containerRef]);
// Imperative scroll function
const scrollToBottom = useCallback(() => {
ref.current?.scrollIntoView({ behavior: 'smooth' });
}, []);
}, [messages, containerRef, scrollToBottom]);
return { scrollRef: ref, isAtBottom, scrollToBottom };
};
@ -551,6 +557,15 @@ export const useSelectDerivedMessages = () => {
setDerivedMessages([]);
}, [setDerivedMessages]);
const removeAllMessagesExceptFirst = useCallback(() => {
setDerivedMessages((list) => {
if (list.length <= 1) {
return list;
}
return list.slice(0, 1);
});
}, [setDerivedMessages]);
return {
scrollRef,
messageContainerRef,
@ -565,6 +580,7 @@ export const useSelectDerivedMessages = () => {
removeMessagesAfterCurrentMessage,
removeAllMessages,
scrollToBottom,
removeAllMessagesExceptFirst,
};
};

View File

@ -24,13 +24,17 @@ export const useNavigatePage = () => {
);
const navigateToHome = useCallback(() => {
navigate(Routes.Home);
navigate(Routes.Root);
}, [navigate]);
const navigateToProfile = useCallback(() => {
navigate(Routes.ProfileSetting);
}, [navigate]);
const navigateToOldProfile = useCallback(() => {
navigate(Routes.UserSetting);
}, [navigate]);
const navigateToChatList = useCallback(() => {
navigate(Routes.Chats);
}, [navigate]);
@ -139,5 +143,6 @@ export const useNavigatePage = () => {
navigateToSearch,
navigateToFiles,
navigateToAgentList,
navigateToOldProfile,
};
};

View File

@ -1,6 +1,10 @@
import message from '@/components/ui/message';
import { ChatSearchParams } from '@/constants/chat';
import { IConversation, IDialog } from '@/interfaces/database/chat';
import {
IConversation,
IDialog,
IExternalChatInfo,
} from '@/interfaces/database/chat';
import { IAskRequestBody } from '@/interfaces/request/chat';
import { IClientConversation } from '@/pages/next-chats/chat/interface';
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
@ -32,6 +36,7 @@ export const enum ChatApiAction {
FetchMindMap = 'fetchMindMap',
FetchRelatedQuestions = 'fetchRelatedQuestions',
UploadAndParse = 'upload_and_parse',
FetchExternalChatInfo = 'fetchExternalChatInfo',
}
export const useGetChatSearchParams = () => {
@ -418,6 +423,29 @@ export function useUploadAndParseFile() {
return { data, loading, uploadAndParseFile: mutateAsync };
}
export const useFetchExternalChatInfo = () => {
const { sharedId: id } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IExternalChatInfo>({
queryKey: [ChatApiAction.FetchExternalChatInfo, id],
gcTime: 0,
initialData: {} as IExternalChatInfo,
enabled: !!id,
refetchOnWindowFocus: false,
queryFn: async () => {
const { data } = await chatService.fetchExternalChatInfo(id!);
return data?.data;
},
});
return { data, loading, refetch };
};
//#endregion
//#region search page

View File

@ -172,3 +172,9 @@ export interface IStats {
round: [string, number][];
thumb_up: [string, number][];
}
export interface IExternalChatInfo {
avatar?: string;
title: string;
prologue?: string;
}

View File

@ -7,4 +7,5 @@ export interface IFeedbackRequestBody {
export interface IAskRequestBody {
question: string;
kb_ids: string[];
search_id?: string;
}

View File

@ -1,6 +1,5 @@
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { useTheme } from '@/components/theme-provider';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@ -41,7 +40,7 @@ export function Header() {
const { t } = useTranslation();
const { pathname } = useLocation();
const navigate = useNavigateWithFromState();
const { navigateToProfile } = useNavigatePage();
const { navigateToOldProfile } = useNavigatePage();
const changeLanguage = useChangeLanguage();
const { setTheme, theme } = useTheme();
@ -75,8 +74,8 @@ export function Header() {
const tagsData = useMemo(
() => [
{ path: Routes.Home, name: t('header.home'), icon: House },
{ path: Routes.Datasets, name: t('header.knowledgeBase'), icon: Library },
{ path: Routes.Root, name: t('header.Root'), icon: House },
{ path: Routes.Datasets, name: t('header.dataset'), icon: Library },
{ path: Routes.Chats, name: t('header.chat'), icon: MessageSquareText },
{ path: Routes.Searches, name: t('header.search'), icon: Search },
{ path: Routes.Agents, name: t('header.flow'), icon: Cpu },
@ -91,7 +90,7 @@ export function Header() {
return {
label:
tag.path === Routes.Home ? (
tag.path === Routes.Root ? (
<HeaderIcon className="size-6"></HeaderIcon>
) : (
<span>{tag.name}</span>
@ -101,18 +100,18 @@ export function Header() {
});
}, [tagsData]);
const currentPath = useMemo(() => {
return (
tagsData.find((x) => pathname.startsWith(x.path))?.path || Routes.Home
);
}, [pathname, tagsData]);
// const currentPath = useMemo(() => {
// return (
// tagsData.find((x) => pathname.startsWith(x.path))?.path || Routes.Root
// );
// }, [pathname, tagsData]);
const handleChange = (path: SegmentedValue) => {
navigate(path as Routes);
};
const handleLogoClick = useCallback(() => {
navigate(Routes.Home);
navigate(Routes.Root);
}, [navigate]);
return (
@ -124,14 +123,19 @@ export function Header() {
className="size-10 mr-[12]"
onClick={handleLogoClick}
/>
<div className="flex items-center gap-1.5 text-text-secondary">
<Github className="size-3.5" />
<span className=" text-base">21.5k stars</span>
</div>
<a
className="flex items-center gap-1.5 text-text-secondary"
target="_blank"
href="https://github.com/infiniflow/ragflow"
rel="noreferrer"
>
<Github className="size-4" />
{/* <span className=" text-base">21.5k stars</span> */}
</a>
</div>
<Segmented
options={options}
value={currentPath}
value={pathname}
onChange={handleChange}
></Segmented>
<div className="flex items-center gap-5 text-text-badge">
@ -161,11 +165,12 @@ export function Header() {
name={nickname}
avatar={avatar}
className="size-8 cursor-pointer"
onClick={navigateToProfile}
onClick={navigateToOldProfile}
></RAGFlowAvatar>
<Badge className="h-5 w-8 absolute font-normal p-0 justify-center -right-8 -top-2 text-bg-base bg-gradient-to-l from-[#42D7E7] to-[#478AF5]">
{/* Temporarily hidden */}
{/* <Badge className="h-5 w-8 absolute font-normal p-0 justify-center -right-8 -top-2 text-bg-base bg-gradient-to-l from-[#42D7E7] to-[#478AF5]">
Pro
</Badge>
</Badge> */}
</div>
</div>
</section>

View File

@ -5,6 +5,7 @@ export default {
deleteModalTitle: 'Are you sure to delete this item?',
ok: 'Yes',
cancel: 'No',
no: 'No',
total: 'Total',
rename: 'Rename',
name: 'Name',
@ -80,6 +81,7 @@ export default {
flow: 'Agent',
search: 'Search',
welcome: 'Welcome to',
dataset: 'Dataset',
},
knowledgeList: {
welcome: 'Welcome back',
@ -575,6 +577,8 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
automatic: 'Automatic',
manual: 'Manual',
},
cancel: 'Cancel',
chatSetting: 'Chat setting',
},
setting: {
profile: 'Profile',
@ -1419,6 +1423,26 @@ This delimiter is used to split the input text into several text pieces echo of
search: {
createSearch: 'Create Search',
searchGreeting: 'How can I help you today ',
profile: 'Hide Profile',
locale: 'Locale',
embedCode: 'Embed code',
id: 'ID',
copySuccess: 'Copy Success',
welcomeBack: 'Welcome back',
searchSettings: 'Search Settings',
name: 'Name',
avatar: 'Avatar',
description: 'Description',
datasets: 'Datasets',
rerankModel: 'Rerank Model',
AISummary: 'AI Summary',
enableWebSearch: 'Enable Web Search',
enableRelatedSearch: 'Enable Related Search',
showQueryMindmap: 'Show Query Mindmap',
embedApp: 'Embed App',
relatedSearch: 'Related Search',
okText: 'Save',
cancelText: 'Cancel',
},
},
};

View File

@ -1192,6 +1192,12 @@ export default {
search: {
createSearch: '新建查詢',
searchGreeting: '今天我能為你做些什麽?',
profile: '隱藏個人資料',
locale: '語言',
embedCode: '嵌入代碼',
id: 'ID',
copySuccess: '複製成功',
welcomeBack: '歡迎回來',
},
},
};

View File

@ -73,6 +73,7 @@ export default {
flow: 'Agent',
search: '搜索',
welcome: '欢迎来到',
dataset: '数据集',
},
knowledgeList: {
welcome: '欢迎回来',
@ -569,6 +570,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
automatic: '自动',
manual: '手动',
},
cancel: '取消',
chatSetting: '聊天设置',
},
setting: {
profile: '概要',
@ -1323,6 +1326,26 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
search: {
createSearch: '新建查询',
searchGreeting: '今天我能为你做些什么?',
profile: '隐藏个人资料',
locale: '语言',
embedCode: '嵌入代码',
id: 'ID',
copySuccess: '复制成功',
welcomeBack: '欢迎回来',
searchSettings: '搜索设置',
name: '姓名',
avatar: '头像',
description: '描述',
datasets: '数据集',
rerankModel: 'rerank 模型',
AISummary: 'AI 总结',
enableWebSearch: '启用网页搜索',
enableRelatedSearch: '启用相关搜索',
showQueryMindmap: '显示查询思维导图',
embedApp: '嵌入网站',
relatedSearch: '相关搜索',
okText: '保存',
cancelText: '返回',
},
},
};

View File

@ -242,7 +242,7 @@ export function InnerNextStepDropdown({
}}
onClick={(e) => e.stopPropagation()}
>
<div className="w-[300px] font-semibold bg-white border border-border rounded-md shadow-lg">
<div className="w-[300px] font-semibold bg-bg-base border border-border rounded-md shadow-lg">
<div className="px-3 py-2 border-b border-border">
<div className="text-sm font-medium">Next Step</div>
</div>

View File

@ -128,7 +128,7 @@ function AgentForm({ node }: INextOperatorForm) {
<FormWrapper>
<FormContainer>
{isSubAgent && <DescriptionField></DescriptionField>}
<LargeModelFormField></LargeModelFormField>
<LargeModelFormField showSpeech2TextModel></LargeModelFormField>
{findLlmByUuid(llmId)?.model_type === LlmModelType.Image2text && (
<QueryVariable
name="visual_files_var"

View File

@ -3,7 +3,6 @@ import { useCallback } from 'react';
import { z } from 'zod';
export const ExeSQLFormSchema = {
sql: z.string(),
db_type: z.string().min(1),
database: z.string().min(1),
username: z.string().min(1),
@ -14,7 +13,7 @@ export const ExeSQLFormSchema = {
};
export const FormSchema = z.object({
query: z.string().optional(),
sql: z.string().optional(),
...ExeSQLFormSchema,
});

View File

@ -25,11 +25,13 @@ const ExeSQLForm = () => {
defaultValues: defaultValues as FormType,
});
const onError = (error: any) => console.log(error);
useWatchFormChange(form);
return (
<Form {...form}>
<FormWrapper onSubmit={form.handleSubmit(onSubmit)}>
<FormWrapper onSubmit={form.handleSubmit(onSubmit, onError)}>
<ExeSQLFormWidgets loading={loading}></ExeSQLFormWidgets>
</FormWrapper>
</Form>

View File

@ -54,6 +54,8 @@ export function useAgentToolInitialValues() {
return pick(initialValues, 'top_n');
case Operator.WenCai:
return pick(initialValues, 'top_n', 'query_type');
case Operator.Code:
return {};
default:
return initialValues;

View File

@ -0,0 +1,65 @@
import { SharedFrom } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { IEventList } from '@/hooks/use-send-message';
import { useSendAgentMessage } from '@/pages/agent/chat/use-send-agent-message';
import trim from 'lodash/trim';
import { useCallback, useState } from 'react';
import { useSearchParams } from 'umi';
export const useSendButtonDisabled = (value: string) => {
return trim(value) === '';
};
export const useGetSharedChatSearchParams = () => {
const [searchParams] = useSearchParams();
const data_prefix = 'data_';
const data = Object.fromEntries(
searchParams
.entries()
.filter(([key]) => key.startsWith(data_prefix))
.map(([key, value]) => [key.replace(data_prefix, ''), value]),
);
return {
from: searchParams.get('from') as SharedFrom,
sharedId: searchParams.get('shared_id'),
locale: searchParams.get('locale'),
data: data,
visibleAvatar: searchParams.get('visible_avatar')
? searchParams.get('visible_avatar') !== '1'
: true,
};
};
export const useSendNextSharedMessage = (
addEventList: (data: IEventList, messageId: string) => void,
) => {
const { from, sharedId: conversationId } = useGetSharedChatSearchParams();
const url = `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`;
const [params, setParams] = useState<any[]>([]);
const {
visible: parameterDialogVisible,
hideModal: hideParameterDialog,
showModal: showParameterDialog,
} = useSetModalState();
const ret = useSendAgentMessage(url, addEventList, params);
const ok = useCallback(
(params: any[]) => {
setParams(params);
hideParameterDialog();
},
[hideParameterDialog],
);
return {
...ret,
hasError: false,
parameterDialogVisible,
hideParameterDialog,
showParameterDialog,
ok,
};
};

View File

@ -1,18 +1,13 @@
import { useFetchTokenListBeforeOtherStep } from '@/components/embed-dialog/use-show-embed-dialog';
import { SharedFrom } from '@/constants/chat';
import {
useSetModalState,
useShowDeleteConfirm,
useTranslate,
} from '@/hooks/common-hooks';
import { useShowDeleteConfirm } from '@/hooks/common-hooks';
import {
useCreateSystemToken,
useFetchManualSystemTokenList,
useFetchSystemTokenList,
useRemoveSystemToken,
} from '@/hooks/user-setting-hooks';
import { IStats } from '@/interfaces/database/chat';
import { useQueryClient } from '@tanstack/react-query';
import { message } from 'antd';
import { useCallback } from 'react';
export const useOperateApiKey = (idKey: string, dialogId?: string) => {
@ -62,94 +57,11 @@ export const useSelectChartStatsList = (): ChartStatsType => {
}, {} as ChartStatsType);
};
export const useShowTokenEmptyError = () => {
const { t } = useTranslate('chat');
const showTokenEmptyError = useCallback(() => {
message.error(t('tokenError'));
}, [t]);
return { showTokenEmptyError };
};
export const useShowBetaEmptyError = () => {
const { t } = useTranslate('chat');
const showBetaEmptyError = useCallback(() => {
message.error(t('betaError'));
}, [t]);
return { showBetaEmptyError };
};
const getUrlWithToken = (token: string, from: string = 'chat') => {
const { protocol, host } = window.location;
return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`;
};
export const useFetchTokenListBeforeOtherStep = () => {
const { showTokenEmptyError } = useShowTokenEmptyError();
const { showBetaEmptyError } = useShowBetaEmptyError();
const { data: tokenList, fetchSystemTokenList } =
useFetchManualSystemTokenList();
let token = '',
beta = '';
if (Array.isArray(tokenList) && tokenList.length > 0) {
token = tokenList[0].token;
beta = tokenList[0].beta;
}
token =
Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : '';
const handleOperate = useCallback(async () => {
const ret = await fetchSystemTokenList();
const list = ret;
if (Array.isArray(list) && list.length > 0) {
if (!list[0].beta) {
showBetaEmptyError();
return false;
}
return list[0]?.token;
} else {
showTokenEmptyError();
return false;
}
}, [fetchSystemTokenList, showBetaEmptyError, showTokenEmptyError]);
return {
token,
beta,
handleOperate,
};
};
export const useShowEmbedModal = () => {
const {
visible: embedVisible,
hideModal: hideEmbedModal,
showModal: showEmbedModal,
} = useSetModalState();
const { handleOperate, token, beta } = useFetchTokenListBeforeOtherStep();
const handleShowEmbedModal = useCallback(async () => {
const succeed = await handleOperate();
if (succeed) {
showEmbedModal();
}
}, [handleOperate, showEmbedModal]);
return {
showEmbedModal: handleShowEmbedModal,
hideEmbedModal,
embedVisible,
embedToken: token,
beta,
};
};
export const usePreviewChat = (idKey: string) => {
const { handleOperate } = useFetchTokenListBeforeOtherStep();

View File

@ -23,8 +23,13 @@ export const useShowFormDrawer = () => {
const handleShow = useCallback(
(e: React.MouseEvent<Element>, nodeId: string) => {
const tool = get(e.target, 'dataset.tool');
// TODO: Operator type judgment should be used
if (nodeId.startsWith(Operator.Tool) && !tool) {
return;
}
setClickedNodeId(nodeId);
setClickedToolId(get(e.target, 'dataset.tool'));
setClickedToolId(tool);
showFormDrawer();
},
[setClickedNodeId, setClickedToolId, showFormDrawer],

View File

@ -1,3 +1,5 @@
import EmbedDialog from '@/components/embed-dialog';
import { useShowEmbedModal } from '@/components/embed-dialog/use-show-embed-dialog';
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
@ -35,7 +37,6 @@ import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import AgentCanvas from './canvas';
import { DropdownProvider } from './canvas/context';
import EmbedDialog from './embed-dialog';
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data';
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
@ -44,7 +45,6 @@ import {
useSaveGraphBeforeOpeningDebugDrawer,
useWatchAgentChange,
} from './hooks/use-save-graph';
import { useShowEmbedModal } from './hooks/use-show-dialog';
import { SettingDialog } from './setting-dialog';
import { UploadAgentDialog } from './upload-agent-dialog';
import { useAgentHistoryManager } from './use-agent-history-manager';
@ -63,7 +63,7 @@ function AgentDropdownMenuItem({
export default function Agent() {
const { id } = useParams();
const { navigateToAgentList } = useNavigatePage();
const { navigateToAgents } = useNavigatePage();
const {
visible: chatDrawerVisible,
hideModal: hideChatDrawer,
@ -113,7 +113,7 @@ export default function Agent() {
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToAgentList}>
<BreadcrumbLink onClick={navigateToAgents}>
Agent
</BreadcrumbLink>
</BreadcrumbItem>

View File

@ -158,8 +158,9 @@ const ToolTimelineItem = ({
</span>
)}
<span className="text-text-secondary text-xs">
{/* 0:00
{x.data.elapsed_time?.toString().slice(0, 6)} */}
{/* 0:00*/}
{tool.elapsed_time?.toString().slice(0, 6) || ''}
{tool.elapsed_time ? 's' : ''}
</span>
<span
className={cn(

View File

@ -153,6 +153,22 @@ export const WorkFlowTimeline = ({
}, []);
}, [currentEventListWithoutMessage, sendLoading]);
const getElapsedTime = (nodeId: string) => {
if (nodeId === 'begin') {
return '';
}
const data = currentEventListWithoutMessage?.find((x) => {
return (
x.data.component_id === nodeId &&
x.event === MessageEventType.NodeFinished
);
});
if (!data || data?.data.elapsed_time < 0.000001) {
return '';
}
return data?.data.elapsed_time || '';
};
const hasTrace = useCallback(
(componentId: string) => {
if (Array.isArray(traceData)) {
@ -272,7 +288,10 @@ export const WorkFlowTimeline = ({
nodeLabel)}
</span>
<span className="text-text-secondary text-xs">
{x.data.elapsed_time?.toString().slice(0, 6)}
{getElapsedTime(x.data.component_id)
.toString()
.slice(0, 6)}
{getElapsedTime(x.data.component_id) ? 's' : ''}
</span>
<span
className={cn(

View File

@ -0,0 +1,233 @@
import { EmbedContainer } from '@/components/embed-container';
import { FileUploadProps } from '@/components/file-upload';
import { NextMessageInput } from '@/components/message-input/next';
import MessageItem from '@/components/next-message-item';
import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { MessageType } from '@/constants/chat';
import {
useFetchExternalAgentInputs,
useUploadCanvasFileWithProgress,
} from '@/hooks/use-agent-request';
import { cn } from '@/lib/utils';
import i18n from '@/locales/config';
import DebugContent from '@/pages/agent/debug-content';
import { useCacheChatLog } from '@/pages/agent/hooks/use-cache-chat-log';
import { useAwaitCompentData } from '@/pages/agent/hooks/use-chat-logic';
import { IInputs } from '@/pages/agent/interface';
import { useSendButtonDisabled } from '@/pages/chat/hooks';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { isEmpty } from 'lodash';
import React, { forwardRef, useCallback, useState } from 'react';
import {
useGetSharedChatSearchParams,
useSendNextSharedMessage,
} from '../hooks/use-send-shared-message';
import { ParameterDialog } from './parameter-dialog';
const ChatContainer = () => {
const {
sharedId: conversationId,
locale,
visibleAvatar,
} = useGetSharedChatSearchParams();
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
const { uploadCanvasFile, loading } =
useUploadCanvasFileWithProgress(conversationId);
const {
addEventList,
setCurrentMessageId,
currentEventListWithoutMessageById,
clearEventList,
} = useCacheChatLog();
const {
handlePressEnter,
handleInputChange,
value,
sendLoading,
scrollRef,
messageContainerRef,
derivedMessages,
hasError,
stopOutputMessage,
findReferenceByMessageId,
appendUploadResponseList,
parameterDialogVisible,
showParameterDialog,
sendFormMessage,
addNewestOneAnswer,
ok,
resetSession,
} = useSendNextSharedMessage(addEventList);
const { buildInputList, handleOk, isWaitting } = useAwaitCompentData({
derivedMessages,
sendFormMessage,
canvasId: conversationId as string,
});
const sendDisabled = useSendButtonDisabled(value);
const { data: inputsData } = useFetchExternalAgentInputs();
const [agentInfo, setAgentInfo] = useState<IInputs>({
avatar: '',
title: '',
inputs: {},
prologue: '',
});
const handleUploadFile: NonNullable<FileUploadProps['onUpload']> =
useCallback(
async (files, options) => {
const ret = await uploadCanvasFile({ files, options });
appendUploadResponseList(ret.data, files);
},
[appendUploadResponseList, uploadCanvasFile],
);
React.useEffect(() => {
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
}
}, [locale, visibleAvatar]);
React.useEffect(() => {
const { avatar, title, inputs } = inputsData;
setAgentInfo({
avatar,
title,
inputs: inputs,
prologue: '',
});
}, [inputsData, setAgentInfo]);
React.useEffect(() => {
if (inputsData.prologue) {
addNewestOneAnswer({
answer: inputsData.prologue,
});
}
}, [inputsData.prologue, addNewestOneAnswer]);
React.useEffect(() => {
if (inputsData && inputsData.inputs && !isEmpty(inputsData.inputs)) {
showParameterDialog();
}
}, [inputsData, showParameterDialog]);
const handleInputsModalOk = (params: any[]) => {
ok(params);
};
const handleReset = () => {
resetSession();
clearEventList();
};
if (!conversationId) {
return <div>empty</div>;
}
return (
<>
<EmbedContainer
title={agentInfo.title}
avatar={agentInfo.avatar}
handleReset={handleReset}
>
<div className="flex flex-1 flex-col p-2.5 h-[90vh] m-3">
<div
className={cn(
'flex flex-1 flex-col overflow-auto scrollbar-auto m-auto w-5/6',
)}
ref={messageContainerRef}
>
<div>
{derivedMessages?.map((message, i) => {
return (
<MessageItem
visibleAvatar={visibleAvatar}
conversationId={conversationId}
currentEventListWithoutMessageById={
currentEventListWithoutMessageById
}
setCurrentMessageId={setCurrentMessageId}
key={buildMessageUuidWithRole(message)}
item={message}
nickname="You"
reference={findReferenceByMessageId(message.id)}
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages?.length - 1 === i
}
isShare={true}
avatarDialog={agentInfo.avatar}
agentName={agentInfo.title}
index={i}
clickDocumentButton={clickDocumentButton}
showLikeButton={false}
showLoudspeaker={false}
showLog={false}
sendLoading={sendLoading}
>
{message.role === MessageType.Assistant &&
derivedMessages.length - 1 === i && (
<DebugContent
parameters={buildInputList(message)}
message={message}
ok={handleOk(message)}
isNext={false}
btnText={'Submit'}
></DebugContent>
)}
{message.role === MessageType.Assistant &&
derivedMessages.length - 1 !== i && (
<div>
<div>{message?.data?.tips}</div>
<div>
{buildInputList(message)?.map((item) => item.value)}
</div>
</div>
)}
</MessageItem>
);
})}
</div>
<div ref={scrollRef} />
</div>
<div className="flex w-full justify-center mb-8">
<div className="w-5/6">
<NextMessageInput
isShared
value={value}
disabled={hasError || isWaitting}
sendDisabled={sendDisabled || isWaitting}
conversationId={conversationId}
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
sendLoading={sendLoading}
stopOutputMessage={stopOutputMessage}
onUpload={handleUploadFile}
isUploading={loading || isWaitting}
></NextMessageInput>
</div>
</div>
</div>
</EmbedContainer>
{visible && (
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
)}
{parameterDialogVisible && (
<ParameterDialog
// hideModal={hideParameterDialog}
ok={handleInputsModalOk}
data={agentInfo.inputs}
></ParameterDialog>
)}
</>
);
};
export default forwardRef(ChatContainer);

Some files were not shown because too many files have changed in this diff Show More