Merge branch 'feat/plugins' of https://github.com/langgenius/dify into feat/plugins

This commit is contained in:
twwu
2024-11-08 11:33:42 +08:00
335 changed files with 8036 additions and 2041 deletions

View File

@ -9,8 +9,8 @@ const DatasetFooter = () => {
<footer className='px-12 py-6 grow-0 shrink-0'>
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('dataset.didYouKnow')}</h3>
<p className='mt-1 text-sm font-normal leading-tight text-gray-700'>
{t('dataset.intro1')}<a className='inline-flex items-center gap-1 link' target='_blank' rel='noopener noreferrer' href='/'>{t('dataset.intro2')}</a>{t('dataset.intro3')}<br />
{t('dataset.intro4')}<a className='inline-flex items-center gap-1 link' target='_blank' rel='noopener noreferrer' href='/'>{t('dataset.intro5')}</a>{t('dataset.intro6')}
{t('dataset.intro1')}<span className='inline-flex items-center gap-1 text-blue-600'>{t('dataset.intro2')}</span>{t('dataset.intro3')}<br />
{t('dataset.intro4')}<span className='inline-flex items-center gap-1 text-blue-600'>{t('dataset.intro5')}</span>{t('dataset.intro6')}
</p>
</footer>
)

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import { type FC, useEffect } from 'react'
import { useContext } from 'use-context-selector'
import TemplateEn from './template/template.en.mdx'
import TemplateZh from './template/template.zh.mdx'
@ -14,6 +14,13 @@ const Doc: FC<DocProps> = ({
apiBaseUrl,
}) => {
const { locale } = useContext(I18n)
useEffect(() => {
const hash = location.hash
if (hash)
document.querySelector(hash)?.scrollIntoView()
}, [])
return (
<article className='mx-1 px-4 sm:mx-12 pt-16 bg-white rounded-t-xl prose prose-xl'>
{

View File

@ -20,17 +20,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</CodeGroup>
</div>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/document/create_by_text'
url='/datasets/{dataset_id}/document/create-by-text'
method='POST'
title='Create a document from text'
name='#create_by_text'
title='Create a Document from Text'
name='#create-by-text'
/>
<Row>
<Col>
This api is based on an existing Knowledge and creates a new document through text based on this Knowledge.
This API is based on an existing knowledge and creates a new document through text based on this knowledge.
### Params
<Properties>
@ -50,7 +50,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Property name='indexing_technique' type='string' key='indexing_technique'>
Index mode
- <code>high_quality</code> High quality: embedding using embedding model, built as vector database index
- <code>economy</code> Economy: Build using inverted index of Keyword Table Index
- <code>economy</code> Economy: Build using inverted index of keyword table index
</Property>
<Property name='process_rule' type='object' key='process_rule'>
Processing rules
@ -62,7 +62,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- <code>remove_extra_spaces</code> Replace consecutive spaces, newlines, tabs
- <code>remove_urls_emails</code> Delete URL, email address
- <code>enabled</code> (bool) Whether to select this rule or not. If no document ID is passed in, it represents the default value.
- <code>segmentation</code> (object) segmentation rules
- <code>segmentation</code> (object) Segmentation rules
- <code>separator</code> Custom segment identifier, currently only allows one delimiter to be set. Default is \n
- <code>max_tokens</code> Maximum length (token) defaults to 1000
</Property>
@ -72,11 +72,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/document/create_by_text"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "text","text": "text","indexing_technique": "high_quality","process_rule": {"mode": "automatic"}}'`}
label="/datasets/{dataset_id}/document/create-by-text"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create-by-text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "text","text": "text","indexing_technique": "high_quality","process_rule": {"mode": "automatic"}}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_text' \
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create-by-text' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -123,17 +123,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/document/create_by_file'
url='/datasets/{dataset_id}/document/create-by-file'
method='POST'
title='Create documents from files'
name='#create_by_file'
title='Create a Document from a File'
name='#create-by-file'
/>
<Row>
<Col>
This api is based on an existing Knowledge and creates a new document through a file based on this Knowledge.
This API is based on an existing knowledge and creates a new document through a file based on this knowledge.
### Params
<Properties>
@ -145,17 +145,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### Request Body
<Properties>
<Property name='data' type='multipart/form-data json string' key='data'>
- original_document_id Source document ID (optional)
- <code>original_document_id</code> Source document ID (optional)
- Used to re-upload the document or modify the document cleaning and segmentation configuration. The missing information is copied from the source document
- The source document cannot be an archived document
- When original_document_id is passed in, the update operation is performed on behalf of the document. process_rule is a fillable item. If not filled in, the segmentation method of the source document will be used by default
- When original_document_id is not passed in, the new operation is performed on behalf of the document, and process_rule is required
- indexing_technique Index mode
- <code>indexing_technique</code> Index mode
- <code>high_quality</code> High quality: embedding using embedding model, built as vector database index
- <code>economy</code> Economy: Build using inverted index of Keyword Table Index
- <code>economy</code> Economy: Build using inverted index of keyword table index
- process_rule Processing rules
- <code>process_rule</code> Processing rules
- <code>mode</code> (string) Cleaning, segmentation mode, automatic / custom
- <code>rules</code> (object) Custom rules (in automatic mode, this field is empty)
- <code>pre_processing_rules</code> (array[object]) Preprocessing rules
@ -164,7 +164,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- <code>remove_extra_spaces</code> Replace consecutive spaces, newlines, tabs
- <code>remove_urls_emails</code> Delete URL, email address
- <code>enabled</code> (bool) Whether to select this rule or not. If no document ID is passed in, it represents the default value.
- <code>segmentation</code> (object) segmentation rules
- <code>segmentation</code> (object) Segmentation rules
- <code>separator</code> Custom segment identifier, currently only allows one delimiter to be set. Default is \n
- <code>max_tokens</code> Maximum length (token) defaults to 1000
</Property>
@ -177,11 +177,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/document/create_by_file"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
label="/datasets/{dataset_id}/document/create-by-file"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create-by-file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_file' \
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create-by-file' \
--header 'Authorization: Bearer {api_key}' \
--form 'data="{\"name\":\"Dify\",\"indexing_technique\":\"high_quality\",\"process_rule\":{\"rules\":{\"pre_processing_rules\":[{\"id\":\"remove_extra_spaces\",\"enabled\":true},{\"id\":\"remove_urls_emails\",\"enabled\":true}],\"segmentation\":{\"separator\":\"###\",\"max_tokens\":500}},\"mode\":\"custom\"}}";type=text/plain' \
--form 'file=@"/path/to/file"'
@ -221,12 +221,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets'
method='POST'
title='Create an empty Knowledge'
title='Create an Empty Knowledge Base'
name='#create_empty_dataset'
/>
<Row>
@ -240,9 +240,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
Knowledge description (optional)
</Property>
<Property name='indexing_technique' type='string' key='indexing_technique'>
Index Technique (optional)
- <code>high_quality</code> high_quality
- <code>economy</code> economy
Index technique (optional)
- <code>high_quality</code> High quality
- <code>economy</code> Economy
</Property>
<Property name='permission' type='string' key='permission'>
Permission
@ -252,21 +252,21 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Property>
<Property name='provider' type='string' key='provider'>
Provider (optional, default: vendor)
- <code>vendor</code> vendor
- <code>external</code> external knowledge
- <code>vendor</code> Vendor
- <code>external</code> External knowledge
</Property>
<Property name='external_knowledge_api_id' type='str' key='external_knowledge_api_id'>
External Knowledge api id (optional)
External knowledge API ID (optional)
</Property>
<Property name='external_knowledge_id' type='str' key='external_knowledge_id'>
External Knowledge id (optional)
External knowledge ID (optional)
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
<CodeGroup
title="Request"
tag="POST"
label="/datasets"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "name", "permission": "only_me"}'`}
>
@ -306,12 +306,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets'
method='GET'
title='Knowledge list'
title='Get Knowledge Base List'
name='#dataset_list'
/>
<Row>
@ -327,9 +327,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
<CodeGroup
title="Request"
tag="POST"
label="/datasets"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets?page=1&limit=20' \\\n--header 'Authorization: Bearer {api_key}'`}
>
@ -369,12 +369,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}'
method='DELETE'
title='Delete knowledge'
title='Delete a Knowledge Base'
name='#delete_dataset'
/>
<Row>
@ -406,17 +406,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/update_by_text'
url='/datasets/{dataset_id}/documents/{document_id}/update-by-text'
method='POST'
title='Update document via text'
name='#update_by_text'
title='Update a Document with Text'
name='#update-by-text'
/>
<Row>
<Col>
This api is based on an existing Knowledge and updates the document through text based on this Knowledge.
This API is based on an existing knowledge and updates the document through text based on this knowledge.
### Params
<Properties>
@ -446,7 +446,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- <code>remove_extra_spaces</code> Replace consecutive spaces, newlines, tabs
- <code>remove_urls_emails</code> Delete URL, email address
- <code>enabled</code> (bool) Whether to select this rule or not. If no document ID is passed in, it represents the default value.
- <code>segmentation</code> (object) segmentation rules
- <code>segmentation</code> (object) Segmentation rules
- <code>separator</code> Custom segment identifier, currently only allows one delimiter to be set. Default is \n
- <code>max_tokens</code> Maximum length (token) defaults to 1000
</Property>
@ -456,11 +456,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/documents/{document_id}/update_by_text"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "name","text": "text"}'`}
label="/datasets/{dataset_id}/documents/{document_id}/update-by-text"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update-by-text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "name","text": "text"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_text' \
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update-by-text' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -503,17 +503,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/update_by_file'
url='/datasets/{dataset_id}/documents/{document_id}/update-by-file'
method='POST'
title='Update a document from a file'
name='#update_by_file'
title='Update a Document with a File'
name='#update-by-file'
/>
<Row>
<Col>
This api is based on an existing Knowledge, and updates documents through files based on this Knowledge
This API is based on an existing knowledge, and updates documents through files based on this knowledge
### Params
<Properties>
@ -543,7 +543,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- <code>remove_extra_spaces</code> Replace consecutive spaces, newlines, tabs
- <code>remove_urls_emails</code> Delete URL, email address
- <code>enabled</code> (bool) Whether to select this rule or not. If no document ID is passed in, it represents the default value.
- <code>segmentation</code> (object) segmentation rules
- <code>segmentation</code> (object) Segmentation rules
- <code>separator</code> Custom segment identifier, currently only allows one delimiter to be set. Default is \n
- <code>max_tokens</code> Maximum length (token) defaults to 1000
</Property>
@ -553,11 +553,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/documents/{document_id}/update_by_file"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"name":"Dify","indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
label="/datasets/{dataset_id}/documents/{document_id}/update-by-file"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update-by-file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"name":"Dify","indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_file' \
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update-by-file' \
--header 'Authorization: Bearer {api_key}' \
--form 'data="{\"name\":\"Dify\",\"indexing_technique\":\"high_quality\",\"process_rule\":{\"rules\":{\"pre_processing_rules\":[{\"id\":\"remove_extra_spaces\",\"enabled\":true},{\"id\":\"remove_urls_emails\",\"enabled\":true}],\"segmentation\":{\"separator\":\"###\",\"max_tokens\":500}},\"mode\":\"custom\"}}";type=text/plain' \
--form 'file=@"/path/to/file"'
@ -597,12 +597,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{batch}/indexing-status'
method='GET'
title='Get document embedding status (progress)'
title='Get Document Embedding Status (Progress)'
name='#indexing_status'
/>
<Row>
@ -652,12 +652,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}'
method='DELETE'
title='Delete document'
title='Delete a Document'
name='#delete_document'
/>
<Row>
@ -694,12 +694,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents'
method='GET'
title='Knowledge document list'
title='Get the Document List of a Knowledge Base'
name='#dataset_document_list'
/>
<Row>
@ -714,13 +714,13 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### Query
<Properties>
<Property name='keyword' type='string' key='keyword'>
Search keywords, currently only search document names(optional)
Search keywords, currently only search document names (optional)
</Property>
<Property name='page' type='string' key='page'>
Page number(optional)
Page number (optional)
</Property>
<Property name='limit' type='string' key='limit'>
Number of items returned, default 20, range 1-100(optional)
Number of items returned, default 20, range 1-100 (optional)
</Property>
</Properties>
</Col>
@ -769,12 +769,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments'
method='POST'
title='Add segment'
title='Add Chunks to a Document'
name='#create_new_segment'
/>
<Row>
@ -792,9 +792,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### Request Body
<Properties>
<Property name='segments' type='object list' key='segments'>
- <code>content</code> (text) Text content/question content, required
- <code>answer</code> (text) Answer content, if the mode of the Knowledge is qa mode, pass the value(optional)
- <code>keywords</code> (list) Keywords(optional)
- <code>content</code> (text) Text content / question content, required
- <code>answer</code> (text) Answer content, if the mode of the knowledge is Q&A mode, pass the value (optional)
- <code>keywords</code> (list) Keywords (optional)
</Property>
</Properties>
</Col>
@ -855,12 +855,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments'
method='GET'
title='get documents segments'
title='Get Chunks from a Document'
name='#get_segment'
/>
<Row>
@ -878,10 +878,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### Query
<Properties>
<Property name='keyword' type='string' key='keyword'>
keywordchoosable
Keyword (optional)
</Property>
<Property name='status' type='string' key='status'>
Search statuscompleted
Search status, completed
</Property>
</Properties>
</Col>
@ -933,12 +933,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
method='DELETE'
title='delete document segment'
title='Delete a Chunk in a Document'
name='#delete_segment'
/>
<Row>
@ -979,12 +979,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
method='POST'
title='update document segment'
title='Update a Chunk in a Document '
name='#update_segment'
/>
<Row>
@ -1005,10 +1005,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### Request Body
<Properties>
<Property name='segment' type='object' key='segment'>
- <code>content</code> (text) text content/question contentrequired
- <code>answer</code> (text) Answer content, not required, passed if the Knowledge is in qa mode
- <code>keywords</code> (list) keyword, not required
- <code>enabled</code> (bool) false/true, not required
- <code>content</code> (text) Text content / question content, required
- <code>answer</code> (text) Answer content, passed if the knowledge is in Q&A mode (optional)
- <code>keywords</code> (list) Keyword (optional)
- <code>enabled</code> (bool) False / true (optional)
</Property>
</Properties>
</Col>
@ -1067,41 +1067,41 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/hit_testing'
url='/datasets/{dataset_id}/retrieve'
method='POST'
title='Dataset hit testing'
name='#dataset_hit_testing'
title='Retrieve Chunks from a Knowledge Base'
name='#dataset_retrieval'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Dataset ID
Knowledge ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='query' type='string' key='query'>
retrieval keywordc
Query keyword
</Property>
<Property name='retrieval_model' type='object' key='retrieval_model'>
retrieval keyword(Optional, if not filled, it will be recalled according to the default method)
Retrieval model (optional, if not filled, it will be recalled according to the default method)
- <code>search_method</code> (text) Search method: One of the following four keywords is required
- <code>keyword_search</code> Keyword search
- <code>semantic_search</code> Semantic search
- <code>full_text_search</code> Full-text search
- <code>hybrid_search</code> Hybrid search
- <code>reranking_enable</code> (bool) Whether to enable reranking, optional, required if the search mode is semantic_search or hybrid_search
- <code>reranking_mode</code> (object) Rerank model configuration, optional, required if reranking is enabled
- <code>reranking_enable</code> (bool) Whether to enable reranking, required if the search mode is semantic_search or hybrid_search (optional)
- <code>reranking_mode</code> (object) Rerank model configuration, required if reranking is enabled
- <code>reranking_provider_name</code> (string) Rerank model provider
- <code>reranking_model_name</code> (string) Rerank model name
- <code>weights</code> (double) Semantic search weight setting in hybrid search mode
- <code>top_k</code> (integer) Number of results to return, optional
- <code>top_k</code> (integer) Number of results to return (optional)
- <code>score_threshold_enabled</code> (bool) Whether to enable score threshold
- <code>score_threshold</code> (double) Score threshold
</Property>
@ -1114,26 +1114,26 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/hit_testing"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/hit_testing' \\\n--header 'Authorization: Bearer {api_key}'\\\n--header 'Content-Type: application/json'\\\n--data-raw '{
"query": "test",
"retrieval_model": {
"search_method": "keyword_search",
"reranking_enable": false,
"reranking_mode": null,
"reranking_model": {
"reranking_provider_name": "",
"reranking_model_name": ""
},
"weights": null,
"top_k": 1,
"score_threshold_enabled": false,
"score_threshold": null
}
}'`}
label="/datasets/{dataset_id}/retrieve"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/retrieve' \\\n--header 'Authorization: Bearer {api_key}'\\\n--header 'Content-Type: application/json'\\\n--data-raw '{
"query": "test",
"retrieval_model": {
"search_method": "keyword_search",
"reranking_enable": false,
"reranking_mode": null,
"reranking_model": {
"reranking_provider_name": "",
"reranking_model_name": ""
},
"weights": null,
"top_k": 1,
"score_threshold_enabled": false,
"score_threshold": null
}
}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/hit_testing' \
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/retrieve' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -1212,7 +1212,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Row>
<Col>

View File

@ -20,13 +20,13 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</CodeGroup>
</div>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/document/create_by_text'
url='/datasets/{dataset_id}/document/create-by-text'
method='POST'
title='通过文本创建文档'
name='#create_by_text'
name='#create-by-text'
/>
<Row>
<Col>
@ -50,7 +50,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Property name='indexing_technique' type='string' key='indexing_technique'>
索引方式
- <code>high_quality</code> 高质量:使用 embedding 模型进行嵌入,构建为向量数据库索引
- <code>economy</code> 经济:使用 Keyword Table Index 的倒排索引进行构建
- <code>economy</code> 经济:使用 keyword table index 的倒排索引进行构建
</Property>
<Property name='process_rule' type='object' key='process_rule'>
处理规则
@ -64,7 +64,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- <code>enabled</code> (bool) 是否选中该规则,不传入文档 ID 时代表默认值
- <code>segmentation</code> (object) 分段规则
- <code>separator</code> 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
- <code>max_tokens</code> 最大长度 (token) 默认为 1000
- <code>max_tokens</code> 最大长度token默认为 1000
</Property>
</Properties>
</Col>
@ -72,11 +72,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/document/create_by_text"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "text","text": "text","indexing_technique": "high_quality","process_rule": {"mode": "automatic"}}'`}
label="/datasets/{dataset_id}/document/create-by-text"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create-by-text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "text","text": "text","indexing_technique": "high_quality","process_rule": {"mode": "automatic"}}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_text' \
curl --location --request --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create-by-text' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -123,13 +123,13 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/document/create_by_file'
url='/datasets/{dataset_id}/document/create-by-file'
method='POST'
title='通过文件创建文档 '
name='#create_by_file'
name='#create-by-file'
/>
<Row>
<Col>
@ -145,17 +145,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### Request Body
<Properties>
<Property name='data' type='multipart/form-data json string' key='data'>
- original_document_id 源文档 ID (选填)
- <code>original_document_id</code> 源文档 ID选填
- 用于重新上传文档或修改文档清洗、分段配置,缺失的信息从源文档复制
- 源文档不可为归档的文档
- 当传入 <code>original_document_id</code> 时,代表文档进行更新操作,<code>process_rule</code> 为可填项目,不填默认使用源文档的分段方式
- 未传入 <code>original_document_id</code> 时,代表文档进行新增操作,<code>process_rule</code> 为必填
- indexing_technique 索引方式
- <code>indexing_technique</code> 索引方式
- <code>high_quality</code> 高质量:使用 embedding 模型进行嵌入,构建为向量数据库索引
- <code>economy</code> 经济:使用 Keyword Table Index 的倒排索引进行构建
- <code>economy</code> 经济:使用 keyword table index 的倒排索引进行构建
- process_rule 处理规则
- <code>process_rule</code> 处理规则
- <code>mode</code> (string) 清洗、分段模式 automatic 自动 / custom 自定义
- <code>rules</code> (object) 自定义规则(自动模式下,该字段为空)
- <code>pre_processing_rules</code> (array[object]) 预处理规则
@ -166,7 +166,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- <code>enabled</code> (bool) 是否选中该规则,不传入文档 ID 时代表默认值
- <code>segmentation</code> (object) 分段规则
- <code>separator</code> 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
- <code>max_tokens</code> 最大长度 (token) 默认为 1000
- <code>max_tokens</code> 最大长度token默认为 1000
</Property>
<Property name='file' type='multipart/form-data' key='file'>
需要上传的文件。
@ -177,11 +177,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/document/create_by_file"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
label="/datasets/{dataset_id}/document/create-by-file"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create-by-file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create_by_file' \
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/document/create-by-file' \
--header 'Authorization: Bearer {api_key}' \
--form 'data="{\"name\":\"Dify\",\"indexing_technique\":\"high_quality\",\"process_rule\":{\"rules\":{\"pre_processing_rules\":[{\"id\":\"remove_extra_spaces\",\"enabled\":true},{\"id\":\"remove_urls_emails\",\"enabled\":true}],\"segmentation\":{\"separator\":\"###\",\"max_tokens\":500}},\"mode\":\"custom\"}}";type=text/plain' \
--form 'file=@"/path/to/file"'
@ -221,7 +221,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets'
@ -245,13 +245,13 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- <code>economy</code> 经济
</Property>
<Property name='permission' type='string' key='permission'>
权限选填默认only_me
权限(选填,默认 only_me
- <code>only_me</code> 仅自己
- <code>all_team_members</code> 所有团队成员
- <code>partial_members</code> 部分团队成员
</Property>
<Property name='provider' type='string' key='provider'>
provider(选填,默认 vendor
Provider选填默认 vendor
- <code>vendor</code> 上传文件
- <code>external</code> 外部知识库
</Property>
@ -264,9 +264,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
<CodeGroup
title="Request"
tag="POST"
label="/datasets"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "name", "permission": "only_me"}'`}
>
@ -306,7 +306,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets'
@ -369,7 +369,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}'
@ -406,13 +406,13 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/update_by_text'
url='/datasets/{dataset_id}/documents/{document_id}/update-by-text'
method='POST'
title='通过文本更新文档 '
name='#update_by_text'
name='#update-by-text'
/>
<Row>
<Col>
@ -431,7 +431,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### Request Body
<Properties>
<Property name='name' type='string' key='name'>
文档名称 (选填)
文档名称(选填)
</Property>
<Property name='text' type='string' key='text'>
文档内容(选填)
@ -448,7 +448,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- <code>enabled</code> (bool) 是否选中该规则,不传入文档 ID 时代表默认值
- <code>segmentation</code> (object) 分段规则
- <code>separator</code> 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
- <code>max_tokens</code> 最大长度 (token) 默认为 1000
- <code>max_tokens</code> 最大长度token默认为 1000
</Property>
</Properties>
</Col>
@ -456,11 +456,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/documents/{document_id}/update_by_text"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "name","text": "text"}'`}
label="/datasets/{dataset_id}/documents/{document_id}/update-by-text"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update-by-text' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "name","text": "text"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_text' \
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update-by-text' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -503,13 +503,13 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/update_by_file'
url='/datasets/{dataset_id}/documents/{document_id}/update-by-file'
method='POST'
title='通过文件更新文档 '
name='#update_by_file'
name='#update-by-file'
/>
<Row>
<Col>
@ -528,7 +528,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### Request Body
<Properties>
<Property name='name' type='string' key='name'>
文档名称 (选填)
文档名称(选填)
</Property>
<Property name='file' type='multipart/form-data' key='file'>
需要上传的文件
@ -545,7 +545,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- <code>enabled</code> (bool) 是否选中该规则,不传入文档 ID 时代表默认值
- <code>segmentation</code> (object) 分段规则
- <code>separator</code> 自定义分段标识符,目前仅允许设置一个分隔符。默认为 \n
- <code>max_tokens</code> 最大长度 (token) 默认为 1000
- <code>max_tokens</code> 最大长度token默认为 1000
</Property>
</Properties>
</Col>
@ -553,11 +553,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/documents/{document_id}/update_by_file"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"name":"Dify","indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
label="/datasets/{dataset_id}/documents/{document_id}/update-by-file"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update-by-file' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'data="{"name":"Dify","indexing_technique":"high_quality","process_rule":{"rules":{"pre_processing_rules":[{"id":"remove_extra_spaces","enabled":true},{"id":"remove_urls_emails","enabled":true}],"segmentation":{"separator":"###","max_tokens":500}},"mode":"custom"}}";type=text/plain' \\\n--form 'file=@"/path/to/file"'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update_by_file' \
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/update-by-file' \
--header 'Authorization: Bearer {api_key}' \
--form 'data="{\"name\":\"Dify\",\"indexing_technique\":\"high_quality\",\"process_rule\":{\"rules\":{\"pre_processing_rules\":[{\"id\":\"remove_extra_spaces\",\"enabled\":true},{\"id\":\"remove_urls_emails\",\"enabled\":true}],\"segmentation\":{\"separator\":\"###\",\"max_tokens\":500}},\"mode\":\"custom\"}}";type=text/plain' \
--form 'file=@"/path/to/file"'
@ -597,7 +597,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{batch}/indexing-status'
@ -652,7 +652,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}'
@ -694,7 +694,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents'
@ -769,7 +769,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments'
@ -793,7 +793,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Properties>
<Property name='segments' type='object list' key='segments'>
- <code>content</code> (text) 文本内容/问题内容,必填
- <code>answer</code> (text) 答案内容,非必填,如果知识库的模式为qa模式则传值
- <code>answer</code> (text) 答案内容,非必填,如果知识库的模式为 Q&A 模式则传值
- <code>keywords</code> (list) 关键字,非必填
</Property>
</Properties>
@ -855,7 +855,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments'
@ -933,7 +933,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
@ -979,7 +979,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
@ -1006,7 +1006,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Properties>
<Property name='segment' type='object' key='segment'>
- <code>content</code> (text) 文本内容/问题内容,必填
- <code>answer</code> (text) 答案内容,非必填,如果知识库的模式为qa模式则传值
- <code>answer</code> (text) 答案内容,非必填,如果知识库的模式为 Q&A 模式则传值
- <code>keywords</code> (list) 关键字,非必填
- <code>enabled</code> (bool) false/true非必填
</Property>
@ -1068,13 +1068,13 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
</Row>
---
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/hit_testing'
url='/datasets/{dataset_id}/retrieve'
method='POST'
title='知识库召回测试'
name='#dataset_hit_testing'
title='检索知识库'
name='#dataset_retrieval'
/>
<Row>
<Col>
@ -1088,23 +1088,23 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### Request Body
<Properties>
<Property name='query' type='string' key='query'>
召回关键词
检索关键词
</Property>
<Property name='retrieval_model' type='object' key='retrieval_model'>
召回参数(选填,如不填,按照默认方式召回)
检索参数选填,如不填,按照默认方式召回
- <code>search_method</code> (text) 检索方法:以下三个关键字之一,必填
- <code>keyword_search</code> 关键字检索
- <code>semantic_search</code> 语义检索
- <code>full_text_search</code> 全文检索
- <code>hybrid_search</code> 混合检索
- <code>reranking_enable</code> (bool) 是否启用 Reranking非必填如果检索模式为semantic_search模式或者hybrid_search则传值
- <code>reranking_enable</code> (bool) 是否启用 Reranking非必填如果检索模式为 semantic_search 模式或者 hybrid_search 则传值
- <code>reranking_mode</code> (object) Rerank模型配置非必填如果启用了 reranking 则传值
- <code>reranking_provider_name</code> (string) Rerank 模型提供商
- <code>reranking_model_name</code> (string) Rerank 模型名称
- <code>weights</code> (double) 混合检索模式下语意检索的权重设置
- <code>top_k</code> (integer) 返回结果数量,非必填
- <code>score_threshold_enabled</code> (bool) 是否开启Score阈值
- <code>score_threshold</code> (double) Score阈值
- <code>score_threshold_enabled</code> (bool) 是否开启 score 阈值
- <code>score_threshold</code> (double) Score 阈值
</Property>
<Property name='external_retrieval_model' type='object' key='external_retrieval_model'>
未启用字段
@ -1115,26 +1115,26 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/hit_testing"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/hit_testing' \\\n--header 'Authorization: Bearer {api_key}'\\\n--header 'Content-Type: application/json'\\\n--data-raw '{
"query": "test",
"retrieval_model": {
"search_method": "keyword_search",
"reranking_enable": false,
"reranking_mode": null,
"reranking_model": {
"reranking_provider_name": "",
"reranking_model_name": ""
},
"weights": null,
"top_k": 1,
"score_threshold_enabled": false,
"score_threshold": null
}
}'`}
label="/datasets/{dataset_id}/retrieve"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/retrieve' \\\n--header 'Authorization: Bearer {api_key}'\\\n--header 'Content-Type: application/json'\\\n--data-raw '{
"query": "test",
"retrieval_model": {
"search_method": "keyword_search",
"reranking_enable": false,
"reranking_mode": null,
"reranking_model": {
"reranking_provider_name": "",
"reranking_model_name": ""
},
"weights": null,
"top_k": 1,
"score_threshold_enabled": false,
"score_threshold": null
}
}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/hit_testing' \
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/retrieve' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -1214,7 +1214,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Row>
---
<hr className='ml-0 mr-0' />
<Row>
<Col>

View File

@ -23,8 +23,9 @@ export default function AppSelector() {
params: {},
})
if (localStorage?.getItem('console_token'))
localStorage.removeItem('console_token')
localStorage.removeItem('setup_status')
localStorage.removeItem('console_token')
localStorage.removeItem('refresh_token')
router.push('/signin')
}

View File

@ -33,6 +33,10 @@ import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
// type
import type { AutomaticRes } from '@/service/debug'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
export interface IGetAutomaticResProps {
mode: AppType
@ -68,7 +72,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
onFinished,
}) => {
const { t } = useTranslation()
const {
currentProvider,
currentModel,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
const tryList = [
{
icon: RiTerminalBoxLine,
@ -191,6 +198,19 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
<div className={`leading-[28px] text-lg font-bold ${s.textGradient}`}>{t('appDebug.generate.title')}</div>
<div className='mt-1 text-[13px] font-normal text-gray-500'>{t('appDebug.generate.description')}</div>
</div>
<div className='flex items-center mb-8'>
<ModelIcon
className='shrink-0 mr-1.5 '
provider={currentProvider}
modelName={currentModel?.model}
/>
<ModelName
className='grow'
modelItem={currentModel!}
showMode
showFeatures
/>
</div>
<div >
<div className='flex items-center'>
<div className='mr-3 shrink-0 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appDebug.generate.tryIt')}</div>

View File

@ -105,6 +105,15 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
<div className='text-[13px] text-gray-400'>{t('appDebug.codegen.loading')}</div>
</div>
)
const renderNoData = (
<div className='w-0 grow flex flex-col items-center px-8 justify-center h-full space-y-3'>
<Generator className='w-14 h-14 text-gray-300' />
<div className='leading-5 text-center text-[13px] font-normal text-gray-400'>
<div>{t('appDebug.codegen.noDataLine1')}</div>
<div>{t('appDebug.codegen.noDataLine2')}</div>
</div>
</div>
)
return (
<Modal
@ -157,6 +166,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
</div>
</div>
{isLoading && renderLoading}
{!isLoading && !res && renderNoData}
{(!isLoading && res) && (
<div className='w-0 grow p-6 pb-0 h-full'>
<div className='shrink-0 mb-3 leading-[160%] text-base font-semibold text-gray-800'>{t('appDebug.codegen.resTitle')}</div>

View File

@ -15,6 +15,7 @@ import { AppType } from '@/types/app'
import type { DataSet } from '@/models/datasets'
import {
getMultipleRetrievalConfig,
getSelectedDatasetsMode,
} from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -38,6 +39,7 @@ const DatasetConfig: FC = () => {
isAgent,
datasetConfigs,
setDatasetConfigs,
setRerankSettingModalOpen,
} = useContext(ConfigContext)
const formattingChangedDispatcher = useFormattingChangedDispatcher()
@ -55,6 +57,20 @@ const DatasetConfig: FC = () => {
...(datasetConfigs as any),
...retrievalConfig,
})
const {
allExternal,
allInternal,
mixtureInternalAndExternal,
mixtureHighQualityAndEconomic,
inconsistentEmbeddingModel,
} = getSelectedDatasetsMode(filteredDataSets)
if (
(allInternal && (mixtureHighQualityAndEconomic || inconsistentEmbeddingModel))
|| mixtureInternalAndExternal
|| allExternal
)
setRerankSettingModalOpen(true)
formattingChangedDispatcher()
}

View File

@ -266,7 +266,7 @@ const ConfigContent: FC<Props> = ({
<div className='mt-2'>
<div className='flex items-center'>
{
selectedDatasetsMode.allEconomic && (
selectedDatasetsMode.allEconomic && !selectedDatasetsMode.mixtureInternalAndExternal && (
<div
className='flex items-center'
onClick={handleDisabledSwitchClick}

View File

@ -12,6 +12,7 @@ import { RETRIEVE_TYPE } from '@/types/app'
import Toast from '@/app/components/base/toast'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { RerankingModeEnum } from '@/models/datasets'
import type { DataSet } from '@/models/datasets'
import type { DatasetConfigs } from '@/models/debug'
import {
@ -47,7 +48,10 @@ const ParamsConfig = ({
const isValid = () => {
let errMsg = ''
if (tempDataSetConfigs.retrieval_model === RETRIEVE_TYPE.multiWay) {
if (!tempDataSetConfigs.reranking_model?.reranking_model_name && (rerankDefaultModel && !isRerankDefaultModelValid))
if (tempDataSetConfigs.reranking_enable
&& tempDataSetConfigs.reranking_mode === RerankingModeEnum.RerankingModel
&& !isRerankDefaultModelValid
)
errMsg = t('appDebug.datasetConfig.rerankModelRequired')
}
if (errMsg) {
@ -62,7 +66,9 @@ const ParamsConfig = ({
if (!isValid())
return
const config = { ...tempDataSetConfigs }
if (config.retrieval_model === RETRIEVE_TYPE.multiWay && !config.reranking_model) {
if (config.retrieval_model === RETRIEVE_TYPE.multiWay
&& config.reranking_mode === RerankingModeEnum.RerankingModel
&& !config.reranking_model) {
config.reranking_model = {
reranking_provider_name: rerankDefaultModel?.provider?.provider,
reranking_model_name: rerankDefaultModel?.model,

View File

@ -252,12 +252,18 @@ const Configuration: FC = () => {
}
hideSelectDataSet()
const {
allEconomic,
allExternal,
allInternal,
mixtureInternalAndExternal,
mixtureHighQualityAndEconomic,
inconsistentEmbeddingModel,
} = getSelectedDatasetsMode(newDatasets)
if (allEconomic || mixtureHighQualityAndEconomic || inconsistentEmbeddingModel)
if (
(allInternal && (mixtureHighQualityAndEconomic || inconsistentEmbeddingModel))
|| mixtureInternalAndExternal
|| allExternal
)
setRerankSettingModalOpen(true)
const { datasets, retrieval_model, score_threshold_enabled, ...restConfigs } = datasetConfigs

View File

@ -36,6 +36,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import TextGeneration from '@/app/components/app/text-generate/item'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import MessageLogModal from '@/app/components/base/message-log-modal'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useAppContext } from '@/context/app-context'
import useTimestamp from '@/hooks/use-timestamp'
@ -168,11 +169,13 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const { userProfile: { timezone } } = useAppContext()
const { formatTime } = useTimestamp()
const { onClose, appDetail } = useContext(DrawerContext)
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
currentLogItem: state.currentLogItem,
setCurrentLogItem: state.setCurrentLogItem,
showMessageLogModal: state.showMessageLogModal,
setShowMessageLogModal: state.setShowMessageLogModal,
showPromptLogModal: state.showPromptLogModal,
setShowPromptLogModal: state.setShowPromptLogModal,
currentLogModalActiveTab: state.currentLogModalActiveTab,
})))
const { t } = useTranslation()
@ -192,8 +195,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
conversation_id: detail.id,
limit: 10,
}
if (allChatItems.at(-1)?.id)
params.first_id = allChatItems.at(-1)?.id.replace('question-', '')
if (allChatItems[0]?.id)
params.first_id = allChatItems[0]?.id.replace('question-', '')
const messageRes = await fetchChatMessages({
url: `/apps/${appDetail?.id}/chat-messages`,
params,
@ -557,6 +560,16 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
defaultTab={currentLogModalActiveTab}
/>
)}
{showPromptLogModal && (
<PromptLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowPromptLogModal(false)
}}
/>
)}
</div>
)
}

View File

@ -8,18 +8,22 @@ import classNames from 'classnames'
import { ImagePlus } from '../icons/src/vender/line/images'
import { useDraggableUploader } from './hooks'
import { checkIsAnimatedImage } from './utils'
import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
type UploaderProps = {
className?: string
onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void
onUpload?: (file?: File) => void
}
const Uploader: FC<UploaderProps> = ({
className,
onImageCropped,
onUpload,
}) => {
const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
useEffect(() => {
return () => {
if (inputImage)
@ -34,12 +38,19 @@ const Uploader: FC<UploaderProps> = ({
if (!inputImage)
return
onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name)
onUpload?.(undefined)
}
const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file)
if (file) {
setInputImage({ file, url: URL.createObjectURL(file) })
checkIsAnimatedImage(file).then((isAnimatedImage) => {
setIsAnimatedImage(!!isAnimatedImage)
if (isAnimatedImage)
onUpload?.(file)
})
}
}
const {
@ -52,6 +63,26 @@ const Uploader: FC<UploaderProps> = ({
const inputRef = createRef<HTMLInputElement>()
const handleShowImage = () => {
if (isAnimatedImage) {
return (
<img src={inputImage?.url} alt='' />
)
}
return (
<Cropper
image={inputImage?.url}
crop={crop}
zoom={zoom}
aspect={1}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
)
}
return (
<div className={classNames(className, 'w-full px-3 py-1.5')}>
<div
@ -79,15 +110,7 @@ const Uploader: FC<UploaderProps> = ({
</div>
<div className="text-xs pointer-events-none">Supports PNG, JPG, JPEG, WEBP and GIF</div>
</>
: <Cropper
image={inputImage.url}
crop={crop}
zoom={zoom}
aspect={1}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
: handleShowImage()
}
</div>
</div>

View File

@ -74,6 +74,11 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
setImageCropInfo({ tempUrl, croppedAreaPixels, fileName })
}
const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>()
const handleUpload = async (file?: File) => {
setUploadImageInfo({ file })
}
const handleSelect = async () => {
if (activeTab === 'emoji') {
if (emoji) {
@ -85,9 +90,13 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
}
}
else {
if (!imageCropInfo)
if (!imageCropInfo && !uploadImageInfo)
return
setUploading(true)
if (imageCropInfo.file) {
handleLocalFileUpload(imageCropInfo.file)
return
}
const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName)
const file = new File([blob], imageCropInfo.fileName, { type: blob.type })
handleLocalFileUpload(file)
@ -121,7 +130,7 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
<Divider className='m-0' />
<EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} />
<Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} />
<Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} onUpload={handleUpload}/>
<Divider className='m-0' />
<div className='w-full flex items-center justify-center p-3 gap-2'>

View File

@ -115,3 +115,52 @@ export default async function getCroppedImg(
}, mimeType)
})
}
export function checkIsAnimatedImage(file) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
fileReader.onload = function (e) {
const arr = new Uint8Array(e.target.result)
// Check file extension
const fileName = file.name.toLowerCase()
if (fileName.endsWith('.gif')) {
// If file is a GIF, assume it's animated
resolve(true)
}
// Check for WebP signature (RIFF and WEBP)
else if (isWebP(arr)) {
resolve(checkWebPAnimation(arr)) // Check if it's animated
}
else {
resolve(false) // Not a GIF or WebP
}
}
fileReader.onerror = function (err) {
reject(err) // Reject the promise on error
}
// Read the file as an array buffer
fileReader.readAsArrayBuffer(file)
})
}
// Function to check for WebP signature
function isWebP(arr) {
return (
arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46
&& arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50
) // "WEBP"
}
// Function to check if the WebP is animated (contains ANIM chunk)
function checkWebPAnimation(arr) {
// Search for the ANIM chunk in WebP to determine if it's animated
for (let i = 12; i < arr.length - 4; i++) {
if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D)
return true // Found animation
}
return false // No animation chunk found
}

View File

@ -1804,6 +1804,280 @@ exports[`build chat item tree and get thread messages should get thread messages
]
`;
exports[`build chat item tree and get thread messages should work with partial messages 1`] = `
[
{
"children": [
{
"agent_thoughts": [
{
"chain_id": null,
"created_at": 1726105809,
"files": [],
"id": "1019cd79-d141-4f9f-880a-fc1441cfd802",
"message_id": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
"observation": "",
"position": 1,
"thought": "Sure! My number is 54. Your turn!",
"tool": "",
"tool_input": "",
"tool_labels": {},
},
],
"children": [
{
"children": [
{
"agent_thoughts": [
{
"chain_id": null,
"created_at": 1726105822,
"files": [],
"id": "0773bec7-b992-4a53-92b2-20ebaeae8798",
"message_id": "324bce32-c98c-435d-a66b-bac974ebb5ed",
"observation": "",
"position": 1,
"thought": "My number is 4729. Your turn!",
"tool": "",
"tool_input": "",
"tool_labels": {},
},
],
"children": [],
"content": "My number is 4729. Your turn!",
"conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
"feedbackDisabled": false,
"id": "324bce32-c98c-435d-a66b-bac974ebb5ed",
"input": {
"inputs": {},
"query": "3306",
},
"isAnswer": true,
"log": [
{
"files": [],
"role": "user",
"text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
},
{
"files": [],
"role": "assistant",
"text": "Sure! My number is 54. Your turn!",
},
{
"files": [],
"role": "user",
"text": "3306",
},
{
"files": [],
"role": "assistant",
"text": "My number is 4729. Your turn!",
},
],
"message_files": [],
"more": {
"latency": "1.30",
"time": "09/11/2024 09:50 PM",
"tokens": 66,
},
"parentMessageId": "question-324bce32-c98c-435d-a66b-bac974ebb5ed",
"siblingIndex": 0,
"workflow_run_id": null,
},
],
"content": "3306",
"id": "question-324bce32-c98c-435d-a66b-bac974ebb5ed",
"isAnswer": false,
"message_files": [],
"parentMessageId": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
},
{
"children": [
{
"agent_thoughts": [
{
"chain_id": null,
"created_at": 1726107812,
"files": [],
"id": "5ca650f3-982c-4399-8b95-9ea241c76707",
"message_id": "684b5396-4e91-4043-88e9-aabe48b21acc",
"observation": "",
"position": 1,
"thought": "My number is 4821. Your turn!",
"tool": "",
"tool_input": "",
"tool_labels": {},
},
],
"children": [
{
"children": [
{
"agent_thoughts": [
{
"chain_id": null,
"created_at": 1726111024,
"files": [],
"id": "095cacab-afad-4387-a41d-1662578b8b13",
"message_id": "19904a7b-7494-4ed8-b72c-1d18668cea8c",
"observation": "",
"position": 1,
"thought": "My number is 1456. Your turn!",
"tool": "",
"tool_input": "",
"tool_labels": {},
},
],
"children": [],
"content": "My number is 1456. Your turn!",
"conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
"feedbackDisabled": false,
"id": "19904a7b-7494-4ed8-b72c-1d18668cea8c",
"input": {
"inputs": {},
"query": "1003",
},
"isAnswer": true,
"log": [
{
"files": [],
"role": "user",
"text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
},
{
"files": [],
"role": "assistant",
"text": "Sure! My number is 54. Your turn!",
},
{
"files": [],
"role": "user",
"text": "3306",
},
{
"files": [],
"role": "assistant",
"text": "My number is 4821. Your turn!",
},
{
"files": [],
"role": "user",
"text": "1003",
},
{
"files": [],
"role": "assistant",
"text": "My number is 1456. Your turn!",
},
],
"message_files": [],
"more": {
"latency": "1.38",
"time": "09/11/2024 11:17 PM",
"tokens": 86,
},
"parentMessageId": "question-19904a7b-7494-4ed8-b72c-1d18668cea8c",
"siblingIndex": 0,
"workflow_run_id": null,
},
],
"content": "1003",
"id": "question-19904a7b-7494-4ed8-b72c-1d18668cea8c",
"isAnswer": false,
"message_files": [],
"parentMessageId": "684b5396-4e91-4043-88e9-aabe48b21acc",
},
],
"content": "My number is 4821. Your turn!",
"conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
"feedbackDisabled": false,
"id": "684b5396-4e91-4043-88e9-aabe48b21acc",
"input": {
"inputs": {},
"query": "3306",
},
"isAnswer": true,
"log": [
{
"files": [],
"role": "user",
"text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
},
{
"files": [],
"role": "assistant",
"text": "Sure! My number is 54. Your turn!",
},
{
"files": [],
"role": "user",
"text": "3306",
},
{
"files": [],
"role": "assistant",
"text": "My number is 4821. Your turn!",
},
],
"message_files": [],
"more": {
"latency": "1.48",
"time": "09/11/2024 10:23 PM",
"tokens": 66,
},
"parentMessageId": "question-684b5396-4e91-4043-88e9-aabe48b21acc",
"siblingIndex": 1,
"workflow_run_id": null,
},
],
"content": "3306",
"id": "question-684b5396-4e91-4043-88e9-aabe48b21acc",
"isAnswer": false,
"message_files": [],
"parentMessageId": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
},
],
"content": "Sure! My number is 54. Your turn!",
"conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
"feedbackDisabled": false,
"id": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
"input": {
"inputs": {},
"query": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
},
"isAnswer": true,
"log": [
{
"files": [],
"role": "user",
"text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
},
{
"files": [],
"role": "assistant",
"text": "Sure! My number is 54. Your turn!",
},
],
"message_files": [],
"more": {
"latency": "1.52",
"time": "09/11/2024 09:50 PM",
"tokens": 46,
},
"parentMessageId": "question-cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
"siblingIndex": 0,
"workflow_run_id": null,
},
],
"content": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
"id": "question-cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
"isAnswer": false,
"message_files": [],
},
]
`;
exports[`build chat item tree and get thread messages should work with real world messages 1`] = `
[
{

View File

@ -255,4 +255,10 @@ describe('build chat item tree and get thread messages', () => {
const threadMessages6_2 = getThreadMessages(tree6, 'ff4c2b43-48a5-47ad-9dc5-08b34ddba61b')
expect(threadMessages6_2).toMatchSnapshot()
})
const partialMessages = (realWorldMessages as ChatItemInTree[]).slice(-10)
const tree7 = buildChatItemTree(partialMessages)
it('should work with partial messages', () => {
expect(tree7).toMatchSnapshot()
})
})

View File

@ -134,6 +134,12 @@ function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] {
}
}
// If no messages have parentMessageId=null (indicating a root node),
// then we likely have a partial chat history. In this case,
// use the first available message as the root node.
if (rootNodes.length === 0 && allMessages.length > 0)
rootNodes.push(map[allMessages[0]!.id]!)
return rootNodes
}

View File

@ -3,5 +3,6 @@ export const IMG_SIZE_LIMIT = 10 * 1024 * 1024
export const FILE_SIZE_LIMIT = 15 * 1024 * 1024
export const AUDIO_SIZE_LIMIT = 50 * 1024 * 1024
export const VIDEO_SIZE_LIMIT = 100 * 1024 * 1024
export const MAX_FILE_UPLOAD_LIMIT = 10
export const FILE_URL_REGEX = /^(https?|ftp):\/\//

View File

@ -1,6 +1,5 @@
import {
memo,
useMemo,
} from 'react'
import {
RiDeleteBinLine,
@ -35,17 +34,9 @@ const FileInAttachmentItem = ({
onRemove,
onReUpload,
}: FileInAttachmentItemProps) => {
const { id, name, type, progress, supportFileType, base64Url, url } = file
const ext = getFileExtension(name, type)
const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file
const ext = getFileExtension(name, type, isRemote)
const isImageFile = supportFileType === SupportUploadFileTypes.image
const nameArr = useMemo(() => {
const nameMatch = name.match(/(.+)\.([^.]+)$/)
if (nameMatch)
return [nameMatch[1], nameMatch[2]]
return [name, '']
}, [name])
return (
<div className={cn(
@ -75,12 +66,7 @@ const FileInAttachmentItem = ({
className='flex items-center mb-0.5 system-xs-medium text-text-secondary truncate'
title={file.name}
>
<div className='truncate'>{nameArr[0]}</div>
{
nameArr[1] && (
<span>.{nameArr[1]}</span>
)
}
<div className='truncate'>{name}</div>
</div>
<div className='flex items-center system-2xs-medium-uppercase text-text-tertiary'>
{
@ -93,7 +79,11 @@ const FileInAttachmentItem = ({
<span className='mx-1 system-2xs-medium'></span>
)
}
<span>{formatFileSize(file.size || 0)}</span>
{
!!file.size && (
<span>{formatFileSize(file.size)}</span>
)
}
</div>
</div>
<div className='shrink-0 flex items-center'>

View File

@ -31,8 +31,8 @@ const FileItem = ({
onRemove,
onReUpload,
}: FileItemProps) => {
const { id, name, type, progress, url } = file
const ext = getFileExtension(name, type)
const { id, name, type, progress, url, isRemote } = file
const ext = getFileExtension(name, type, isRemote)
const uploadError = progress === -1
return (
@ -75,7 +75,9 @@ const FileItem = ({
</>
)
}
{formatFileSize(file.size || 0)}
{
!!file.size && formatFileSize(file.size)
}
</div>
{
showDownloadAction && (

View File

@ -18,6 +18,7 @@ import {
AUDIO_SIZE_LIMIT,
FILE_SIZE_LIMIT,
IMG_SIZE_LIMIT,
MAX_FILE_UPLOAD_LIMIT,
VIDEO_SIZE_LIMIT,
} from '@/app/components/base/file-uploader/constants'
import { useToastContext } from '@/app/components/base/toast'
@ -25,7 +26,7 @@ import { TransferMethod } from '@/types/app'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import type { FileUpload } from '@/app/components/base/features/types'
import { formatFileSize } from '@/utils/format'
import { fetchRemoteFileInfo } from '@/service/common'
import { uploadRemoteFileInfo } from '@/service/common'
import type { FileUploadConfigResponse } from '@/models/common'
export const useFileSizeLimit = (fileUploadConfig?: FileUploadConfigResponse) => {
@ -33,12 +34,14 @@ export const useFileSizeLimit = (fileUploadConfig?: FileUploadConfigResponse) =>
const docSizeLimit = Number(fileUploadConfig?.file_size_limit) * 1024 * 1024 || FILE_SIZE_LIMIT
const audioSizeLimit = Number(fileUploadConfig?.audio_file_size_limit) * 1024 * 1024 || AUDIO_SIZE_LIMIT
const videoSizeLimit = Number(fileUploadConfig?.video_file_size_limit) * 1024 * 1024 || VIDEO_SIZE_LIMIT
const maxFileUploadLimit = Number(fileUploadConfig?.workflow_file_upload_limit) || MAX_FILE_UPLOAD_LIMIT
return {
imgSizeLimit,
docSizeLimit,
audioSizeLimit,
videoSizeLimit,
maxFileUploadLimit,
}
}
@ -49,7 +52,7 @@ export const useFile = (fileConfig: FileUpload) => {
const params = useParams()
const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileConfig.fileUploadConfig)
const checkSizeLimit = (fileType: string, fileSize: number) => {
const checkSizeLimit = useCallback((fileType: string, fileSize: number) => {
switch (fileType) {
case SupportUploadFileTypes.image: {
if (fileSize > imgSizeLimit) {
@ -120,7 +123,7 @@ export const useFile = (fileConfig: FileUpload) => {
return true
}
}
}
}, [audioSizeLimit, docSizeLimit, imgSizeLimit, notify, t, videoSizeLimit])
const handleAddFile = useCallback((newFile: FileEntity) => {
const {
@ -188,6 +191,17 @@ export const useFile = (fileConfig: FileUpload) => {
}
}, [fileStore, notify, t, handleUpdateFile, params])
const startProgressTimer = useCallback((fileId: string) => {
const timer = setInterval(() => {
const files = fileStore.getState().files
const file = files.find(file => file.id === fileId)
if (file && file.progress < 80 && file.progress >= 0)
handleUpdateFile({ ...file, progress: file.progress + 20 })
else
clearTimeout(timer)
}, 200)
}, [fileStore, handleUpdateFile])
const handleLoadFileFromLink = useCallback((url: string) => {
const allowedFileTypes = fileConfig.allowed_file_types
@ -197,19 +211,27 @@ export const useFile = (fileConfig: FileUpload) => {
type: '',
size: 0,
progress: 0,
transferMethod: TransferMethod.remote_url,
transferMethod: TransferMethod.local_file,
supportFileType: '',
url,
isRemote: true,
}
handleAddFile(uploadingFile)
startProgressTimer(uploadingFile.id)
fetchRemoteFileInfo(url).then((res) => {
uploadRemoteFileInfo(url, !!params.token).then((res) => {
const newFile = {
...uploadingFile,
type: res.file_type,
size: res.file_length,
type: res.mime_type,
size: res.size,
progress: 100,
supportFileType: getSupportFileType(url, res.file_type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)),
supportFileType: getSupportFileType(res.name, res.mime_type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)),
uploadedId: res.id,
url: res.url,
}
if (!isAllowedFileExtension(res.name, res.mime_type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) {
notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') })
handleRemoveFile(uploadingFile.id)
}
if (!checkSizeLimit(newFile.supportFileType, newFile.size))
handleRemoveFile(uploadingFile.id)
@ -219,7 +241,7 @@ export const useFile = (fileConfig: FileUpload) => {
notify({ type: 'error', message: t('common.fileUploader.pasteFileLinkInvalid') })
handleRemoveFile(uploadingFile.id)
})
}, [checkSizeLimit, handleAddFile, handleUpdateFile, notify, t, handleRemoveFile, fileConfig?.allowed_file_types])
}, [checkSizeLimit, handleAddFile, handleUpdateFile, notify, t, handleRemoveFile, fileConfig?.allowed_file_types, fileConfig.allowed_file_extensions, startProgressTimer])
const handleLoadFileFromLinkSuccess = useCallback(() => { }, [])

View File

@ -29,4 +29,5 @@ export type FileEntity = {
uploadedId?: string
base64Url?: string
url?: string
isRemote?: boolean
}

View File

@ -43,10 +43,13 @@ export const fileUpload: FileUpload = ({
})
}
export const getFileExtension = (fileName: string, fileMimetype: string) => {
export const getFileExtension = (fileName: string, fileMimetype: string, isRemote?: boolean) => {
if (fileMimetype)
return mime.getExtension(fileMimetype) || ''
if (isRemote)
return ''
if (fileName) {
const fileNamePair = fileName.split('.')
const fileNamePairLength = fileNamePair.length

View File

@ -133,6 +133,7 @@ const ImageList: FC<ImageListProps> = ({
<ImagePreview
url={imagePreviewUrl}
onCancel={() => setImagePreviewUrl('')}
title=''
/>
)}
</div>

View File

@ -1,5 +1,5 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiSearchLine } from '@remixicon/react'
import cn from '@/utils/classnames'
@ -12,6 +12,7 @@ type SearchInputProps = {
onChange: (v: string) => void
white?: boolean
}
const SearchInput: FC<SearchInputProps> = ({
placeholder,
className,
@ -21,6 +22,7 @@ const SearchInput: FC<SearchInputProps> = ({
}) => {
const { t } = useTranslation()
const [focus, setFocus] = useState<boolean>(false)
const isComposing = useRef<boolean>(false)
return (
<div className={cn(
@ -45,7 +47,14 @@ const SearchInput: FC<SearchInputProps> = ({
placeholder={placeholder || t('common.operation.search')!}
value={value}
onChange={(e) => {
onChange(e.target.value)
if (!isComposing.current)
onChange(e.target.value)
}}
onCompositionStart={() => {
isComposing.current = true
}}
onCompositionEnd={() => {
isComposing.current = false
}}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}

View File

@ -126,7 +126,7 @@ const Select: FC<ISelectProps> = ({
</Combobox.Button>
</div>
{filteredItems.length > 0 && (
{(filteredItems.length > 0 && open) && (
<Combobox.Options className={`absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm ${overlayClassName}`}>
{filteredItems.map((item: Item) => (
<Combobox.Option

View File

@ -74,7 +74,7 @@ const UpgradeBtn: FC<Props> = ({
onClick={onClick}
>
<GoldCoin className='mr-1 w-3.5 h-3.5' />
<div className='text-xs font-normal'>{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}</div>
<div className='text-xs font-normal text-nowrap'>{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}</div>
<Sparkles
className='absolute -right-1 -top-2 w-4 h-5 bg-cover'
/>

View File

@ -39,6 +39,7 @@ export const Heading = function H2({
}
return (
<>
<span id={name?.replace(/^#/, '')} className='relative -top-28' />
<div className="flex items-center gap-x-3" >
<span className={`font-mono text-[0.625rem] font-semibold leading-6 rounded-lg px-1.5 ring-1 ring-inset ${style}`}>{method}</span>
{/* <span className="h-0.5 w-0.5 rounded-full bg-zinc-300 dark:bg-zinc-600"></span> */}

View File

@ -656,6 +656,11 @@ Chat applications support session persistence, allowing previous chat history to
<Property name='pinned' type='bool' key='pinned'>
Return only pinned conversations as `true`, only non-pinned as `false`
</Property>
<Property name='sort_by' type='string' key='sort_by'>
Sorting Field (Optional), Default: -updated_at (sorted in descending order by update time)
- Available Values: created_at, -created_at, updated_at, -updated_at
- The symbol before the field represents the order or reverse, "-" represents reverse order.
</Property>
</Properties>
### Response

View File

@ -691,6 +691,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Property name='pinned' type='bool' key='pinned'>
只返回置顶 true只返回非置顶 false
</Property>
<Property name='sort_by' type='string' key='sort_by'>
排序字段(选题),默认 -updated_at(按更新时间倒序排列)
- 可选值created_at, -created_at, updated_at, -updated_at
- 字段前面的符号代表顺序或倒序,-代表倒序
</Property>
</Properties>
### Response

View File

@ -690,6 +690,11 @@ Chat applications support session persistence, allowing previous chat history to
<Property name='pinned' type='bool' key='pinned'>
Return only pinned conversations as `true`, only non-pinned as `false`
</Property>
<Property name='sort_by' type='string' key='sort_by'>
Sorting Field (Optional), Default: -updated_at (sorted in descending order by update time)
- Available Values: created_at, -created_at, updated_at, -updated_at
- The symbol before the field represents the order or reverse, "-" represents reverse order.
</Property>
</Properties>
### Response

View File

@ -705,6 +705,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Property name='pinned' type='bool' key='pinned'>
只返回置顶 true只返回非置顶 false
</Property>
<Property name='sort_by' type='string' key='sort_by'>
排序字段(选题),默认 -updated_at(按更新时间倒序排列)
- 可选值created_at, -created_at, updated_at, -updated_at
- 字段前面的符号代表顺序或倒序,-代表倒序
</Property>
</Properties>
### Response

View File

@ -28,7 +28,7 @@ const Category: FC<ICategoryProps> = ({
allCategoriesEn,
}) => {
const { t } = useTranslation()
const isAllCategories = !list.includes(value as AppCategory)
const isAllCategories = !list.includes(value as AppCategory) || value === allCategoriesEn
const itemClassName = (isSelected: boolean) => cn(
'flex items-center px-3 py-[7px] h-[32px] rounded-lg border-[0.5px] border-transparent text-gray-700 font-medium leading-[18px] cursor-pointer hover:bg-gray-200',
@ -44,7 +44,7 @@ const Category: FC<ICategoryProps> = ({
<ThumbsUp className='mr-1 w-3.5 h-3.5' />
{t('explore.apps.allCategories')}
</div>
{list.map(name => (
{list.filter(name => name !== allCategoriesEn).map(name => (
<div
key={name}
className={itemClassName(name === value)}

View File

@ -46,8 +46,9 @@ export default function AppSelector({ isMobile }: IAppSelector) {
params: {},
})
if (localStorage?.getItem('console_token'))
localStorage.removeItem('console_token')
localStorage.removeItem('setup_status')
localStorage.removeItem('console_token')
localStorage.removeItem('refresh_token')
router.push('/signin')
}

View File

@ -1,6 +1,6 @@
'use client'
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useState } from 'react'
import Modal from '@/app/components/base/modal'
import type { Item } from '@/app/components/base/select'
import type { InstallState } from '@/app/components/plugins/types'
@ -35,7 +35,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
: '',
selectedVersion: '',
selectedPackage: '',
releases: [],
releases: updatePayload ? updatePayload.originalPackageInfo.releases : [],
})
const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null)
const [manifest, setManifest] = useState<PluginDeclaration | null>(null)
@ -133,11 +133,6 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
})
}
useEffect(() => {
if (state.step === InstallStepFromGitHub.selectPackage)
handleUrlSubmit()
}, [])
return (
<Modal
isShow={true}

View File

@ -74,6 +74,7 @@ const Action: FC<Props> = ({
repo: meta!.repo,
version: meta!.version,
package: meta!.package,
releases: fetchedReleases,
},
},
},

View File

@ -155,6 +155,7 @@ export type UpdateFromGitHubPayload = {
repo: string
version: string
package: string
releases: GitHubRepoReleaseResponse[]
}
}

View File

@ -4,7 +4,6 @@ import { SWRConfig } from 'swr'
import { useCallback, useEffect, useState } from 'react'
import type { ReactNode } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import useRefreshToken from '@/hooks/use-refresh-token'
import { fetchSetupStatus } from '@/service/common'
interface SwrInitorProps {
@ -15,12 +14,11 @@ const SwrInitor = ({
}: SwrInitorProps) => {
const router = useRouter()
const searchParams = useSearchParams()
const pathname = usePathname()
const { getNewAccessToken } = useRefreshToken()
const consoleToken = searchParams.get('access_token')
const refreshToken = searchParams.get('refresh_token')
const consoleToken = decodeURIComponent(searchParams.get('access_token') || '')
const refreshToken = decodeURIComponent(searchParams.get('refresh_token') || '')
const consoleTokenFromLocalStorage = localStorage?.getItem('console_token')
const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token')
const pathname = usePathname()
const [init, setInit] = useState(false)
const isSetupFinished = useCallback(async () => {
@ -41,25 +39,6 @@ const SwrInitor = ({
}
}, [])
const setRefreshToken = useCallback(async () => {
try {
if (!(consoleToken || refreshToken || consoleTokenFromLocalStorage || refreshTokenFromLocalStorage))
return Promise.reject(new Error('No token found'))
if (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage)
await getNewAccessToken()
if (consoleToken && refreshToken) {
localStorage.setItem('console_token', consoleToken)
localStorage.setItem('refresh_token', refreshToken)
await getNewAccessToken()
}
}
catch (error) {
return Promise.reject(error)
}
}, [consoleToken, refreshToken, consoleTokenFromLocalStorage, refreshTokenFromLocalStorage, getNewAccessToken])
useEffect(() => {
(async () => {
try {
@ -68,9 +47,15 @@ const SwrInitor = ({
router.replace('/install')
return
}
await setRefreshToken()
if (searchParams.has('access_token') || searchParams.has('refresh_token'))
if (!((consoleToken && refreshToken) || (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage))) {
router.replace('/signin')
return
}
if (searchParams.has('access_token') || searchParams.has('refresh_token')) {
consoleToken && localStorage.setItem('console_token', consoleToken)
refreshToken && localStorage.setItem('refresh_token', refreshToken)
router.replace(pathname)
}
setInit(true)
}
@ -78,7 +63,7 @@ const SwrInitor = ({
router.replace('/signin')
}
})()
}, [isSetupFinished, setRefreshToken, router, pathname, searchParams])
}, [isSetupFinished, router, pathname, searchParams, consoleToken, refreshToken, consoleTokenFromLocalStorage, refreshTokenFromLocalStorage])
return init
? (

View File

@ -340,7 +340,9 @@ export const NODES_INITIAL_DATA = {
...ListFilterDefault.defaultValue,
},
}
export const MAX_ITERATION_PARALLEL_NUM = 10
export const MIN_ITERATION_PARALLEL_NUM = 1
export const DEFAULT_ITER_TIMES = 1
export const NODE_WIDTH = 240
export const X_OFFSET = 60
export const NODE_WIDTH_X_OFFSET = NODE_WIDTH + X_OFFSET

View File

@ -644,6 +644,11 @@ export const useNodesInteractions = () => {
newNode.data.isInIteration = true
newNode.data.iteration_id = prevNode.parentId
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
if (newNode.data.type === BlockEnum.Answer || newNode.data.type === BlockEnum.Tool || newNode.data.type === BlockEnum.Assigner) {
const parentIterNodeIndex = nodes.findIndex(node => node.id === prevNode.parentId)
const iterNodeData: IterationNodeType = nodes[parentIterNodeIndex].data
iterNodeData._isShowTips = true
}
}
const newEdge: Edge = {

View File

@ -14,6 +14,7 @@ import {
NodeRunningStatus,
WorkflowRunningStatus,
} from '../types'
import { DEFAULT_ITER_TIMES } from '../constants'
import { useWorkflowUpdate } from './use-workflow-interactions'
import { useStore as useAppStore } from '@/app/components/app/store'
import type { IOtherOptions } from '@/service/base'
@ -170,11 +171,13 @@ export const useWorkflowRun = () => {
const {
workflowRunningData,
setWorkflowRunningData,
setIterParallelLogMap,
} = workflowStore.getState()
const {
edges,
setEdges,
} = store.getState()
setIterParallelLogMap(new Map())
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.task_id = task_id
draft.result = {
@ -244,6 +247,8 @@ export const useWorkflowRun = () => {
const {
workflowRunningData,
setWorkflowRunningData,
iterParallelLogMap,
setIterParallelLogMap,
} = workflowStore.getState()
const {
getNodes,
@ -259,10 +264,21 @@ export const useWorkflowRun = () => {
const tracing = draft.tracing!
const iterations = tracing.find(trace => trace.node_id === node?.parentId)
const currIteration = iterations?.details![node.data.iteration_index] || iterations?.details![iterations.details!.length - 1]
currIteration?.push({
...data,
status: NodeRunningStatus.Running,
} as any)
if (!data.parallel_run_id) {
currIteration?.push({
...data,
status: NodeRunningStatus.Running,
} as any)
}
else {
if (!iterParallelLogMap.has(data.parallel_run_id))
iterParallelLogMap.set(data.parallel_run_id, [{ ...data, status: NodeRunningStatus.Running } as any])
else
iterParallelLogMap.get(data.parallel_run_id)!.push({ ...data, status: NodeRunningStatus.Running } as any)
setIterParallelLogMap(iterParallelLogMap)
if (iterations)
iterations.details = Array.from(iterParallelLogMap.values())
}
}))
}
else {
@ -309,6 +325,8 @@ export const useWorkflowRun = () => {
const {
workflowRunningData,
setWorkflowRunningData,
iterParallelLogMap,
setIterParallelLogMap,
} = workflowStore.getState()
const {
getNodes,
@ -317,21 +335,21 @@ export const useWorkflowRun = () => {
const nodes = getNodes()
const nodeParentId = nodes.find(node => node.id === data.node_id)!.parentId
if (nodeParentId) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const iterations = tracing.find(trace => trace.node_id === nodeParentId) // the iteration node
if (!data.execution_metadata.parallel_mode_run_id) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const iterations = tracing.find(trace => trace.node_id === nodeParentId) // the iteration node
if (iterations && iterations.details) {
const iterationIndex = data.execution_metadata?.iteration_index || 0
if (!iterations.details[iterationIndex])
iterations.details[iterationIndex] = []
if (iterations && iterations.details) {
const iterationIndex = data.execution_metadata?.iteration_index || 0
if (!iterations.details[iterationIndex])
iterations.details[iterationIndex] = []
const currIteration = iterations.details[iterationIndex]
const nodeIndex = currIteration.findIndex(node =>
node.node_id === data.node_id && (
node.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || node.parallel_id === data.execution_metadata?.parallel_id),
)
if (data.status === NodeRunningStatus.Succeeded) {
const currIteration = iterations.details[iterationIndex]
const nodeIndex = currIteration.findIndex(node =>
node.node_id === data.node_id && (
node.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || node.parallel_id === data.execution_metadata?.parallel_id),
)
if (nodeIndex !== -1) {
currIteration[nodeIndex] = {
...currIteration[nodeIndex],
@ -344,8 +362,40 @@ export const useWorkflowRun = () => {
} as any)
}
}
}
}))
}))
}
else {
// open parallel mode
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const iterations = tracing.find(trace => trace.node_id === nodeParentId) // the iteration node
if (iterations && iterations.details) {
const iterRunID = data.execution_metadata?.parallel_mode_run_id
const currIteration = iterParallelLogMap.get(iterRunID)
const nodeIndex = currIteration?.findIndex(node =>
node.node_id === data.node_id && (
node?.parallel_run_id === data.execution_metadata?.parallel_mode_run_id),
)
if (currIteration) {
if (nodeIndex !== undefined && nodeIndex !== -1) {
currIteration[nodeIndex] = {
...currIteration[nodeIndex],
...data,
} as any
}
else {
currIteration.push({
...data,
} as any)
}
}
setIterParallelLogMap(iterParallelLogMap)
iterations.details = Array.from(iterParallelLogMap.values())
}
}))
}
}
else {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
@ -379,6 +429,7 @@ export const useWorkflowRun = () => {
const {
workflowRunningData,
setWorkflowRunningData,
setIterTimes,
} = workflowStore.getState()
const {
getNodes,
@ -388,6 +439,7 @@ export const useWorkflowRun = () => {
transform,
} = store.getState()
const nodes = getNodes()
setIterTimes(DEFAULT_ITER_TIMES)
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
@ -431,6 +483,8 @@ export const useWorkflowRun = () => {
const {
workflowRunningData,
setWorkflowRunningData,
iterTimes,
setIterTimes,
} = workflowStore.getState()
const { data } = params
@ -445,13 +499,14 @@ export const useWorkflowRun = () => {
if (iteration.details!.length >= iteration.metadata.iterator_length!)
return
}
iteration?.details!.push([])
if (!data.parallel_mode_run_id)
iteration?.details!.push([])
}))
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._iterationIndex = data.index > 0 ? data.index : 1
currentNode.data._iterationIndex = iterTimes
setIterTimes(iterTimes + 1)
})
setNodes(newNodes)
@ -464,6 +519,7 @@ export const useWorkflowRun = () => {
const {
workflowRunningData,
setWorkflowRunningData,
setIterTimes,
} = workflowStore.getState()
const {
getNodes,
@ -480,7 +536,7 @@ export const useWorkflowRun = () => {
})
}
}))
setIterTimes(DEFAULT_ITER_TIMES)
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!

View File

@ -26,7 +26,7 @@ interface Props {
isFocus: boolean
isInNode?: boolean
onGenerated?: (prompt: string) => void
codeLanguages: CodeLanguage
codeLanguages?: CodeLanguage
fileList?: FileEntity[]
showFileList?: boolean
showCodeGenerator?: boolean
@ -78,7 +78,7 @@ const Base: FC<Props> = ({
e.stopPropagation()
}}>
{headerRight}
{showCodeGenerator && (
{showCodeGenerator && codeLanguages && (
<div className='ml-1'>
<CodeGeneratorButton onGenerated={onGenerated} codeLanguages={codeLanguages}/>
</div>

View File

@ -31,6 +31,7 @@ export interface Props {
noWrapper?: boolean
isExpand?: boolean
showFileList?: boolean
onGenerated?: (value: string) => void
showCodeGenerator?: boolean
}
@ -64,6 +65,7 @@ const CodeEditor: FC<Props> = ({
noWrapper,
isExpand,
showFileList,
onGenerated,
showCodeGenerator = false,
}) => {
const [isFocus, setIsFocus] = React.useState(false)
@ -151,9 +153,6 @@ const CodeEditor: FC<Props> = ({
return isFocus ? 'focus-theme' : 'blur-theme'
})()
const handleGenerated = (code: string) => {
handleEditorChange(code)
}
const main = (
<>
@ -205,7 +204,7 @@ const CodeEditor: FC<Props> = ({
isFocus={isFocus && !readOnly}
minHeight={minHeight}
isInNode={isInNode}
onGenerated={handleGenerated}
onGenerated={onGenerated}
codeLanguages={language}
fileList={fileList}
showFileList={showFileList}

View File

@ -12,15 +12,15 @@ import Tooltip from '@/app/components/base/tooltip'
interface Props {
className?: string
title: JSX.Element | string | DefaultTFuncReturn
tooltip?: React.ReactNode
isSubTitle?: boolean
tooltip?: string
supportFold?: boolean
children?: JSX.Element | string | null
operations?: JSX.Element
inline?: boolean
}
const Filed: FC<Props> = ({
const Field: FC<Props> = ({
className,
title,
isSubTitle,
@ -60,4 +60,4 @@ const Filed: FC<Props> = ({
</div>
)
}
export default React.memo(Filed)
export default React.memo(Field)

View File

@ -39,7 +39,13 @@ const FileUploadSetting: FC<Props> = ({
allowed_file_extensions,
} = payload
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileUploadConfigResponse)
const {
imgSizeLimit,
docSizeLimit,
audioSizeLimit,
videoSizeLimit,
maxFileUploadLimit,
} = useFileSizeLimit(fileUploadConfigResponse)
const handleSupportFileTypeChange = useCallback((type: SupportUploadFileTypes) => {
const newPayload = produce(payload, (draft) => {
@ -156,7 +162,7 @@ const FileUploadSetting: FC<Props> = ({
<InputNumberWithSlider
value={max_length}
min={1}
max={10}
max={maxFileUploadLimit}
onChange={handleMaxUploadNumLimitChange}
/>
</div>

View File

@ -106,32 +106,29 @@ const useOneStepRun = <T>({
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables)
const getVar = (valueSelector: ValueSelector): Var | undefined => {
let res: Var | undefined
const isSystem = valueSelector[0] === 'sys'
const targetVar = isSystem ? allOutputVars.find(item => !!item.isStartNode) : allOutputVars.find(v => v.nodeId === valueSelector[0])
const targetVar = allOutputVars.find(item => isSystem ? !!item.isStartNode : item.nodeId === valueSelector[0])
if (!targetVar)
return undefined
if (isSystem)
return targetVar.vars.find(item => item.variable.split('.')[1] === valueSelector[1])
let curr: any = targetVar.vars
if (!curr)
return
for (let i = 1; i < valueSelector.length; i++) {
const key = valueSelector[i]
const isLast = i === valueSelector.length - 1
valueSelector.slice(1).forEach((key, i) => {
const isLast = i === valueSelector.length - 2
// conversation variable is start with 'conversation.'
curr = curr?.find((v: any) => v.variable.replace('conversation.', '') === key)
if (isLast) {
res = curr
}
else {
if (curr?.type === VarType.object || curr?.type === VarType.file)
curr = curr.children
}
})
if (Array.isArray(curr))
curr = curr.find((v: any) => v.variable.replace('conversation.', '') === key)
return res
if (isLast)
return curr
else if (curr?.type === VarType.object || curr?.type === VarType.file)
curr = curr.children
}
return undefined
}
const checkValid = checkValidFns[data.type]

View File

@ -25,6 +25,7 @@ import {
useToolIcon,
} from '../../hooks'
import { useNodeIterationInteractions } from '../iteration/use-interactions'
import type { IterationNodeType } from '../iteration/types'
import {
NodeSourceHandle,
NodeTargetHandle,
@ -34,6 +35,7 @@ import NodeControl from './components/node-control'
import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon'
import Tooltip from '@/app/components/base/tooltip'
type BaseNodeProps = {
children: ReactElement
@ -166,9 +168,27 @@ const BaseNode: FC<BaseNodeProps> = ({
/>
<div
title={data.title}
className='grow mr-1 system-sm-semibold-uppercase text-text-primary truncate'
className='grow mr-1 system-sm-semibold-uppercase text-text-primary truncate flex items-center'
>
{data.title}
<div>
{data.title}
</div>
{
data.type === BlockEnum.Iteration && (data as IterationNodeType).is_parallel && (
<Tooltip popupContent={
<div className='w-[180px]'>
<div className='font-extrabold'>
{t('workflow.nodes.iteration.parallelModeEnableTitle')}
</div>
{t('workflow.nodes.iteration.parallelModeEnableDesc')}
</div>}
>
<div className='flex justify-center items-center px-[5px] py-[3px] ml-1 border-[1px] border-text-warning rounded-[5px] text-text-warning system-2xs-medium-uppercase '>
{t('workflow.nodes.iteration.parallelModeUpper')}
</div>
</Tooltip>
)
}
</div>
{
data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && (

View File

@ -0,0 +1,326 @@
import { VarType } from '../../types'
import { extractFunctionParams, extractReturnType } from './code-parser'
import { CodeLanguage } from './types'
const SAMPLE_CODES = {
python3: {
noParams: 'def main():',
singleParam: 'def main(param1):',
multipleParams: `def main(param1, param2, param3):
return {"result": param1}`,
withTypes: `def main(param1: str, param2: int, param3: List[str]):
result = process_data(param1, param2)
return {"output": result}`,
withDefaults: `def main(param1: str = "default", param2: int = 0):
return {"data": param1}`,
},
javascript: {
noParams: 'function main() {',
singleParam: 'function main(param1) {',
multipleParams: `function main(param1, param2, param3) {
return { result: param1 }
}`,
withComments: `// Main function
function main(param1, param2) {
// Process data
return { output: process(param1, param2) }
}`,
withSpaces: 'function main( param1 , param2 ) {',
},
}
describe('extractFunctionParams', () => {
describe('Python3', () => {
test('handles no parameters', () => {
const result = extractFunctionParams(SAMPLE_CODES.python3.noParams, CodeLanguage.python3)
expect(result).toEqual([])
})
test('extracts single parameter', () => {
const result = extractFunctionParams(SAMPLE_CODES.python3.singleParam, CodeLanguage.python3)
expect(result).toEqual(['param1'])
})
test('extracts multiple parameters', () => {
const result = extractFunctionParams(SAMPLE_CODES.python3.multipleParams, CodeLanguage.python3)
expect(result).toEqual(['param1', 'param2', 'param3'])
})
test('handles type hints', () => {
const result = extractFunctionParams(SAMPLE_CODES.python3.withTypes, CodeLanguage.python3)
expect(result).toEqual(['param1', 'param2', 'param3'])
})
test('handles default values', () => {
const result = extractFunctionParams(SAMPLE_CODES.python3.withDefaults, CodeLanguage.python3)
expect(result).toEqual(['param1', 'param2'])
})
})
// JavaScriptのテストケース
describe('JavaScript', () => {
test('handles no parameters', () => {
const result = extractFunctionParams(SAMPLE_CODES.javascript.noParams, CodeLanguage.javascript)
expect(result).toEqual([])
})
test('extracts single parameter', () => {
const result = extractFunctionParams(SAMPLE_CODES.javascript.singleParam, CodeLanguage.javascript)
expect(result).toEqual(['param1'])
})
test('extracts multiple parameters', () => {
const result = extractFunctionParams(SAMPLE_CODES.javascript.multipleParams, CodeLanguage.javascript)
expect(result).toEqual(['param1', 'param2', 'param3'])
})
test('handles comments in code', () => {
const result = extractFunctionParams(SAMPLE_CODES.javascript.withComments, CodeLanguage.javascript)
expect(result).toEqual(['param1', 'param2'])
})
test('handles whitespace', () => {
const result = extractFunctionParams(SAMPLE_CODES.javascript.withSpaces, CodeLanguage.javascript)
expect(result).toEqual(['param1', 'param2'])
})
})
})
const RETURN_TYPE_SAMPLES = {
python3: {
singleReturn: `
def main(param1):
return {"result": "value"}`,
multipleReturns: `
def main(param1, param2):
return {"result": "value", "status": "success"}`,
noReturn: `
def main():
print("Hello")`,
complexReturn: `
def main():
data = process()
return {"result": data, "count": 42, "messages": ["hello"]}`,
nestedObject: `
def main(name, age, city):
return {
'personal_info': {
'name': name,
'age': age,
'city': city
},
'timestamp': int(time.time()),
'status': 'active'
}`,
},
javascript: {
singleReturn: `
function main(param1) {
return { result: "value" }
}`,
multipleReturns: `
function main(param1) {
return { result: "value", status: "success" }
}`,
withParentheses: `
function main() {
return ({ result: "value", status: "success" })
}`,
noReturn: `
function main() {
console.log("Hello")
}`,
withQuotes: `
function main() {
return { "result": 'value', 'status': "success" }
}`,
nestedObject: `
function main(name, age, city) {
return {
personal_info: {
name: name,
age: age,
city: city
},
timestamp: Date.now(),
status: 'active'
}
}`,
withJSDoc: `
/**
* Creates a user profile with personal information and metadata
* @param {string} name - The user's name
* @param {number} age - The user's age
* @param {string} city - The user's city of residence
* @returns {Object} An object containing the user profile
*/
function main(name, age, city) {
return {
result: {
personal_info: {
name: name,
age: age,
city: city
},
timestamp: Date.now(),
status: 'active'
}
};
}`,
},
}
describe('extractReturnType', () => {
// Python3のテスト
describe('Python3', () => {
test('extracts single return value', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.singleReturn, CodeLanguage.python3)
expect(result).toEqual({
result: {
type: VarType.string,
children: null,
},
})
})
test('extracts multiple return values', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.multipleReturns, CodeLanguage.python3)
expect(result).toEqual({
result: {
type: VarType.string,
children: null,
},
status: {
type: VarType.string,
children: null,
},
})
})
test('returns empty object when no return statement', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.noReturn, CodeLanguage.python3)
expect(result).toEqual({})
})
test('handles complex return statement', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.complexReturn, CodeLanguage.python3)
expect(result).toEqual({
result: {
type: VarType.string,
children: null,
},
count: {
type: VarType.string,
children: null,
},
messages: {
type: VarType.string,
children: null,
},
})
})
test('handles nested object structure', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.nestedObject, CodeLanguage.python3)
expect(result).toEqual({
personal_info: {
type: VarType.string,
children: null,
},
timestamp: {
type: VarType.string,
children: null,
},
status: {
type: VarType.string,
children: null,
},
})
})
})
// JavaScriptのテスト
describe('JavaScript', () => {
test('extracts single return value', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.singleReturn, CodeLanguage.javascript)
expect(result).toEqual({
result: {
type: VarType.string,
children: null,
},
})
})
test('extracts multiple return values', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.multipleReturns, CodeLanguage.javascript)
expect(result).toEqual({
result: {
type: VarType.string,
children: null,
},
status: {
type: VarType.string,
children: null,
},
})
})
test('handles return with parentheses', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.withParentheses, CodeLanguage.javascript)
expect(result).toEqual({
result: {
type: VarType.string,
children: null,
},
status: {
type: VarType.string,
children: null,
},
})
})
test('returns empty object when no return statement', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.noReturn, CodeLanguage.javascript)
expect(result).toEqual({})
})
test('handles quoted keys', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.withQuotes, CodeLanguage.javascript)
expect(result).toEqual({
result: {
type: VarType.string,
children: null,
},
status: {
type: VarType.string,
children: null,
},
})
})
test('handles nested object structure', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.nestedObject, CodeLanguage.javascript)
expect(result).toEqual({
personal_info: {
type: VarType.string,
children: null,
},
timestamp: {
type: VarType.string,
children: null,
},
status: {
type: VarType.string,
children: null,
},
})
})
})
})

View File

@ -0,0 +1,86 @@
import { VarType } from '../../types'
import type { OutputVar } from './types'
import { CodeLanguage } from './types'
export const extractFunctionParams = (code: string, language: CodeLanguage) => {
if (language === CodeLanguage.json)
return []
const patterns: Record<Exclude<CodeLanguage, CodeLanguage.json>, RegExp> = {
[CodeLanguage.python3]: /def\s+main\s*\((.*?)\)/,
[CodeLanguage.javascript]: /function\s+main\s*\((.*?)\)/,
}
const match = code.match(patterns[language])
const params: string[] = []
if (match?.[1]) {
params.push(...match[1].split(',')
.map(p => p.trim())
.filter(Boolean)
.map(p => p.split(':')[0].trim()),
)
}
return params
}
export const extractReturnType = (code: string, language: CodeLanguage): OutputVar => {
const codeWithoutComments = code.replace(/\/\*\*[\s\S]*?\*\//, '')
console.log(codeWithoutComments)
const returnIndex = codeWithoutComments.indexOf('return')
if (returnIndex === -1)
return {}
// returnから始まる部分文字列を取得
const codeAfterReturn = codeWithoutComments.slice(returnIndex)
let bracketCount = 0
let startIndex = codeAfterReturn.indexOf('{')
if (language === CodeLanguage.javascript && startIndex === -1) {
const parenStart = codeAfterReturn.indexOf('(')
if (parenStart !== -1)
startIndex = codeAfterReturn.indexOf('{', parenStart)
}
if (startIndex === -1)
return {}
let endIndex = -1
for (let i = startIndex; i < codeAfterReturn.length; i++) {
if (codeAfterReturn[i] === '{')
bracketCount++
if (codeAfterReturn[i] === '}') {
bracketCount--
if (bracketCount === 0) {
endIndex = i + 1
break
}
}
}
if (endIndex === -1)
return {}
const returnContent = codeAfterReturn.slice(startIndex + 1, endIndex - 1)
console.log(returnContent)
const result: OutputVar = {}
const keyRegex = /['"]?(\w+)['"]?\s*:(?![^{]*})/g
const matches = returnContent.matchAll(keyRegex)
for (const match of matches) {
console.log(`Found key: "${match[1]}" from match: "${match[0]}"`)
const key = match[1]
result[key] = {
type: VarType.string,
children: null,
}
}
console.log(result)
return result
}

View File

@ -5,6 +5,7 @@ import RemoveEffectVarConfirm from '../_base/components/remove-effect-var-confir
import useConfig from './use-config'
import type { CodeNodeType } from './types'
import { CodeLanguage } from './types'
import { extractFunctionParams, extractReturnType } from './code-parser'
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
import OutputVarList from '@/app/components/workflow/nodes/_base/components/variable/output-var-list'
import AddButton from '@/app/components/base/button/add-button'
@ -12,10 +13,9 @@ import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import type { NodePanelProps } from '@/app/components/workflow/types'
import { type NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
const i18nPrefix = 'workflow.nodes.code'
const codeLanguages = [
@ -38,6 +38,7 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({
readOnly,
inputs,
outputKeyOrders,
handleCodeAndVarsChange,
handleVarListChange,
handleAddVariable,
handleRemoveVariable,
@ -61,6 +62,18 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({
setInputVarValues,
} = useConfig(id, data)
const handleGeneratedCode = (value: string) => {
const params = extractFunctionParams(value, inputs.code_language)
const codeNewInput = params.map((p) => {
return {
variable: p,
value_selector: [],
}
})
const returnTypes = extractReturnType(value, inputs.code_language)
handleCodeAndVarsChange(value, codeNewInput, returnTypes)
}
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
@ -92,6 +105,7 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({
language={inputs.code_language}
value={inputs.code}
onChange={handleCodeChange}
onGenerated={handleGeneratedCode}
showCodeGenerator={true}
/>
</div>

View File

@ -3,7 +3,7 @@ import produce from 'immer'
import useVarList from '../_base/hooks/use-var-list'
import useOutputVarList from '../_base/hooks/use-output-var-list'
import { BlockEnum, VarType } from '../../types'
import type { Var } from '../../types'
import type { Var, Variable } from '../../types'
import { useStore } from '../../store'
import type { CodeNodeType, OutputVar } from './types'
import { CodeLanguage } from './types'
@ -136,7 +136,15 @@ const useConfig = (id: string, payload: CodeNodeType) => {
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
setRunInputData(newPayload)
}, [setRunInputData])
const handleCodeAndVarsChange = useCallback((code: string, inputVariables: Variable[], outputVariables: OutputVar) => {
const newInputs = produce(inputs, (draft) => {
draft.code = code
draft.variables = inputVariables
draft.outputs = outputVariables
})
setInputs(newInputs)
syncOutputKeyOrders(outputVariables)
}, [inputs, setInputs, syncOutputKeyOrders])
return {
readOnly,
inputs,
@ -163,6 +171,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
inputVarValues,
setInputVarValues,
runResult,
handleCodeAndVarsChange,
}
}

View File

@ -78,24 +78,24 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
})
const handleAddCase = useCallback(() => {
const newInputs = produce(inputs, () => {
if (inputs.cases) {
const newInputs = produce(inputs, (draft) => {
if (draft.cases) {
const case_id = uuid4()
inputs.cases.push({
draft.cases.push({
case_id,
logical_operator: LogicalOperator.and,
conditions: [],
})
if (inputs._targetBranches) {
const elseCaseIndex = inputs._targetBranches.findIndex(branch => branch.id === 'false')
if (draft._targetBranches) {
const elseCaseIndex = draft._targetBranches.findIndex(branch => branch.id === 'false')
if (elseCaseIndex > -1) {
inputs._targetBranches = branchNameCorrect([
...inputs._targetBranches.slice(0, elseCaseIndex),
draft._targetBranches = branchNameCorrect([
...draft._targetBranches.slice(0, elseCaseIndex),
{
id: case_id,
name: '',
},
...inputs._targetBranches.slice(elseCaseIndex),
...draft._targetBranches.slice(elseCaseIndex),
])
}
}

View File

@ -1,7 +1,10 @@
import { BlockEnum } from '../../types'
import { BlockEnum, ErrorHandleMode } from '../../types'
import type { NodeDefault } from '../../types'
import type { IterationNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import {
ALL_CHAT_AVAILABLE_BLOCKS,
ALL_COMPLETION_AVAILABLE_BLOCKS,
} from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow'
const nodeDefault: NodeDefault<IterationNodeType> = {
@ -10,25 +13,45 @@ const nodeDefault: NodeDefault<IterationNodeType> = {
iterator_selector: [],
output_selector: [],
_children: [],
_isShowTips: false,
is_parallel: false,
parallel_nums: 10,
error_handle_mode: ErrorHandleMode.Terminated,
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(
type => type !== BlockEnum.End,
)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: IterationNodeType, t: any) {
let errorMessages = ''
if (!errorMessages && (!payload.iterator_selector || payload.iterator_selector.length === 0))
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.iteration.input`) })
if (
!errorMessages
&& (!payload.iterator_selector || payload.iterator_selector.length === 0)
) {
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, {
field: t(`${i18nPrefix}.nodes.iteration.input`),
})
}
if (!errorMessages && (!payload.output_selector || payload.output_selector.length === 0))
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.iteration.output`) })
if (
!errorMessages
&& (!payload.output_selector || payload.output_selector.length === 0)
) {
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, {
field: t(`${i18nPrefix}.nodes.iteration.output`),
})
}
return {
isValid: !errorMessages,

View File

@ -8,12 +8,16 @@ import {
useNodesInitialized,
useViewport,
} from 'reactflow'
import { useTranslation } from 'react-i18next'
import { IterationStartNodeDumb } from '../iteration-start'
import { useNodeIterationInteractions } from './use-interactions'
import type { IterationNodeType } from './types'
import AddBlock from './add-block'
import cn from '@/utils/classnames'
import type { NodeProps } from '@/app/components/workflow/types'
import Toast from '@/app/components/base/toast'
const i18nPrefix = 'workflow.nodes.iteration'
const Node: FC<NodeProps<IterationNodeType>> = ({
id,
@ -22,11 +26,20 @@ const Node: FC<NodeProps<IterationNodeType>> = ({
const { zoom } = useViewport()
const nodesInitialized = useNodesInitialized()
const { handleNodeIterationRerender } = useNodeIterationInteractions()
const { t } = useTranslation()
useEffect(() => {
if (nodesInitialized)
handleNodeIterationRerender(id)
}, [nodesInitialized, id, handleNodeIterationRerender])
if (data.is_parallel && data._isShowTips) {
Toast.notify({
type: 'warning',
message: t(`${i18nPrefix}.answerNodeWarningDesc`),
duration: 5000,
})
data._isShowTips = false
}
}, [nodesInitialized, id, handleNodeIterationRerender, data, t])
return (
<div className={cn(

View File

@ -8,11 +8,17 @@ import VarReferencePicker from '../_base/components/variable/var-reference-picke
import Split from '../_base/components/split'
import ResultPanel from '../../run/result-panel'
import IterationResultPanel from '../../run/iteration-result-panel'
import { MAX_ITERATION_PARALLEL_NUM, MIN_ITERATION_PARALLEL_NUM } from '../../constants'
import type { IterationNodeType } from './types'
import useConfig from './use-config'
import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types'
import { ErrorHandleMode, InputVarType, type NodePanelProps } from '@/app/components/workflow/types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import Switch from '@/app/components/base/switch'
import Select from '@/app/components/base/select'
import Slider from '@/app/components/base/slider'
import Input from '@/app/components/base/input'
import Divider from '@/app/components/base/divider'
const i18nPrefix = 'workflow.nodes.iteration'
@ -21,7 +27,20 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
data,
}) => {
const { t } = useTranslation()
const responseMethod = [
{
value: ErrorHandleMode.Terminated,
name: t(`${i18nPrefix}.ErrorMethod.operationTerminated`),
},
{
value: ErrorHandleMode.ContinueOnError,
name: t(`${i18nPrefix}.ErrorMethod.continueOnError`),
},
{
value: ErrorHandleMode.RemoveAbnormalOutput,
name: t(`${i18nPrefix}.ErrorMethod.removeAbnormalOutput`),
},
]
const {
readOnly,
inputs,
@ -47,6 +66,9 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
setIterator,
iteratorInputKey,
iterationRunResult,
changeParallel,
changeErrorResponseMode,
changeParallelNums,
} = useConfig(id, data)
return (
@ -87,6 +109,39 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
/>
</Field>
</div>
<div className='px-4 pb-2'>
<Field title={t(`${i18nPrefix}.parallelMode`)} tooltip={<div className='w-[230px]'>{t(`${i18nPrefix}.parallelPanelDesc`)}</div>} inline>
<Switch defaultValue={inputs.is_parallel} onChange={changeParallel} />
</Field>
</div>
{
inputs.is_parallel && (<div className='px-4 pb-2'>
<Field title={t(`${i18nPrefix}.MaxParallelismTitle`)} isSubTitle tooltip={<div className='w-[230px]'>{t(`${i18nPrefix}.MaxParallelismDesc`)}</div>}>
<div className='flex row'>
<Input type='number' wrapperClassName='w-18 mr-4 ' max={MAX_ITERATION_PARALLEL_NUM} min={MIN_ITERATION_PARALLEL_NUM} value={inputs.parallel_nums} onChange={(e) => { changeParallelNums(Number(e.target.value)) }} />
<Slider
value={inputs.parallel_nums}
onChange={changeParallelNums}
max={MAX_ITERATION_PARALLEL_NUM}
min={MIN_ITERATION_PARALLEL_NUM}
className=' flex-shrink-0 flex-1 mt-4'
/>
</div>
</Field>
</div>)
}
<div className='px-4 py-2'>
<Divider className='h-[1px]'/>
</div>
<div className='px-4 py-2'>
<Field title={t(`${i18nPrefix}.errorResponseMethod`)} >
<Select items={responseMethod} defaultValue={inputs.error_handle_mode} onSelect={changeErrorResponseMode} allowSearch={false}>
</Select>
</Field>
</div>
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}

View File

@ -1,6 +1,7 @@
import type {
BlockEnum,
CommonNodeType,
ErrorHandleMode,
ValueSelector,
VarType,
} from '@/app/components/workflow/types'
@ -12,4 +13,8 @@ export type IterationNodeType = CommonNodeType & {
iterator_selector: ValueSelector
output_selector: ValueSelector
output_type: VarType // output type.
is_parallel: boolean // open the parallel mode or not
parallel_nums: number // the numbers of parallel
error_handle_mode: ErrorHandleMode // how to handle error in the iteration
_isShowTips: boolean // when answer node in parallel mode iteration show tips
}

View File

@ -8,12 +8,13 @@ import {
useWorkflow,
} from '../../hooks'
import { VarType } from '../../types'
import type { ValueSelector, Var } from '../../types'
import type { ErrorHandleMode, ValueSelector, Var } from '../../types'
import useNodeCrud from '../_base/hooks/use-node-crud'
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils'
import useOneStepRun from '../_base/hooks/use-one-step-run'
import type { IterationNodeType } from './types'
import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import type { Item } from '@/app/components/base/select'
const DELIMITER = '@@@@@'
const useConfig = (id: string, payload: IterationNodeType) => {
@ -184,6 +185,25 @@ const useConfig = (id: string, payload: IterationNodeType) => {
})
}, [iteratorInputKey, runInputData, setRunInputData])
const changeParallel = useCallback((value: boolean) => {
const newInputs = produce(inputs, (draft) => {
draft.is_parallel = value
})
setInputs(newInputs)
}, [inputs, setInputs])
const changeErrorResponseMode = useCallback((item: Item) => {
const newInputs = produce(inputs, (draft) => {
draft.error_handle_mode = item.value as ErrorHandleMode
})
setInputs(newInputs)
}, [inputs, setInputs])
const changeParallelNums = useCallback((num: number) => {
const newInputs = produce(inputs, (draft) => {
draft.parallel_nums = num
})
setInputs(newInputs)
}, [inputs, setInputs])
return {
readOnly,
inputs,
@ -210,6 +230,9 @@ const useConfig = (id: string, payload: IterationNodeType) => {
setIterator,
iteratorInputKey,
iterationRunResult,
changeParallel,
changeErrorResponseMode,
changeParallelNums,
}
}

View File

@ -240,7 +240,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
if (
(allInternal && (mixtureHighQualityAndEconomic || inconsistentEmbeddingModel))
|| mixtureInternalAndExternal
|| (allExternal && newDatasets.length > 1)
|| allExternal
)
setRerankModelOpen(true)
}, [inputs, setInputs, payload.retrieval_mode, selectedDatasets, currentRerankModel])

View File

@ -9,6 +9,8 @@ import { produce, setAutoFreeze } from 'immer'
import { uniqBy } from 'lodash-es'
import { useWorkflowRun } from '../../hooks'
import { NodeRunningStatus, WorkflowRunningStatus } from '../../types'
import { useWorkflowStore } from '../../store'
import { DEFAULT_ITER_TIMES } from '../../constants'
import type {
ChatItem,
Inputs,
@ -43,6 +45,7 @@ export const useChat = (
const { notify } = useToastContext()
const { handleRun } = useWorkflowRun()
const hasStopResponded = useRef(false)
const workflowStore = useWorkflowStore()
const conversationId = useRef('')
const taskIdRef = useRef('')
const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || [])
@ -52,6 +55,9 @@ export const useChat = (
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
const {
setIterTimes,
} = workflowStore.getState()
useEffect(() => {
setAutoFreeze(false)
return () => {
@ -102,15 +108,16 @@ export const useChat = (
handleResponding(false)
if (stopChat && taskIdRef.current)
stopChat(taskIdRef.current)
setIterTimes(DEFAULT_ITER_TIMES)
if (suggestedQuestionsAbortControllerRef.current)
suggestedQuestionsAbortControllerRef.current.abort()
}, [handleResponding, stopChat])
}, [handleResponding, setIterTimes, stopChat])
const handleRestart = useCallback(() => {
conversationId.current = ''
taskIdRef.current = ''
handleStop()
setIterTimes(DEFAULT_ITER_TIMES)
const newChatList = config?.opening_statement
? [{
id: `${Date.now()}`,
@ -126,6 +133,7 @@ export const useChat = (
config,
handleStop,
handleUpdateChatList,
setIterTimes,
])
const updateCurrentQA = useCallback(({

View File

@ -60,36 +60,67 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
}, [notify, getResultCallback])
const formatNodeList = useCallback((list: NodeTracing[]) => {
const allItems = list.reverse()
const allItems = [...list].reverse()
const result: NodeTracing[] = []
allItems.forEach((item) => {
const { node_type, execution_metadata } = item
if (node_type !== BlockEnum.Iteration) {
const isInIteration = !!execution_metadata?.iteration_id
const groupMap = new Map<string, NodeTracing[]>()
if (isInIteration) {
const iterationNode = result.find(node => node.node_id === execution_metadata?.iteration_id)
const iterationDetails = iterationNode?.details
const currentIterationIndex = execution_metadata?.iteration_index ?? 0
if (Array.isArray(iterationDetails)) {
if (iterationDetails.length === 0 || !iterationDetails[currentIterationIndex])
iterationDetails[currentIterationIndex] = [item]
else
iterationDetails[currentIterationIndex].push(item)
}
return
}
// not in iteration
result.push(item)
return
}
const processIterationNode = (item: NodeTracing) => {
result.push({
...item,
details: [],
})
}
const updateParallelModeGroup = (runId: string, item: NodeTracing, iterationNode: NodeTracing) => {
if (!groupMap.has(runId))
groupMap.set(runId, [item])
else
groupMap.get(runId)!.push(item)
if (item.status === 'failed') {
iterationNode.status = 'failed'
iterationNode.error = item.error
}
iterationNode.details = Array.from(groupMap.values())
}
const updateSequentialModeGroup = (index: number, item: NodeTracing, iterationNode: NodeTracing) => {
const { details } = iterationNode
if (details) {
if (!details[index])
details[index] = [item]
else
details[index].push(item)
}
if (item.status === 'failed') {
iterationNode.status = 'failed'
iterationNode.error = item.error
}
}
const processNonIterationNode = (item: NodeTracing) => {
const { execution_metadata } = item
if (!execution_metadata?.iteration_id) {
result.push(item)
return
}
const iterationNode = result.find(node => node.node_id === execution_metadata.iteration_id)
if (!iterationNode || !Array.isArray(iterationNode.details))
return
const { parallel_mode_run_id, iteration_index = 0 } = execution_metadata
if (parallel_mode_run_id)
updateParallelModeGroup(parallel_mode_run_id, item, iterationNode)
else
updateSequentialModeGroup(iteration_index, item, iterationNode)
}
allItems.forEach((item) => {
item.node_type === BlockEnum.Iteration
? processIterationNode(item)
: processNonIterationNode(item)
})
return result
}, [])

View File

@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
RiCloseLine,
RiErrorWarningLine,
} from '@remixicon/react'
import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows'
import TracingPanel from './tracing-panel'
@ -27,7 +28,7 @@ const IterationResultPanel: FC<Props> = ({
noWrap,
}) => {
const { t } = useTranslation()
const [expandedIterations, setExpandedIterations] = useState<Record<number, boolean>>([])
const [expandedIterations, setExpandedIterations] = useState<Record<number, boolean>>({})
const toggleIteration = useCallback((index: number) => {
setExpandedIterations(prev => ({
@ -71,10 +72,19 @@ const IterationResultPanel: FC<Props> = ({
<span className='system-sm-semibold-uppercase text-text-primary flex-grow'>
{t(`${i18nPrefix}.iteration`)} {index + 1}
</span>
<RiArrowRightSLine className={cn(
'w-4 h-4 text-text-tertiary transition-transform duration-200 flex-shrink-0',
expandedIterations[index] && 'transform rotate-90',
)} />
{
iteration.some(item => item.status === 'failed')
? (
<RiErrorWarningLine className='w-4 h-4 text-text-destructive' />
)
: (< RiArrowRightSLine className={
cn(
'w-4 h-4 text-text-tertiary transition-transform duration-200 flex-shrink-0',
expandedIterations[index] && 'transform rotate-90',
)} />
)
}
</div>
</div>
{expandedIterations[index] && <div

View File

@ -72,7 +72,16 @@ const NodePanel: FC<Props> = ({
return iteration_length
}
const getErrorCount = (details: NodeTracing[][] | undefined) => {
if (!details || details.length === 0)
return 0
return details.reduce((acc, iteration) => {
if (iteration.some(item => item.status === 'failed'))
acc++
return acc
}, 0)
}
useEffect(() => {
setCollapseState(!nodeInfo.expand)
}, [nodeInfo.expand, setCollapseState])
@ -136,7 +145,12 @@ const NodePanel: FC<Props> = ({
onClick={handleOnShowIterationDetail}
>
<Iteration className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
<div className='flex-1 text-left system-sm-medium text-components-button-tertiary-text'>{t('workflow.nodes.iteration.iteration', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.iterator_length) })}</div>
<div className='flex-1 text-left system-sm-medium text-components-button-tertiary-text'>{t('workflow.nodes.iteration.iteration', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.iterator_length) })}{getErrorCount(nodeInfo.details) > 0 && (
<>
{t('workflow.nodes.iteration.comma')}
{t('workflow.nodes.iteration.error', { count: getErrorCount(nodeInfo.details) })}
</>
)}</div>
{justShowIterationNavArrow
? (
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />

View File

@ -21,6 +21,7 @@ import type {
WorkflowRunningData,
} from './types'
import { WorkflowContext } from './context'
import type { NodeTracing } from '@/types/workflow'
// #TODO chatVar#
// const MOCK_DATA = [
@ -166,6 +167,10 @@ interface Shape {
setShowImportDSLModal: (showImportDSLModal: boolean) => void
showTips: string
setShowTips: (showTips: string) => void
iterTimes: number
setIterTimes: (iterTimes: number) => void
iterParallelLogMap: Map<string, NodeTracing[]>
setIterParallelLogMap: (iterParallelLogMap: Map<string, NodeTracing[]>) => void
}
export const createWorkflowStore = () => {
@ -281,6 +286,11 @@ export const createWorkflowStore = () => {
setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })),
showTips: '',
setShowTips: showTips => set(() => ({ showTips })),
iterTimes: 1,
setIterTimes: iterTimes => set(() => ({ iterTimes })),
iterParallelLogMap: new Map<string, NodeTracing[]>(),
setIterParallelLogMap: iterParallelLogMap => set(() => ({ iterParallelLogMap })),
}))
}

View File

@ -37,7 +37,12 @@ export enum ControlMode {
Hand = 'hand',
}
export interface Branch {
export enum ErrorHandleMode {
Terminated = 'terminated',
ContinueOnError = 'continue-on-error',
RemoveAbnormalOutput = 'remove-abnormal-output',
}
export type Branch = {
id: string
name: string
}

View File

@ -19,7 +19,7 @@ import type {
ToolWithProvider,
ValueSelector,
} from './types'
import { BlockEnum } from './types'
import { BlockEnum, ErrorHandleMode } from './types'
import {
CUSTOM_NODE,
ITERATION_CHILDREN_Z_INDEX,
@ -267,8 +267,13 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
})
}
if (node.data.type === BlockEnum.Iteration)
node.data._children = iterationNodeMap[node.id] || []
if (node.data.type === BlockEnum.Iteration) {
const iterationNodeData = node.data as IterationNodeType
iterationNodeData._children = iterationNodeMap[node.id] || []
iterationNodeData.is_parallel = iterationNodeData.is_parallel || false
iterationNodeData.parallel_nums = iterationNodeData.parallel_nums || 10
iterationNodeData.error_handle_mode = iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated
}
return node
})

View File

@ -12,11 +12,9 @@ import cn from '@/utils/classnames'
import { getSystemFeatures, invitationCheck } from '@/service/common'
import { defaultSystemFeatures } from '@/types/feature'
import Toast from '@/app/components/base/toast'
import useRefreshToken from '@/hooks/use-refresh-token'
import { IS_CE_EDITION } from '@/config'
const NormalForm = () => {
const { getNewAccessToken } = useRefreshToken()
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
@ -38,7 +36,6 @@ const NormalForm = () => {
if (consoleToken && refreshToken) {
localStorage.setItem('console_token', consoleToken)
localStorage.setItem('refresh_token', refreshToken)
getNewAccessToken()
router.replace('/apps')
return
}
@ -71,7 +68,7 @@ const NormalForm = () => {
setSystemFeatures(defaultSystemFeatures)
}
finally { setIsLoading(false) }
}, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, getNewAccessToken])
}, [consoleToken, refreshToken, message, router, invite_token, isInviteLink])
useEffect(() => {
init()
}, [init])