Merge branch 'langgenius:main' into feat/17150

pull/18136/head
yourchanges 1 year ago committed by GitHub
commit 55e015a915
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -2,10 +2,10 @@
npm add -g pnpm@10.8.0 npm add -g pnpm@10.8.0
cd web && pnpm install cd web && pnpm install
pipx install poetry pipx install uv
echo 'alias start-api="cd /workspaces/dify/api && poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc echo 'alias start-api="cd /workspaces/dify/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc echo 'alias start-worker="cd /workspaces/dify/api && uv run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d"' >> ~/.bashrc echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d"' >> ~/.bashrc
echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc

@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
cd api && poetry install cd api && uv sync

@ -1,36 +0,0 @@
name: Setup Poetry and Python
inputs:
python-version:
description: Python version to use and the Poetry installed with
required: true
default: '3.11'
poetry-version:
description: Poetry version to set up
required: true
default: '2.0.1'
poetry-lockfile:
description: Path to the Poetry lockfile to restore cache from
required: true
default: ''
runs:
using: composite
steps:
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: pip
- name: Install Poetry
shell: bash
run: pip install poetry==${{ inputs.poetry-version }}
- name: Restore Poetry cache
if: ${{ inputs.poetry-lockfile != '' }}
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: poetry
cache-dependency-path: ${{ inputs.poetry-lockfile }}

@ -0,0 +1,34 @@
name: Setup UV and Python
inputs:
python-version:
description: Python version to use and the UV installed with
required: true
default: '3.12'
uv-version:
description: UV version to set up
required: true
default: '0.6.14'
uv-lockfile:
description: Path to the UV lockfile to restore cache from
required: true
default: ''
enable-cache:
required: true
default: true
runs:
using: composite
steps:
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: ${{ inputs.uv-version }}
python-version: ${{ inputs.python-version }}
enable-cache: ${{ inputs.enable-cache }}
cache-dependency-glob: ${{ inputs.uv-lockfile }}

@ -17,6 +17,9 @@ jobs:
test: test:
name: API Tests name: API Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults:
run:
shell: bash
strategy: strategy:
matrix: matrix:
python-version: python-version:
@ -30,37 +33,32 @@ jobs:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Setup Poetry and Python ${{ matrix.python-version }} - name: Setup UV and Python
uses: ./.github/actions/setup-poetry uses: ./.github/actions/setup-uv
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
poetry-lockfile: api/poetry.lock uv-lockfile: api/uv.lock
- name: Check Poetry lockfile - name: Check UV lockfile
run: | run: uv lock --project api --check
poetry check -C api --lock
poetry show -C api
- name: Install dependencies - name: Install dependencies
run: poetry install -C api --with dev run: uv sync --project api --group dev
- name: Check dependencies in pyproject.toml
run: poetry run -P api bash dev/pytest/pytest_artifacts.sh
- name: Run Unit tests - name: Run Unit tests
run: poetry run -P api bash dev/pytest/pytest_unit_tests.sh run: uv run --project api bash dev/pytest/pytest_unit_tests.sh
- name: Run dify config tests - name: Run dify config tests
run: poetry run -P api python dev/pytest/pytest_config_tests.py run: uv run --project api dev/pytest/pytest_config_tests.py
- name: Cache MyPy - name: MyPy Cache
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: api/.mypy_cache path: api/.mypy_cache
key: mypy-${{ matrix.python-version }}-${{ runner.os }}-${{ hashFiles('api/poetry.lock') }} key: mypy-${{ matrix.python-version }}-${{ runner.os }}-${{ hashFiles('api/uv.lock') }}
- name: Run mypy - name: Run MyPy Checks
run: dev/run-mypy run: dev/mypy-check
- name: Set up dotenvs - name: Set up dotenvs
run: | run: |
@ -80,4 +78,4 @@ jobs:
ssrf_proxy ssrf_proxy
- name: Run Workflow - name: Run Workflow
run: poetry run -P api bash dev/pytest/pytest_workflow.sh run: uv run --project api bash dev/pytest/pytest_workflow.sh

@ -24,13 +24,13 @@ jobs:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Setup Poetry and Python - name: Setup UV and Python
uses: ./.github/actions/setup-poetry uses: ./.github/actions/setup-uv
with: with:
poetry-lockfile: api/poetry.lock uv-lockfile: api/uv.lock
- name: Install dependencies - name: Install dependencies
run: poetry install -C api run: uv sync --project api
- name: Prepare middleware env - name: Prepare middleware env
run: | run: |
@ -54,6 +54,4 @@ jobs:
- name: Run DB Migration - name: Run DB Migration
env: env:
DEBUG: true DEBUG: true
run: | run: uv run --directory api flask upgrade-db
cd api
poetry run python -m flask upgrade-db

@ -42,6 +42,7 @@ jobs:
with: with:
push: false push: false
context: "{{defaultContext}}:${{ matrix.context }}" context: "{{defaultContext}}:${{ matrix.context }}"
file: "${{ matrix.file }}"
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

@ -29,24 +29,27 @@ jobs:
api/** api/**
.github/workflows/style.yml .github/workflows/style.yml
- name: Setup Poetry and Python - name: Setup UV and Python
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/actions/setup-poetry uses: ./.github/actions/setup-uv
with:
uv-lockfile: api/uv.lock
enable-cache: false
- name: Install dependencies - name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: poetry install -C api --only lint run: uv sync --project api --only-group lint
- name: Ruff check - name: Ruff check
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: | run: |
poetry run -C api ruff --version uv run --directory api ruff --version
poetry run -C api ruff check ./ uv run --directory api ruff check ./
poetry run -C api ruff format --check ./ uv run --directory api ruff format --check ./
- name: Dotenv check - name: Dotenv check
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: poetry run -P api dotenv-linter ./api/.env.example ./web/.env.example run: uv run --project api dotenv-linter ./api/.env.example ./web/.env.example
- name: Lint hints - name: Lint hints
if: failure() if: failure()

@ -8,7 +8,7 @@ on:
- api/core/rag/datasource/** - api/core/rag/datasource/**
- docker/** - docker/**
- .github/workflows/vdb-tests.yml - .github/workflows/vdb-tests.yml
- api/poetry.lock - api/uv.lock
- api/pyproject.toml - api/pyproject.toml
concurrency: concurrency:
@ -32,19 +32,17 @@ jobs:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Setup Poetry and Python ${{ matrix.python-version }} - name: Setup UV and Python
uses: ./.github/actions/setup-poetry uses: ./.github/actions/setup-uv
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
poetry-lockfile: api/poetry.lock uv-lockfile: api/uv.lock
- name: Check Poetry lockfile - name: Check UV lockfile
run: | run: uv lock --project api --check
poetry check -C api --lock
poetry show -C api
- name: Install dependencies - name: Install dependencies
run: poetry install -C api --with dev run: uv sync --project api --group dev
- name: Set up dotenvs - name: Set up dotenvs
run: | run: |
@ -80,7 +78,7 @@ jobs:
elasticsearch elasticsearch
- name: Check TiDB Ready - name: Check TiDB Ready
run: poetry run -P api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
- name: Test Vector Stores - name: Test Vector Stores
run: poetry run -P api bash dev/pytest/pytest_vdb.sh run: uv run --project api bash dev/pytest/pytest_vdb.sh

@ -3,20 +3,11 @@ FROM python:3.12-slim-bookworm AS base
WORKDIR /app/api WORKDIR /app/api
# Install Poetry # Install uv
ENV POETRY_VERSION=2.0.1 ENV UV_VERSION=0.6.14
# if you located in China, you can use aliyun mirror to speed up RUN pip install --no-cache-dir uv==${UV_VERSION}
# RUN pip install --no-cache-dir poetry==${POETRY_VERSION} -i https://mirrors.aliyun.com/pypi/simple/
RUN pip install --no-cache-dir poetry==${POETRY_VERSION}
# Configure Poetry
ENV POETRY_CACHE_DIR=/tmp/poetry_cache
ENV POETRY_NO_INTERACTION=1
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
ENV POETRY_VIRTUALENVS_CREATE=true
ENV POETRY_REQUESTS_TIMEOUT=15
FROM base AS packages FROM base AS packages
@ -27,8 +18,8 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc g++ libc-dev libffi-dev libgmp-dev libmpfr-dev libmpc-dev && apt-get install -y --no-install-recommends gcc g++ libc-dev libffi-dev libgmp-dev libmpfr-dev libmpc-dev
# Install Python dependencies # Install Python dependencies
COPY pyproject.toml poetry.lock ./ COPY pyproject.toml uv.lock ./
RUN poetry install --sync --no-cache --no-root RUN uv sync --locked
# production stage # production stage
FROM base AS production FROM base AS production

@ -3,7 +3,10 @@
## Usage ## Usage
> [!IMPORTANT] > [!IMPORTANT]
> In the v0.6.12 release, we deprecated `pip` as the package management tool for Dify API Backend service and replaced it with `poetry`. >
> In the v1.3.0 release, `poetry` has been replaced with
> [`uv`](https://docs.astral.sh/uv/) as the package manager
> for Dify API backend service.
1. Start the docker-compose stack 1. Start the docker-compose stack
@ -37,19 +40,19 @@
4. Create environment. 4. Create environment.
Dify API service uses [Poetry](https://python-poetry.org/docs/) to manage dependencies. First, you need to add the poetry shell plugin, if you don't have it already, in order to run in a virtual environment. [Note: Poetry shell is no longer a native command so you need to install the poetry plugin beforehand] Dify API service uses [UV](https://docs.astral.sh/uv/) to manage dependencies.
First, you need to add the uv package manager, if you don't have it already.
```bash ```bash
poetry self add poetry-plugin-shell pip install uv
# Or on macOS
brew install uv
``` ```
Then, You can execute `poetry shell` to activate the environment.
5. Install dependencies 5. Install dependencies
```bash ```bash
poetry env use 3.12 uv sync --group lint --group dev
poetry install
``` ```
6. Run migrate 6. Run migrate
@ -57,21 +60,21 @@
Before the first launch, migrate the database to the latest version. Before the first launch, migrate the database to the latest version.
```bash ```bash
poetry run python -m flask db upgrade uv run flask db upgrade
``` ```
7. Start backend 7. Start backend
```bash ```bash
poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug uv run flask run --host 0.0.0.0 --port=5001 --debug
``` ```
8. Start Dify [web](../web) service. 8. Start Dify [web](../web) service.
9. Setup your application by visiting `http://localhost:3000`... 9. Setup your application by visiting `http://localhost:3000`.
10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service. 10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service.
```bash ```bash
poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion
``` ```
## Testing ## Testing
@ -79,11 +82,11 @@
1. Install dependencies for both the backend and the test environment 1. Install dependencies for both the backend and the test environment
```bash ```bash
poetry install -C api --with dev uv sync --group lint --group dev
``` ```
2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml` 2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml`
```bash ```bash
poetry run -P api bash dev/pytest/pytest_all_tests.sh uv run -P api bash dev/pytest/pytest_all_tests.sh
``` ```

@ -89,7 +89,7 @@ class AnnotationReplyActionStatusApi(Resource):
app_annotation_job_key = "{}_app_annotation_job_{}".format(action, str(job_id)) app_annotation_job_key = "{}_app_annotation_job_{}".format(action, str(job_id))
cache_result = redis_client.get(app_annotation_job_key) cache_result = redis_client.get(app_annotation_job_key)
if cache_result is None: if cache_result is None:
raise ValueError("The job is not exist.") raise ValueError("The job does not exist.")
job_status = cache_result.decode() job_status = cache_result.decode()
error_msg = "" error_msg = ""
@ -226,7 +226,7 @@ class AnnotationBatchImportStatusApi(Resource):
indexing_cache_key = "app_annotation_batch_import_{}".format(str(job_id)) indexing_cache_key = "app_annotation_batch_import_{}".format(str(job_id))
cache_result = redis_client.get(indexing_cache_key) cache_result = redis_client.get(indexing_cache_key)
if cache_result is None: if cache_result is None:
raise ValueError("The job is not exist.") raise ValueError("The job does not exist.")
job_status = cache_result.decode() job_status = cache_result.decode()
error_msg = "" error_msg = ""
if job_status == "error": if job_status == "error":

@ -398,7 +398,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
indexing_cache_key = "segment_batch_import_{}".format(job_id) indexing_cache_key = "segment_batch_import_{}".format(job_id)
cache_result = redis_client.get(indexing_cache_key) cache_result = redis_client.get(indexing_cache_key)
if cache_result is None: if cache_result is None:
raise ValueError("The job is not exist.") raise ValueError("The job does not exist.")
return {"job_id": job_id, "job_status": cache_result.decode()}, 200 return {"job_id": job_id, "job_status": cache_result.decode()}, 200

@ -249,6 +249,31 @@ class PluginInstallFromMarketplaceApi(Resource):
return jsonable_encoder(response) return jsonable_encoder(response)
class PluginFetchMarketplacePkgApi(Resource):
@setup_required
@login_required
@account_initialization_required
@plugin_permission_required(install_required=True)
def get(self):
tenant_id = current_user.current_tenant_id
parser = reqparse.RequestParser()
parser.add_argument("plugin_unique_identifier", type=str, required=True, location="args")
args = parser.parse_args()
try:
return jsonable_encoder(
{
"manifest": PluginService.fetch_marketplace_pkg(
tenant_id,
args["plugin_unique_identifier"],
)
}
)
except PluginDaemonClientSideError as e:
raise ValueError(e)
class PluginFetchManifestApi(Resource): class PluginFetchManifestApi(Resource):
@setup_required @setup_required
@login_required @login_required
@ -488,6 +513,7 @@ api.add_resource(PluginDeleteInstallTaskApi, "/workspaces/current/plugin/tasks/<
api.add_resource(PluginDeleteAllInstallTaskItemsApi, "/workspaces/current/plugin/tasks/delete_all") api.add_resource(PluginDeleteAllInstallTaskItemsApi, "/workspaces/current/plugin/tasks/delete_all")
api.add_resource(PluginDeleteInstallTaskItemApi, "/workspaces/current/plugin/tasks/<task_id>/delete/<path:identifier>") api.add_resource(PluginDeleteInstallTaskItemApi, "/workspaces/current/plugin/tasks/<task_id>/delete/<path:identifier>")
api.add_resource(PluginUninstallApi, "/workspaces/current/plugin/uninstall") api.add_resource(PluginUninstallApi, "/workspaces/current/plugin/uninstall")
api.add_resource(PluginFetchMarketplacePkgApi, "/workspaces/current/plugin/marketplace/pkg")
api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change") api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change")
api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch")

@ -13,6 +13,7 @@ from fields.dataset_fields import dataset_detail_fields
from libs.login import current_user from libs.login import current_user
from models.dataset import Dataset, DatasetPermissionEnum from models.dataset import Dataset, DatasetPermissionEnum
from services.dataset_service import DatasetPermissionService, DatasetService from services.dataset_service import DatasetPermissionService, DatasetService
from services.entities.knowledge_entities.knowledge_entities import RetrievalModel
def _validate_name(name): def _validate_name(name):
@ -120,8 +121,11 @@ class DatasetListApi(DatasetApiResource):
nullable=True, nullable=True,
required=False, required=False,
) )
args = parser.parse_args() parser.add_argument("retrieval_model", type=dict, required=False, nullable=True, location="json")
parser.add_argument("embedding_model", type=str, required=False, nullable=True, location="json")
parser.add_argument("embedding_model_provider", type=str, required=False, nullable=True, location="json")
args = parser.parse_args()
try: try:
dataset = DatasetService.create_empty_dataset( dataset = DatasetService.create_empty_dataset(
tenant_id=tenant_id, tenant_id=tenant_id,
@ -133,6 +137,9 @@ class DatasetListApi(DatasetApiResource):
provider=args["provider"], provider=args["provider"],
external_knowledge_api_id=args["external_knowledge_api_id"], external_knowledge_api_id=args["external_knowledge_api_id"],
external_knowledge_id=args["external_knowledge_id"], external_knowledge_id=args["external_knowledge_id"],
embedding_model_provider=args["embedding_model_provider"],
embedding_model_name=args["embedding_model"],
retrieval_model=RetrievalModel(**args["retrieval_model"]),
) )
except services.errors.dataset.DatasetNameDuplicateError: except services.errors.dataset.DatasetNameDuplicateError:
raise DatasetNameDuplicateError() raise DatasetNameDuplicateError()

@ -49,7 +49,9 @@ class DocumentAddByTextApi(DatasetApiResource):
parser.add_argument( parser.add_argument(
"indexing_technique", type=str, choices=Dataset.INDEXING_TECHNIQUE_LIST, nullable=False, location="json" "indexing_technique", type=str, choices=Dataset.INDEXING_TECHNIQUE_LIST, nullable=False, location="json"
) )
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json") parser.add_argument("retrieval_model", type=dict, required=False, nullable=True, location="json")
parser.add_argument("embedding_model", type=str, required=False, nullable=True, location="json")
parser.add_argument("embedding_model_provider", type=str, required=False, nullable=True, location="json")
args = parser.parse_args() args = parser.parse_args()
dataset_id = str(dataset_id) dataset_id = str(dataset_id)
@ -57,7 +59,7 @@ class DocumentAddByTextApi(DatasetApiResource):
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first() dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset: if not dataset:
raise ValueError("Dataset is not exist.") raise ValueError("Dataset does not exist.")
if not dataset.indexing_technique and not args["indexing_technique"]: if not dataset.indexing_technique and not args["indexing_technique"]:
raise ValueError("indexing_technique is required.") raise ValueError("indexing_technique is required.")
@ -114,7 +116,7 @@ class DocumentUpdateByTextApi(DatasetApiResource):
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first() dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset: if not dataset:
raise ValueError("Dataset is not exist.") raise ValueError("Dataset does not exist.")
# indexing_technique is already set in dataset since this is an update # indexing_technique is already set in dataset since this is an update
args["indexing_technique"] = dataset.indexing_technique args["indexing_technique"] = dataset.indexing_technique
@ -172,7 +174,7 @@ class DocumentAddByFileApi(DatasetApiResource):
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first() dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset: if not dataset:
raise ValueError("Dataset is not exist.") raise ValueError("Dataset does not exist.")
if not dataset.indexing_technique and not args.get("indexing_technique"): if not dataset.indexing_technique and not args.get("indexing_technique"):
raise ValueError("indexing_technique is required.") raise ValueError("indexing_technique is required.")
@ -239,7 +241,7 @@ class DocumentUpdateByFileApi(DatasetApiResource):
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first() dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset: if not dataset:
raise ValueError("Dataset is not exist.") raise ValueError("Dataset does not exist.")
# indexing_technique is already set in dataset since this is an update # indexing_technique is already set in dataset since this is an update
args["indexing_technique"] = dataset.indexing_technique args["indexing_technique"] = dataset.indexing_technique
@ -303,7 +305,7 @@ class DocumentDeleteApi(DatasetApiResource):
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first() dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset: if not dataset:
raise ValueError("Dataset is not exist.") raise ValueError("Dataset does not exist.")
document = DocumentService.get_document(dataset.id, document_id) document = DocumentService.get_document(dataset.id, document_id)

@ -191,7 +191,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
# action is final answer, return final answer directly # action is final answer, return final answer directly
try: try:
if isinstance(scratchpad.action.action_input, dict): if isinstance(scratchpad.action.action_input, dict):
final_answer = json.dumps(scratchpad.action.action_input) final_answer = json.dumps(scratchpad.action.action_input, ensure_ascii=False)
elif isinstance(scratchpad.action.action_input, str): elif isinstance(scratchpad.action.action_input, str):
final_answer = scratchpad.action.action_input final_answer = scratchpad.action.action_input
else: else:

@ -153,6 +153,7 @@ class MessageBasedAppGenerator(BaseAppGenerator):
query = application_generate_entity.query or "New conversation" query = application_generate_entity.query or "New conversation"
else: else:
query = next(iter(application_generate_entity.inputs.values()), "New conversation") query = next(iter(application_generate_entity.inputs.values()), "New conversation")
query = query or "New conversation"
conversation_name = (query[:20] + "") if len(query) > 20 else query conversation_name = (query[:20] + "") if len(query) > 20 else query
if not conversation: if not conversation:

@ -453,7 +453,7 @@ class TraceTask:
"version": workflow_run_version, "version": workflow_run_version,
"total_tokens": total_tokens, "total_tokens": total_tokens,
"file_list": file_list, "file_list": file_list,
"triggered_form": workflow_run.triggered_from, "triggered_from": workflow_run.triggered_from,
"user_id": user_id, "user_id": user_id,
} }

@ -131,7 +131,7 @@ def cast_parameter_value(typ: enum.StrEnum, value: Any, /):
raise ValueError("The selector must be a dictionary.") raise ValueError("The selector must be a dictionary.")
return value return value
case PluginParameterType.TOOLS_SELECTOR: case PluginParameterType.TOOLS_SELECTOR:
if not isinstance(value, list): if value and not isinstance(value, list):
raise ValueError("The tools selector must be a list.") raise ValueError("The tools selector must be a list.")
return value return value
case _: case _:
@ -147,7 +147,7 @@ def init_frontend_parameter(rule: PluginParameter, type: enum.StrEnum, value: An
init frontend parameter by rule init frontend parameter by rule
""" """
parameter_value = value parameter_value = value
if not parameter_value and parameter_value != 0 and type != PluginParameterType.TOOLS_SELECTOR: if not parameter_value and parameter_value != 0:
# get default value # get default value
parameter_value = rule.default parameter_value = rule.default
if not parameter_value and rule.required: if not parameter_value and rule.required:

@ -70,6 +70,9 @@ class PluginDeclaration(BaseModel):
models: Optional[list[str]] = Field(default_factory=list) models: Optional[list[str]] = Field(default_factory=list)
endpoints: Optional[list[str]] = Field(default_factory=list) endpoints: Optional[list[str]] = Field(default_factory=list)
class Meta(BaseModel):
minimum_dify_version: Optional[str] = Field(default=None, pattern=r"^\d{1,4}(\.\d{1,4}){1,3}(-\w{1,16})?$")
version: str = Field(..., pattern=r"^\d{1,4}(\.\d{1,4}){1,3}(-\w{1,16})?$") version: str = Field(..., pattern=r"^\d{1,4}(\.\d{1,4}){1,3}(-\w{1,16})?$")
author: Optional[str] = Field(..., pattern=r"^[a-zA-Z0-9_-]{1,64}$") author: Optional[str] = Field(..., pattern=r"^[a-zA-Z0-9_-]{1,64}$")
name: str = Field(..., pattern=r"^[a-z0-9_-]{1,128}$") name: str = Field(..., pattern=r"^[a-z0-9_-]{1,128}$")
@ -86,6 +89,7 @@ class PluginDeclaration(BaseModel):
model: Optional[ProviderEntity] = None model: Optional[ProviderEntity] = None
endpoint: Optional[EndpointProviderDeclaration] = None endpoint: Optional[EndpointProviderDeclaration] = None
agent_strategy: Optional[AgentStrategyProviderEntity] = None agent_strategy: Optional[AgentStrategyProviderEntity] = None
meta: Meta
@model_validator(mode="before") @model_validator(mode="before")
@classmethod @classmethod

@ -139,6 +139,7 @@ class AnalyticdbVectorBySql:
) )
if embedding_dimension is not None: if embedding_dimension is not None:
index_name = f"{self._collection_name}_embedding_idx" index_name = f"{self._collection_name}_embedding_idx"
try:
cur.execute(f"ALTER TABLE {self.table_name} ALTER COLUMN vector SET STORAGE PLAIN") cur.execute(f"ALTER TABLE {self.table_name} ALTER COLUMN vector SET STORAGE PLAIN")
cur.execute( cur.execute(
f"CREATE INDEX {index_name} ON {self.table_name} USING ann(vector) " f"CREATE INDEX {index_name} ON {self.table_name} USING ann(vector) "
@ -146,6 +147,9 @@ class AnalyticdbVectorBySql:
f"pq_enable=0, external_storage=0)" f"pq_enable=0, external_storage=0)"
) )
cur.execute(f"CREATE INDEX ON {self.table_name} USING gin(to_tsvector)") cur.execute(f"CREATE INDEX ON {self.table_name} USING gin(to_tsvector)")
except Exception as e:
if "already exists" not in str(e):
raise e
redis_client.set(collection_exist_cache_key, 1, ex=3600) redis_client.set(collection_exist_cache_key, 1, ex=3600)
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs): def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
@ -177,9 +181,11 @@ class AnalyticdbVectorBySql:
return cur.fetchone() is not None return cur.fetchone() is not None
def delete_by_ids(self, ids: list[str]) -> None: def delete_by_ids(self, ids: list[str]) -> None:
if not ids:
return
with self._get_cursor() as cur: with self._get_cursor() as cur:
try: try:
cur.execute(f"DELETE FROM {self.table_name} WHERE ref_doc_id IN %s", (tuple(ids),)) cur.execute(f"DELETE FROM {self.table_name} WHERE ref_doc_id = ANY(%s)", (ids,))
except Exception as e: except Exception as e:
if "does not exist" not in str(e): if "does not exist" not in str(e):
raise e raise e
@ -240,7 +246,7 @@ class AnalyticdbVectorBySql:
ts_rank(to_tsvector, to_tsquery_from_text(%s, 'zh_cn'), 32) AS score ts_rank(to_tsvector, to_tsquery_from_text(%s, 'zh_cn'), 32) AS score
FROM {self.table_name} FROM {self.table_name}
WHERE to_tsvector@@to_tsquery_from_text(%s, 'zh_cn') {where_clause} WHERE to_tsvector@@to_tsquery_from_text(%s, 'zh_cn') {where_clause}
ORDER BY score DESC ORDER BY (score,id) DESC
LIMIT {top_k}""", LIMIT {top_k}""",
(f"'{query}'", f"'{query}'"), (f"'{query}'", f"'{query}'"),
) )

@ -444,7 +444,7 @@ class QdrantVectorFactory(AbstractVectorFactory):
if dataset_collection_binding: if dataset_collection_binding:
collection_name = dataset_collection_binding.collection_name collection_name = dataset_collection_binding.collection_name
else: else:
raise ValueError("Dataset Collection Bindings is not exist!") raise ValueError("Dataset Collection Bindings does not exist!")
else: else:
if dataset.index_struct_dict: if dataset.index_struct_dict:
class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"] class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"]

@ -1,3 +1,4 @@
import base64
import json import json
from collections.abc import Mapping from collections.abc import Mapping
from copy import deepcopy from copy import deepcopy
@ -259,7 +260,9 @@ class Executor:
if self.auth.config.type == "bearer": if self.auth.config.type == "bearer":
headers[authorization.config.header] = f"Bearer {authorization.config.api_key}" headers[authorization.config.header] = f"Bearer {authorization.config.api_key}"
elif self.auth.config.type == "basic": elif self.auth.config.type == "basic":
headers[authorization.config.header] = f"Basic {authorization.config.api_key}" credentials = authorization.config.api_key
encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("utf-8")
headers[authorization.config.header] = f"Basic {encoded_credentials}"
elif self.auth.config.type == "custom": elif self.auth.config.type == "custom":
headers[authorization.config.header] = authorization.config.api_key or "" headers[authorization.config.header] = authorization.config.api_key or ""

@ -186,6 +186,8 @@ class ParameterExtractorNode(LLMNode):
"usage": None, "usage": None,
"function": {} if not prompt_message_tools else jsonable_encoder(prompt_message_tools[0]), "function": {} if not prompt_message_tools else jsonable_encoder(prompt_message_tools[0]),
"tool_call": None, "tool_call": None,
"model_provider": model_config.provider,
"model_name": model_config.model,
} }
try: try:

@ -130,6 +130,8 @@ class QuestionClassifierNode(LLMNode):
), ),
"usage": jsonable_encoder(usage), "usage": jsonable_encoder(usage),
"finish_reason": finish_reason, "finish_reason": finish_reason,
"model_provider": model_config.provider,
"model_name": model_config.model,
} }
outputs = {"class_name": category_name, "class_id": category_id} outputs = {"class_name": category_name, "class_id": category_id}

@ -36,6 +36,8 @@ def init_app(app: DifyApp):
handlers=log_handlers, handlers=log_handlers,
force=True, force=True,
) )
# Disable propagation for noisy loggers to avoid duplicate logs
logging.getLogger("sqlalchemy.engine").propagate = False
log_tz = dify_config.LOG_TZ log_tz = dify_config.LOG_TZ
if log_tz: if log_tz:
from datetime import datetime from datetime import datetime

@ -1,16 +1,20 @@
import atexit import atexit
import logging
import os import os
import platform import platform
import socket import socket
import sys
from typing import Union from typing import Union
from celery.signals import worker_init # type: ignore
from flask_login import user_loaded_from_request, user_logged_in # type: ignore from flask_login import user_loaded_from_request, user_logged_in # type: ignore
from opentelemetry import trace from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.metrics import set_meter_provider from opentelemetry.metrics import get_meter_provider, set_meter_provider
from opentelemetry.propagate import set_global_textmap from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.b3 import B3Format from opentelemetry.propagators.b3 import B3Format
from opentelemetry.propagators.composite import CompositePropagator from opentelemetry.propagators.composite import CompositePropagator
@ -24,7 +28,7 @@ from opentelemetry.sdk.trace.export import (
) )
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.trace import Span, get_current_span, set_tracer_provider from opentelemetry.trace import Span, get_current_span, get_tracer_provider, set_tracer_provider
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.trace.status import StatusCode from opentelemetry.trace.status import StatusCode
@ -96,7 +100,18 @@ def init_app(app: DifyApp):
export_timeout_millis=dify_config.OTEL_METRIC_EXPORT_TIMEOUT, export_timeout_millis=dify_config.OTEL_METRIC_EXPORT_TIMEOUT,
) )
set_meter_provider(MeterProvider(resource=resource, metric_readers=[reader])) set_meter_provider(MeterProvider(resource=resource, metric_readers=[reader]))
if not is_celery_worker():
init_flask_instrumentor(app)
CeleryInstrumentor(tracer_provider=get_tracer_provider(), meter_provider=get_meter_provider()).instrument()
init_sqlalchemy_instrumentor(app)
atexit.register(shutdown_tracer)
def is_celery_worker():
return "celery" in sys.argv[0].lower()
def init_flask_instrumentor(app: DifyApp):
def response_hook(span: Span, status: str, response_headers: list): def response_hook(span: Span, status: str, response_headers: list):
if span and span.is_recording(): if span and span.is_recording():
if status.startswith("2"): if status.startswith("2"):
@ -105,11 +120,15 @@ def init_app(app: DifyApp):
span.set_status(StatusCode.ERROR, status) span.set_status(StatusCode.ERROR, status)
instrumentor = FlaskInstrumentor() instrumentor = FlaskInstrumentor()
if dify_config.DEBUG:
logging.info("Initializing Flask instrumentor")
instrumentor.instrument_app(app, response_hook=response_hook) instrumentor.instrument_app(app, response_hook=response_hook)
def init_sqlalchemy_instrumentor(app: DifyApp):
with app.app_context(): with app.app_context():
engines = list(app.extensions["sqlalchemy"].engines.values()) engines = list(app.extensions["sqlalchemy"].engines.values())
SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines) SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines)
atexit.register(shutdown_tracer)
def setup_context_propagation(): def setup_context_propagation():
@ -124,6 +143,15 @@ def setup_context_propagation():
) )
@worker_init.connect(weak=False)
def init_celery_worker(*args, **kwargs):
tracer_provider = get_tracer_provider()
metric_provider = get_meter_provider()
if dify_config.DEBUG:
logging.info("Initializing OpenTelemetry for Celery worker")
CeleryInstrumentor(tracer_provider=tracer_provider, meter_provider=metric_provider).instrument()
def shutdown_tracer(): def shutdown_tracer():
provider = trace.get_tracer_provider() provider = trace.get_tracer_provider()
if hasattr(provider, "force_flush"): if hasattr(provider, "force_flush"):

10563
api/poetry.lock generated

File diff suppressed because it is too large Load Diff

@ -1,214 +1,203 @@
[project] [project]
name = "dify-api" name = "dify-api"
version = "1.2.0"
requires-python = ">=3.11,<3.13" requires-python = ">=3.11,<3.13"
dynamic = ["dependencies"]
[build-system] dependencies = [
requires = ["poetry-core>=2.0.0"] "authlib==1.3.1",
build-backend = "poetry.core.masonry.api" "azure-identity==1.16.1",
"beautifulsoup4==4.12.2",
"boto3==1.35.99",
"bs4~=0.0.1",
"cachetools~=5.3.0",
"celery~=5.4.0",
"chardet~=5.1.0",
"flask~=3.1.0",
"flask-compress~=1.17",
"flask-cors~=4.0.0",
"flask-login~=0.6.3",
"flask-migrate~=4.0.7",
"flask-restful~=0.3.10",
"flask-sqlalchemy~=3.1.1",
"gevent~=24.11.1",
"gmpy2~=2.2.1",
"google-api-core==2.18.0",
"google-api-python-client==2.90.0",
"google-auth==2.29.0",
"google-auth-httplib2==0.2.0",
"google-cloud-aiplatform==1.49.0",
"googleapis-common-protos==1.63.0",
"gunicorn~=23.0.0",
"httpx[socks]~=0.27.0",
"jieba==0.42.1",
"langfuse~=2.51.3",
"langsmith~=0.1.77",
"mailchimp-transactional~=1.0.50",
"markdown~=3.5.1",
"numpy~=1.26.4",
"oci~=2.135.1",
"openai~=1.61.0",
"openpyxl~=3.1.5",
"opik~=1.3.4",
"opentelemetry-api==1.27.0",
"opentelemetry-distro==0.48b0",
"opentelemetry-exporter-otlp==1.27.0",
"opentelemetry-exporter-otlp-proto-common==1.27.0",
"opentelemetry-exporter-otlp-proto-grpc==1.27.0",
"opentelemetry-exporter-otlp-proto-http==1.27.0",
"opentelemetry-instrumentation==0.48b0",
"opentelemetry-instrumentation-celery==0.48b0",
"opentelemetry-instrumentation-flask==0.48b0",
"opentelemetry-instrumentation-sqlalchemy==0.48b0",
"opentelemetry-propagator-b3==1.27.0",
# opentelemetry-proto1.28.0 depends on protobuf (>=5.0,<6.0),
# which is conflict with googleapis-common-protos (1.63.0)
"opentelemetry-proto==1.27.0",
"opentelemetry-sdk==1.27.0",
"opentelemetry-semantic-conventions==0.48b0",
"opentelemetry-util-http==0.48b0",
"pandas-stubs~=2.2.3.241009",
"pandas[excel,output-formatting,performance]~=2.2.2",
"pandoc~=2.4",
"psycogreen~=1.0.2",
"psycopg2-binary~=2.9.6",
"pycryptodome==3.19.1",
"pydantic~=2.9.2",
"pydantic-extra-types~=2.9.0",
"pydantic-settings~=2.6.0",
"pyjwt~=2.8.0",
"pypdfium2~=4.30.0",
"python-docx~=1.1.0",
"python-dotenv==1.0.1",
"pyyaml~=6.0.1",
"readabilipy==0.2.0",
"redis[hiredis]~=5.0.3",
"resend~=0.7.0",
"sentry-sdk[flask]~=1.44.1",
"sqlalchemy~=2.0.29",
"starlette==0.41.0",
"tiktoken~=0.8.0",
"tokenizers~=0.15.0",
"transformers~=4.35.0",
"unstructured[docx,epub,md,ppt,pptx]~=0.16.1",
"validators==0.21.0",
"yarl~=1.18.3",
]
# Before adding new dependency, consider place it in
# alphabet order (a-z) and suitable group.
[tool.poetry] [tool.uv]
package-mode = false default-groups = ["storage", "tools", "vdb"]
[dependency-groups]
############################################################ ############################################################
# [ Main ] Dependency group # [ Dev ] dependency group
# Required for development and running tests
############################################################ ############################################################
[tool.poetry.dependencies] dev = [
authlib = "1.3.1" "coverage~=7.2.4",
azure-identity = "1.16.1" "faker~=32.1.0",
beautifulsoup4 = "4.12.2" "lxml-stubs~=0.5.1",
boto3 = "1.35.99" "mypy~=1.15.0",
bs4 = "~0.0.1" "pytest~=8.3.2",
cachetools = "~5.3.0" "pytest-benchmark~=4.0.0",
celery = "~5.4.0" "pytest-env~=1.1.3",
chardet = "~5.1.0" "pytest-mock~=3.14.0",
flask = "~3.1.0" "types-aiofiles~=24.1.0",
flask-compress = "~1.17" "types-beautifulsoup4~=4.12.0",
flask-cors = "~4.0.0" "types-cachetools~=5.5.0",
flask-login = "~0.6.3" "types-colorama~=0.4.15",
flask-migrate = "~4.0.7" "types-defusedxml~=0.7.0",
flask-restful = "~0.3.10" "types-deprecated~=1.2.15",
flask-sqlalchemy = "~3.1.1" "types-docutils~=0.21.0",
gevent = "~24.11.1" "types-flask-cors~=5.0.0",
gmpy2 = "~2.2.1" "types-flask-migrate~=4.1.0",
google-api-core = "2.18.0" "types-gevent~=24.11.0",
google-api-python-client = "2.90.0" "types-greenlet~=3.1.0",
google-auth = "2.29.0" "types-html5lib~=1.1.11",
google-auth-httplib2 = "0.2.0" "types-markdown~=3.7.0",
google-cloud-aiplatform = "1.49.0" "types-oauthlib~=3.2.0",
googleapis-common-protos = "1.63.0" "types-objgraph~=3.6.0",
gunicorn = "~23.0.0" "types-olefile~=0.47.0",
httpx = { version = "~0.27.0", extras = ["socks"] } "types-openpyxl~=3.1.5",
jieba = "0.42.1" "types-pexpect~=4.9.0",
langfuse = "~2.51.3" "types-protobuf~=5.29.1",
langsmith = "~0.1.77" "types-psutil~=7.0.0",
mailchimp-transactional = "~1.0.50" "types-psycopg2~=2.9.21",
markdown = "~3.5.1" "types-pygments~=2.19.0",
numpy = "~1.26.4" "types-pymysql~=1.1.0",
oci = "~2.135.1" "types-python-dateutil~=2.9.0",
openai = "~1.61.0" "types-pywin32~=310.0.0",
openpyxl = "~3.1.5" "types-pyyaml~=6.0.12",
opentelemetry-api = "1.27.0" "types-regex~=2024.11.6",
opentelemetry-distro = "0.48b0" "types-requests~=2.32.0",
opentelemetry-exporter-otlp = "1.27.0" "types-requests-oauthlib~=2.0.0",
opentelemetry-exporter-otlp-proto-common = "1.27.0" "types-shapely~=2.0.0",
opentelemetry-exporter-otlp-proto-grpc = "1.27.0" "types-simplejson~=3.20.0",
opentelemetry-exporter-otlp-proto-http = "1.27.0" "types-six~=1.17.0",
opentelemetry-instrumentation = "0.48b0" "types-tensorflow~=2.18.0",
opentelemetry-instrumentation-flask = "0.48b0" "types-tqdm~=4.67.0",
opentelemetry-instrumentation-sqlalchemy = "0.48b0" "types-ujson~=5.10.0",
opentelemetry-propagator-b3 = "1.27.0" ]
opentelemetry-proto = "1.27.0" # 1.28.0 depends on protobuf (>=5.0,<6.0), conflict with googleapis-common-protos (1.63.0)
opentelemetry-sdk = "1.27.0"
opentelemetry-semantic-conventions = "0.48b0"
opentelemetry-util-http = "0.48b0"
opik = "~1.3.4"
pandas = { version = "~2.2.2", extras = ["performance", "excel", "output-formatting"] }
pandas-stubs = "~2.2.3.241009"
pandoc = "~2.4"
psycogreen = "~1.0.2"
psycopg2-binary = "~2.9.6"
pycryptodome = "3.19.1"
pydantic = "~2.9.2"
pydantic-settings = "~2.6.0"
pydantic_extra_types = "~2.9.0"
pyjwt = "~2.8.0"
pypdfium2 = "~4.30.0"
python = ">=3.11,<3.13"
python-docx = "~1.1.0"
python-dotenv = "1.0.1"
pyyaml = "~6.0.1"
readabilipy = "0.2.0"
redis = { version = "~5.0.3", extras = ["hiredis"] }
resend = "~0.7.0"
sentry-sdk = { version = "~1.44.1", extras = ["flask"] }
sqlalchemy = "~2.0.29"
starlette = "0.41.0"
tiktoken = "~0.8.0"
tokenizers = "~0.15.0"
transformers = "~4.35.0"
unstructured = { version = "~0.16.1", extras = ["docx", "epub", "md", "ppt", "pptx"] }
validators = "0.21.0"
yarl = "~1.18.3"
# Before adding new dependency, consider place it in alphabet order (a-z) and suitable group.
############################################################
# [ Indirect ] dependency group
# Related transparent dependencies with pinned version
# required by main implementations
############################################################
[tool.poetry.group.indirect.dependencies]
kaleido = "0.2.1"
rank-bm25 = "~0.2.2"
safetensors = "~0.4.3"
############################################################ ############################################################
# [ Tools ] dependency group # [ Lint ] dependency group
# Required for code style linting
############################################################ ############################################################
[tool.poetry.group.tools.dependencies] lint = [
cloudscraper = "1.2.71" "dotenv-linter~=0.5.0",
nltk = "3.9.1" "ruff~=0.11.0",
]
############################################################ ############################################################
# [ Storage ] dependency group # [ Storage ] dependency group
# Required for storage clients # Required for storage clients
############################################################ ############################################################
[tool.poetry.group.storage.dependencies] storage = [
azure-storage-blob = "12.13.0" "azure-storage-blob==12.13.0",
bce-python-sdk = "~0.9.23" "bce-python-sdk~=0.9.23",
cos-python-sdk-v5 = "1.9.30" "cos-python-sdk-v5==1.9.30",
esdk-obs-python = "3.24.6.1" "esdk-obs-python==3.24.6.1",
google-cloud-storage = "2.16.0" "google-cloud-storage==2.16.0",
opendal = "~0.45.16" "opendal~=0.45.16",
oss2 = "2.18.5" "oss2==2.18.5",
supabase = "~2.8.1" "supabase~=2.8.1",
tos = "~2.7.1" "tos~=2.7.1",
]
############################################################
# [ VDB ] dependency group
# Required by vector store clients
############################################################
[tool.poetry.group.vdb.dependencies]
alibabacloud_gpdb20160503 = "~3.8.0"
alibabacloud_tea_openapi = "~0.3.9"
chromadb = "0.5.20"
clickhouse-connect = "~0.7.16"
couchbase = "~4.3.0"
elasticsearch = "8.14.0"
opensearch-py = "2.4.0"
oracledb = "~2.2.1"
pgvecto-rs = { version = "~0.2.1", extras = ['sqlalchemy'] }
pgvector = "0.2.5"
pymilvus = "~2.5.0"
pymochow = "1.3.1"
pyobvector = "~0.1.6"
qdrant-client = "1.7.3"
tablestore = "6.1.0"
tcvectordb = "~1.6.4"
tidb-vector = "0.0.9"
upstash-vector = "0.6.0"
volcengine-compat = "~1.0.156"
weaviate-client = "~3.21.0"
xinference-client = "~1.2.2"
############################################################ ############################################################
# [ Dev ] dependency group # [ Tools ] dependency group
# Required for development and running tests
############################################################ ############################################################
[tool.poetry.group.dev] tools = [
optional = true "cloudscraper~=1.2.71",
[tool.poetry.group.dev.dependencies] "nltk~=3.9.1",
coverage = "~7.2.4" ]
faker = "~32.1.0"
lxml-stubs = "~0.5.1"
mypy = "~1.15.0"
pytest = "~8.3.2"
pytest-benchmark = "~4.0.0"
pytest-env = "~1.1.3"
pytest-mock = "~3.14.0"
types-aiofiles = "~24.1.0"
types-beautifulsoup4 = "~4.12.0"
types-cachetools = "~5.5.0"
types-colorama = "~0.4.15"
types-defusedxml = "~0.7.0"
types-deprecated = "~1.2.15"
types-docutils = "~0.21.0"
types-flask-cors = "~5.0.0"
types-flask-migrate = "~4.1.0"
types-gevent = "~24.11.0"
types-greenlet = "~3.1.0"
types-html5lib = "~1.1.11"
types-markdown = "~3.7.0"
types-oauthlib = "~3.2.0"
types-objgraph = "~3.6.0"
types-olefile = "~0.47.0"
types-openpyxl = "~3.1.5"
types-pexpect = "~4.9.0"
types-protobuf = "~5.29.1"
types-psutil = "~7.0.0"
types-psycopg2 = "~2.9.21"
types-pygments = "~2.19.0"
types-pymysql = "~1.1.0"
types-python-dateutil = "~2.9.0"
types-pywin32 = "~310.0.0"
types-pyyaml = "~6.0.12"
types-regex = "~2024.11.6"
types-requests = "~2.32.0"
types-requests-oauthlib = "~2.0.0"
types-shapely = "~2.0.0"
types-simplejson = "~3.20.0"
types-six = "~1.17.0"
types-tensorflow = "~2.18.0"
types-tqdm = "~4.67.0"
types-ujson = "~5.10.0"
############################################################ ############################################################
# [ Lint ] dependency group # [ VDB ] dependency group
# Required for code style linting # Required by vector store clients
############################################################ ############################################################
[tool.poetry.group.lint] vdb = [
optional = true "alibabacloud_gpdb20160503~=3.8.0",
[tool.poetry.group.lint.dependencies] "alibabacloud_tea_openapi~=0.3.9",
dotenv-linter = "~0.5.0" "chromadb==0.5.20",
ruff = "~0.11.0" "clickhouse-connect~=0.7.16",
"couchbase~=4.3.0",
"elasticsearch==8.14.0",
"opensearch-py==2.4.0",
"oracledb~=2.2.1",
"pgvecto-rs[sqlalchemy]~=0.2.1",
"pgvector==0.2.5",
"pymilvus~=2.5.0",
"pymochow==1.3.1",
"pyobvector~=0.1.6",
"qdrant-client==1.7.3",
"tablestore==6.1.0",
"tcvectordb~=1.6.4",
"tidb-vector==0.0.9",
"upstash-vector==0.6.0",
"volcengine-compat~=1.0.156",
"weaviate-client~=3.21.0",
"xinference-client~=1.2.2",
]

@ -169,6 +169,9 @@ class DatasetService:
provider: str = "vendor", provider: str = "vendor",
external_knowledge_api_id: Optional[str] = None, external_knowledge_api_id: Optional[str] = None,
external_knowledge_id: Optional[str] = None, external_knowledge_id: Optional[str] = None,
embedding_model_provider: Optional[str] = None,
embedding_model_name: Optional[str] = None,
retrieval_model: Optional[RetrievalModel] = None,
): ):
# check if dataset name already exists # check if dataset name already exists
if Dataset.query.filter_by(name=name, tenant_id=tenant_id).first(): if Dataset.query.filter_by(name=name, tenant_id=tenant_id).first():
@ -176,9 +179,30 @@ class DatasetService:
embedding_model = None embedding_model = None
if indexing_technique == "high_quality": if indexing_technique == "high_quality":
model_manager = ModelManager() model_manager = ModelManager()
if embedding_model_provider and embedding_model_name:
# check if embedding model setting is valid
DatasetService.check_embedding_model_setting(tenant_id, embedding_model_provider, embedding_model_name)
embedding_model = model_manager.get_model_instance(
tenant_id=tenant_id,
provider=embedding_model_provider,
model_type=ModelType.TEXT_EMBEDDING,
model=embedding_model_name,
)
else:
embedding_model = model_manager.get_default_model_instance( embedding_model = model_manager.get_default_model_instance(
tenant_id=tenant_id, model_type=ModelType.TEXT_EMBEDDING tenant_id=tenant_id, model_type=ModelType.TEXT_EMBEDDING
) )
if retrieval_model and retrieval_model.reranking_model:
if (
retrieval_model.reranking_model.reranking_provider_name
and retrieval_model.reranking_model.reranking_model_name
):
# check if reranking model setting is valid
DatasetService.check_embedding_model_setting(
tenant_id,
retrieval_model.reranking_model.reranking_provider_name,
retrieval_model.reranking_model.reranking_model_name,
)
dataset = Dataset(name=name, indexing_technique=indexing_technique) dataset = Dataset(name=name, indexing_technique=indexing_technique)
# dataset = Dataset(name=name, provider=provider, config=config) # dataset = Dataset(name=name, provider=provider, config=config)
dataset.description = description dataset.description = description
@ -187,6 +211,7 @@ class DatasetService:
dataset.tenant_id = tenant_id dataset.tenant_id = tenant_id
dataset.embedding_model_provider = embedding_model.provider if embedding_model else None dataset.embedding_model_provider = embedding_model.provider if embedding_model else None
dataset.embedding_model = embedding_model.model if embedding_model else None dataset.embedding_model = embedding_model.model if embedding_model else None
dataset.retrieval_model = retrieval_model.model_dump() if retrieval_model else None
dataset.permission = permission or DatasetPermissionEnum.ONLY_ME dataset.permission = permission or DatasetPermissionEnum.ONLY_ME
dataset.provider = provider dataset.provider = provider
db.session.add(dataset) db.session.add(dataset)

@ -309,6 +309,22 @@ class PluginService:
], ],
) )
@staticmethod
def fetch_marketplace_pkg(
tenant_id: str, plugin_unique_identifier: str, verify_signature: bool = False
) -> PluginDeclaration:
"""
Fetch marketplace package
"""
manager = PluginInstallationManager()
try:
declaration = manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
except Exception:
pkg = download_plugin_pkg(plugin_unique_identifier)
declaration = manager.upload_pkg(tenant_id, pkg, verify_signature).manifest
return declaration
@staticmethod @staticmethod
def install_from_marketplace_pkg( def install_from_marketplace_pkg(
tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False

@ -1,49 +0,0 @@
from typing import Any
import toml # type: ignore
def load_api_poetry_configs() -> dict[str, Any]:
pyproject_toml = toml.load("api/pyproject.toml")
return pyproject_toml["tool"]["poetry"]
def load_all_dependency_groups() -> dict[str, dict[str, dict[str, Any]]]:
configs = load_api_poetry_configs()
configs_by_group = {"main": configs}
for group_name in configs["group"]:
configs_by_group[group_name] = configs["group"][group_name]
dependencies_by_group = {group_name: base["dependencies"] for group_name, base in configs_by_group.items()}
return dependencies_by_group
def test_group_dependencies_sorted():
for group_name, dependencies in load_all_dependency_groups().items():
dependency_names = list(dependencies.keys())
expected_dependency_names = sorted(set(dependency_names))
section = f"tool.poetry.group.{group_name}.dependencies" if group_name else "tool.poetry.dependencies"
assert expected_dependency_names == dependency_names, (
f"Dependencies in group {group_name} are not sorted. "
f"Check and fix [{section}] section in pyproject.toml file"
)
def test_group_dependencies_version_operator():
for group_name, dependencies in load_all_dependency_groups().items():
for dependency_name, specification in dependencies.items():
version_spec = specification if isinstance(specification, str) else specification["version"]
assert not version_spec.startswith("^"), (
f"Please replace '{dependency_name} = {version_spec}' with '{dependency_name} = ~{version_spec[1:]}' "
f"'^' operator is too wide and not allowed in the version specification."
)
def test_duplicated_dependency_crossing_groups() -> None:
all_dependency_names: list[str] = []
for dependencies in load_all_dependency_groups().values():
dependency_names = list(dependencies.keys())
all_dependency_names.extend(dependency_names)
expected_all_dependency_names = set(all_dependency_names)
assert sorted(expected_all_dependency_names) == sorted(all_dependency_names), (
"Duplicated dependencies crossing groups are found"
)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,7 @@
#!/bin/bash
set -x
# run mypy checks
uv run --directory api --group dev \
python -m mypy --install-types --non-interactive .

@ -2,20 +2,14 @@
set -x set -x
# style checks rely on commands in path
if ! command -v ruff &> /dev/null || ! command -v dotenv-linter &> /dev/null; then
echo "Installing linting tools (Ruff, dotenv-linter ...) ..."
poetry install -C api --only lint
fi
# run ruff linter # run ruff linter
poetry run -C api ruff check --fix ./ uv run --directory api --group lint ruff check --fix ./
# run ruff formatter # run ruff formatter
poetry run -C api ruff format ./ uv run --directory api --group lint ruff format ./
# run dotenv-linter linter # run dotenv-linter linter
poetry run -P api dotenv-linter ./api/.env.example ./web/.env.example uv run --project api --group lint dotenv-linter ./api/.env.example ./web/.env.example
# run mypy check # run mypy check
dev/run-mypy dev/mypy-check

@ -2,10 +2,6 @@
set -x set -x
if ! command -v mypy &> /dev/null; then
poetry install -C api --with dev
fi
# run mypy checks # run mypy checks
poetry run -C api \ uv run --directory api --group dev \
python -m mypy --install-types --non-interactive . python -m mypy --install-types --non-interactive .

@ -1,18 +0,0 @@
#!/bin/bash
# rely on `poetry` in path
if ! command -v poetry &> /dev/null; then
echo "Installing Poetry ..."
pip install poetry
fi
# check poetry.lock in sync with pyproject.toml
poetry check -C api --lock
if [ $? -ne 0 ]; then
# update poetry.lock
# refreshing lockfile only without updating locked versions
echo "poetry.lock is outdated, refreshing without updating locked versions ..."
poetry lock -C api
else
echo "poetry.lock is ready."
fi

@ -0,0 +1,10 @@
#!/bin/bash
# rely on `uv` in path
if ! command -v uv &> /dev/null; then
echo "Installing uv ..."
pip install uv
fi
# check uv.lock in sync with pyproject.toml
uv lock --project api

@ -1,13 +0,0 @@
#!/bin/bash
# rely on `poetry` in path
if ! command -v poetry &> /dev/null; then
echo "Installing Poetry ..."
pip install poetry
fi
# refreshing lockfile, updating locked versions
poetry update -C api
# check poetry.lock in sync with pyproject.toml
poetry check -C api --lock

@ -0,0 +1,22 @@
#!/bin/bash
# Update dependencies in dify/api project using uv
set -e
set -o pipefail
SCRIPT_DIR="$(dirname "$0")"
REPO_ROOT="$(dirname "${SCRIPT_DIR}")"
# rely on `poetry` in path
if ! command -v uv &> /dev/null; then
echo "Installing uv ..."
pip install uv
fi
cd "${REPO_ROOT}"
# refreshing lockfile, updating locked versions
uv lock --project api --upgrade
# check uv.lock in sync with pyproject.toml
uv lock --project api --check

@ -163,7 +163,7 @@ services:
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-}
PAWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-} AWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-}
AWS_REGION: ${PLUGIN_AWS_REGION:-} AWS_REGION: ${PLUGIN_AWS_REGION:-}
AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-} AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-}
AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-} AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-}

@ -107,7 +107,7 @@ services:
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-}
PAWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-} AWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-}
AWS_REGION: ${PLUGIN_AWS_REGION:-} AWS_REGION: ${PLUGIN_AWS_REGION:-}
AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-} AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-}
AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-} AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-}

@ -631,7 +631,7 @@ services:
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-}
PAWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-} AWS_SECRET_KEY: ${PLUGIN_AWS_SECRET_KEY:-}
AWS_REGION: ${PLUGIN_AWS_REGION:-} AWS_REGION: ${PLUGIN_AWS_REGION:-}
AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-} AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-}
AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-} AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-}

@ -30,14 +30,14 @@ if $api_modified; then
# python style checks rely on `ruff` in path # python style checks rely on `ruff` in path
if ! command -v ruff > /dev/null 2>&1; then if ! command -v ruff > /dev/null 2>&1; then
echo "Installing linting tools (Ruff, dotenv-linter ...) ..." echo "Installing linting tools (Ruff, dotenv-linter ...) ..."
poetry install -C api --only lint uv sync --project api --only-group lint
fi fi
# run Ruff linter auto-fixing # run Ruff linter auto-fixing
ruff check --fix ./api uv run --project api ruff check --fix ./api
# run Ruff linter checks # run Ruff linter checks
ruff check ./api || status=$? uv run --project api ruff check ./api || status=$?
status=${status:-0} status=${status:-0}

@ -124,7 +124,7 @@ const ConfigPopup: FC<PopupProps> = ({
) )
const configuredProviderPanel = () => { const configuredProviderPanel = () => {
const configuredPanels: any[] = [] const configuredPanels: JSX.Element[] = []
if (langSmithConfig) if (langSmithConfig)
configuredPanels.push(langSmithPanel) configuredPanels.push(langSmithPanel)
@ -139,7 +139,7 @@ const ConfigPopup: FC<PopupProps> = ({
} }
const moreProviderPanel = () => { const moreProviderPanel = () => {
const notConfiguredPanels: any[] = [] const notConfiguredPanels: JSX.Element[] = []
if (!langSmithConfig) if (!langSmithConfig)
notConfiguredPanels.push(langSmithPanel) notConfiguredPanels.push(langSmithPanel)

@ -1,8 +1,6 @@
import React from 'react' import React from 'react'
type Props = {} const page = () => {
const page = (props: Props) => {
return ( return (
<div>dataset detail api</div> <div>dataset detail api</div>
) )

@ -1,22 +1,22 @@
'use client' 'use client'
import type { FC, SVGProps } from 'react' import type { FC } from 'react'
import React, { useEffect, useMemo } from 'react' import React, { useEffect, useMemo } from 'react'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import useSWR from 'swr' import useSWR from 'swr'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { import {
Cog8ToothIcon, RiEqualizer2Fill,
DocumentTextIcon, RiEqualizer2Line,
RiFileTextFill,
RiFileTextLine,
RiFocus2Fill,
RiFocus2Line,
} from '@remixicon/react'
import {
PaperClipIcon, PaperClipIcon,
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import {
Cog8ToothIcon as Cog8ToothSolidIcon,
// CommandLineIcon as CommandLineSolidIcon,
DocumentTextIcon as DocumentTextSolidIcon,
} from '@heroicons/react/24/solid'
import { RiApps2AddLine, RiBookOpenLine, RiInformation2Line } from '@remixicon/react' import { RiApps2AddLine, RiBookOpenLine, RiInformation2Line } from '@remixicon/react'
import s from './style.module.css'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets' import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets'
import type { RelatedAppResponse } from '@/models/datasets' import type { RelatedAppResponse } from '@/models/datasets'
@ -37,27 +37,6 @@ export type IAppDetailLayoutProps = {
params: { datasetId: string } params: { datasetId: string }
} }
const TargetIcon = ({ className }: SVGProps<SVGElement>) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<g clipPath="url(#clip0_4610_6951)">
<path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</g>
<defs>
<clipPath id="clip0_4610_6951">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
}
const TargetSolidIcon = ({ className }: SVGProps<SVGElement>) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path fillRule="evenodd" clipRule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
<path d="M1.99984 7.99984C1.99984 4.68613 4.68613 1.99984 7.99984 1.99984C8.36803 1.99984 8.6665 1.70136 8.6665 1.33317C8.6665 0.964981 8.36803 0.666504 7.99984 0.666504C3.94975 0.666504 0.666504 3.94975 0.666504 7.99984C0.666504 12.0499 3.94975 15.3332 7.99984 15.3332C12.0499 15.3332 15.3332 12.0499 15.3332 7.99984C15.3332 7.63165 15.0347 7.33317 14.6665 7.33317C14.2983 7.33317 13.9998 7.63165 13.9998 7.99984C13.9998 11.3135 11.3135 13.9998 7.99984 13.9998C4.68613 13.9998 1.99984 11.3135 1.99984 7.99984Z" fill="#155EEF" />
<path d="M5.33317 7.99984C5.33317 6.52708 6.52708 5.33317 7.99984 5.33317C8.36803 5.33317 8.6665 5.03469 8.6665 4.6665C8.6665 4.29831 8.36803 3.99984 7.99984 3.99984C5.7907 3.99984 3.99984 5.7907 3.99984 7.99984C3.99984 10.209 5.7907 11.9998 7.99984 11.9998C10.209 11.9998 11.9998 10.209 11.9998 7.99984C11.9998 7.63165 11.7014 7.33317 11.3332 7.33317C10.965 7.33317 10.6665 7.63165 10.6665 7.99984C10.6665 9.4726 9.4726 10.6665 7.99984 10.6665C6.52708 10.6665 5.33317 9.4726 5.33317 7.99984Z" fill="#155EEF" />
</svg>
}
type IExtraInfoProps = { type IExtraInfoProps = {
isMobile: boolean isMobile: boolean
relatedApps?: RelatedAppResponse relatedApps?: RelatedAppResponse
@ -98,9 +77,9 @@ const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
</Tooltip> </Tooltip>
)} )}
{isMobile && <div className={classNames(s.subTitle, 'flex items-center justify-center !px-0 gap-1')}> {isMobile && <div className={classNames('uppercase text-xs text-text-tertiary font-medium pb-2 pt-4', 'flex items-center justify-center !px-0 gap-1')}>
{relatedAppsTotal || '--'} {relatedAppsTotal || '--'}
<PaperClipIcon className='h-4 w-4 text-gray-700' /> <PaperClipIcon className='h-4 w-4 text-text-secondary' />
</div>} </div>}
</> </>
)} )}
@ -164,17 +143,16 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const navigation = useMemo(() => { const navigation = useMemo(() => {
const baseNavigation = [ const baseNavigation = [
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon }, { name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: RiFocus2Line, selectedIcon: RiFocus2Fill },
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon }, { name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: RiEqualizer2Line, selectedIcon: RiEqualizer2Fill },
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
] ]
if (datasetRes?.provider !== 'external') { if (datasetRes?.provider !== 'external') {
baseNavigation.unshift({ baseNavigation.unshift({
name: t('common.datasetMenus.documents'), name: t('common.datasetMenus.documents'),
href: `/datasets/${datasetId}/documents`, href: `/datasets/${datasetId}/documents`,
icon: DocumentTextIcon, icon: RiFileTextLine,
selectedIcon: DocumentTextSolidIcon, selectedIcon: RiFileTextFill,
}) })
} }
return baseNavigation return baseNavigation

@ -1,9 +0,0 @@
.statusPoint {
@apply flex justify-center items-center absolute -right-0.5 -bottom-0.5 w-2.5 h-2.5 bg-white rounded;
}
.subTitle {
@apply uppercase text-xs text-gray-500 font-medium px-3 pb-2 pt-4;
}
.emptyIconDiv {
@apply h-7 w-7 bg-gray-50 border border-[#EAECF5] inline-flex justify-center items-center rounded-lg;
}

@ -11,7 +11,7 @@ import { useQuery } from '@tanstack/react-query'
import ExternalAPIPanel from '../../components/datasets/external-api/external-api-panel' import ExternalAPIPanel from '../../components/datasets/external-api/external-api-panel'
import Datasets from './Datasets' import Datasets from './Datasets'
import DatasetFooter from './DatasetFooter' import DatasetFooter from './DatasetFooter'
import ApiServer from './ApiServer' import ApiServer from '../../components/develop/ApiServer'
import Doc from './Doc' import Doc from './Doc'
import TabSliderNew from '@/app/components/base/tab-slider-new' import TabSliderNew from '@/app/components/base/tab-slider-new'
import TagManagementModal from '@/app/components/base/tag-management' import TagManagementModal from '@/app/components/base/tag-management'

@ -61,7 +61,7 @@ const DatasetCard = ({
if (onSuccess) if (onSuccess)
onSuccess() onSuccess()
} }
catch (e: any) { catch {
} }
setShowConfirmDelete(false) setShowConfirmDelete(false)
}, [dataset.id, notify, onSuccess, t]) }, [dataset.id, notify, onSuccess, t])

@ -9,6 +9,9 @@ import TemplateZh from './template/template.zh.mdx'
import TemplateJa from './template/template.ja.mdx' import TemplateJa from './template/template.ja.mdx'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language' import { LanguagesSupported } from '@/i18n/language'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import cn from '@/utils/classnames'
type DocProps = { type DocProps = {
apiBaseUrl: string apiBaseUrl: string
@ -19,6 +22,7 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [toc, setToc] = useState<Array<{ href: string; text: string }>>([]) const [toc, setToc] = useState<Array<{ href: string; text: string }>>([])
const [isTocExpanded, setIsTocExpanded] = useState(false) const [isTocExpanded, setIsTocExpanded] = useState(false)
const { theme } = useTheme()
// Set initial TOC expanded state based on screen width // Set initial TOC expanded state based on screen width
useEffect(() => { useEffect(() => {
@ -83,12 +87,12 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
<div className={`fixed right-20 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}> <div className={`fixed right-20 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}>
{isTocExpanded {isTocExpanded
? ( ? (
<nav className="toc max-h-[calc(100vh-150px)] w-full overflow-y-auto rounded-lg bg-gray-50 p-4 shadow-md"> <nav className="toc max-h-[calc(100vh-150px)] w-full overflow-y-auto rounded-lg bg-components-panel-bg p-4 shadow-md">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold">{t('appApi.develop.toc')}</h3> <h3 className="text-lg font-semibold text-text-primary">{t('appApi.develop.toc')}</h3>
<button <button
onClick={() => setIsTocExpanded(false)} onClick={() => setIsTocExpanded(false)}
className="text-gray-500 hover:text-gray-700" className="text-text-tertiary hover:text-text-secondary"
> >
</button> </button>
@ -98,7 +102,7 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
<li key={index}> <li key={index}>
<a <a
href={item.href} href={item.href}
className="text-gray-600 transition-colors duration-200 hover:text-gray-900 hover:underline" className="text-text-secondary transition-colors duration-200 hover:text-text-primary hover:underline"
onClick={e => handleTocClick(e, item)} onClick={e => handleTocClick(e, item)}
> >
{item.text} {item.text}
@ -111,13 +115,13 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
: ( : (
<button <button
onClick={() => setIsTocExpanded(true)} onClick={() => setIsTocExpanded(true)}
className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 shadow-md transition-colors duration-200 hover:bg-gray-100" className="flex h-10 w-10 items-center justify-center rounded-full bg-components-button-secondary-bg shadow-md transition-colors duration-200 hover:bg-components-button-secondary-bg-hover"
> >
<RiListUnordered className="h-6 w-6" /> <RiListUnordered className="h-6 w-6 text-components-button-secondary-text" />
</button> </button>
)} )}
</div> </div>
<article className='prose-xl prose mx-1 rounded-t-xl bg-white px-4 pt-16 sm:mx-12'> <article className={cn('prose-xl prose mx-1 rounded-t-xl bg-background-default px-4 pt-16 sm:mx-12', theme === Theme.dark && 'dark:prose-invert')}>
{Template} {Template}
</article> </article>
</div> </div>

@ -37,7 +37,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Col> <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 ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -175,7 +175,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Col> <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 ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -314,6 +314,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
</Property> </Property>
<Property name='indexing_technique' type='string' key='indexing_technique'> <Property name='indexing_technique' type='string' key='indexing_technique'>
Index technique (optional) Index technique (optional)
If this is not set, embedding_model, embedding_provider_name and retrieval_model will be set to null
- <code>high_quality</code> High quality - <code>high_quality</code> High quality
- <code>economy</code> Economy - <code>economy</code> Economy
</Property> </Property>
@ -334,6 +335,26 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='external_knowledge_id' type='str' key='external_knowledge_id'> <Property name='external_knowledge_id' type='str' key='external_knowledge_id'>
External knowledge ID (optional) External knowledge ID (optional)
</Property> </Property>
<Property name='embedding_model' type='str' key='embedding_model'>
Embedding model name (optional)
</Property>
<Property name='embedding_provider_name' type='str' key='embedding_provider_name'>
Embedding model provider name (optional)
</Property>
<Property name='retrieval_model' type='object' key='retrieval_model'>
Retrieval model (optional)
- <code>search_method</code> (string) Search method
- <code>hybrid_search</code> Hybrid search
- <code>semantic_search</code> Semantic search
- <code>full_text_search</code> Full-text search
- <code>reranking_enable</code> (bool) Whether to enable reranking
- <code>reranking_model</code> (object) Rerank model configuration
- <code>reranking_provider_name</code> (string) Rerank model provider
- <code>reranking_model_name</code> (string) Rerank model name
- <code>top_k</code> (int) Number of results to return
- <code>score_threshold_enabled</code> (bool) Whether to enable score threshold
- <code>score_threshold</code> (float) Score threshold
</Property>
</Properties> </Properties>
</Col> </Col>
<Col sticky> <Col sticky>
@ -461,7 +482,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Query ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge Base ID Knowledge Base ID
@ -542,7 +563,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Query ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge Base ID Knowledge Base ID
@ -650,7 +671,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -689,7 +710,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Col> <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 ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -791,7 +812,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Col> <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 ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -888,7 +909,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -943,7 +964,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -985,7 +1006,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -1060,7 +1081,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -1252,10 +1273,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
title="Request" title="Request"
tag="DELETE" tag="DELETE"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}" label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}"
targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`} targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
> >
```bash {{ title: 'cURL' }} ```bash {{ title: 'cURL' }}
curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/segments/{segment_id}' \ curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \
--header 'Authorization: Bearer {api_key}' \ --header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' --header 'Content-Type: application/json'
``` ```
@ -1369,7 +1390,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -1440,7 +1461,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -1517,7 +1538,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -1565,7 +1586,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -1834,7 +1855,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -1881,7 +1902,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -1930,7 +1951,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -1963,7 +1984,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID
@ -1996,7 +2017,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID Knowledge ID

@ -37,7 +37,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Col> <Col>
この API は既存のナレッジに基づいており、このナレッジを基にテキストを使用して新しいドキュメントを作成します。 この API は既存のナレッジに基づいており、このナレッジを基にテキストを使用して新しいドキュメントを作成します。
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -175,7 +175,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Col> <Col>
この API は既存のナレッジに基づいており、このナレッジを基にファイルを使用して新しいドキュメントを作成します。 この API は既存のナレッジに基づいており、このナレッジを基にファイルを使用して新しいドキュメントを作成します。
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -334,6 +334,26 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='external_knowledge_id' type='str' key='external_knowledge_id'> <Property name='external_knowledge_id' type='str' key='external_knowledge_id'>
外部ナレッジ ID (オプション) 外部ナレッジ ID (オプション)
</Property> </Property>
<Property name='embedding_model' type='str' key='embedding_model'>
埋め込みモデル名(任意)
</Property>
<Property name='embedding_provider_name' type='str' key='embedding_provider_name'>
埋め込みモデルのプロバイダ名(任意)
</Property>
<Property name='retrieval_model' type='object' key='retrieval_model'>
検索モデル(任意)
- <code>search_method</code> (文字列) 検索方法
- <code>hybrid_search</code> ハイブリッド検索
- <code>semantic_search</code> セマンティック検索
- <code>full_text_search</code> 全文検索
- <code>reranking_enable</code> (ブール値) リランキングを有効にするかどうか
- <code>reranking_model</code> (オブジェクト) リランクモデルの設定
- <code>reranking_provider_name</code> (文字列) リランクモデルのプロバイダ
- <code>reranking_model_name</code> (文字列) リランクモデル名
- <code>top_k</code> (整数) 返される結果の数
- <code>score_threshold_enabled</code> (ブール値) スコア閾値を有効にするかどうか
- <code>score_threshold</code> (浮動小数点数) スコア閾値
</Property>
</Properties> </Properties>
</Col> </Col>
<Col sticky> <Col sticky>
@ -500,7 +520,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Col> <Col>
この API は既存のナレッジに基づいており、このナレッジを基にテキストを使用してドキュメントを更新します。 この API は既存のナレッジに基づいており、このナレッジを基にテキストを使用してドキュメントを更新します。
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -602,7 +622,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Col> <Col>
この API は既存のナレッジに基づいており、このナレッジを基にファイルを使用してドキュメントを更新します。 この API は既存のナレッジに基づいており、このナレッジを基にファイルを使用してドキュメントを更新します。
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -754,7 +774,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -796,7 +816,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -871,7 +891,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -1063,10 +1083,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
title="リクエスト" title="リクエスト"
tag="DELETE" tag="DELETE"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}" label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}"
targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`} targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
> >
```bash {{ title: 'cURL' }} ```bash {{ title: 'cURL' }}
curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/segments/{segment_id}' \ curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \
--header 'Authorization: Bearer {api_key}' \ --header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' --header 'Content-Type: application/json'
``` ```
@ -1180,7 +1200,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -1251,7 +1271,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -1328,7 +1348,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -1376,7 +1396,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -1645,7 +1665,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -1692,7 +1712,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -1741,7 +1761,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -1774,7 +1794,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -1807,7 +1827,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID
@ -1848,7 +1868,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### パラメータ ### パ
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
ナレッジ ID ナレッジ ID

@ -335,6 +335,26 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='external_knowledge_id' type='str' key='external_knowledge_id'> <Property name='external_knowledge_id' type='str' key='external_knowledge_id'>
外部知识库 ID选填 外部知识库 ID选填
</Property> </Property>
<Property name='embedding_model' type='str' key='embedding_model'>
Embedding 模型名称
</Property>
<Property name='embedding_provider_name' type='str' key='embedding_provider_name'>
Embedding 模型供应商
</Property>
<Property name='retrieval_model' type='object' key='retrieval_model'>
检索模式
- <code>search_method</code> (string) 检索方法
- <code>hybrid_search</code> 混合检索
- <code>semantic_search</code> 语义检索
- <code>full_text_search</code> 全文检索
- <code>reranking_enable</code> (bool) 是否开启rerank
- <code>reranking_model</code> (object) Rerank 模型配置
- <code>reranking_provider_name</code> (string) Rerank 模型的提供商
- <code>reranking_model_name</code> (string) Rerank 模型的名称
- <code>top_k</code> (int) 召回条数
- <code>score_threshold_enabled</code> (bool)是否开启召回分数限制
- <code>score_threshold</code> (float) 召回分数限制
</Property>
</Properties> </Properties>
</Col> </Col>
<Col sticky> <Col sticky>
@ -462,7 +482,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Query ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID 知识库 ID
@ -543,11 +563,15 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Query ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID 知识库 ID
</Property> </Property>
</Properties>
### Request Body
<Properties>
<Property name='indexing_technique' type='string' key='indexing_technique'> <Property name='indexing_technique' type='string' key='indexing_technique'>
索引模式(选填,建议填写) 索引模式(选填,建议填写)
- <code>high_quality</code> 高质量 - <code>high_quality</code> 高质量
@ -1917,7 +1941,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID 知识库 ID
@ -1966,7 +1990,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID 知识库 ID
@ -1999,7 +2023,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID 知识库 ID
@ -2032,7 +2056,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Params ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID 知识库 ID
@ -2073,7 +2097,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
/> />
<Row> <Row>
<Col> <Col>
### Query ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string' key='dataset_id'> <Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID 知识库 ID

@ -85,7 +85,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
setAppDetail(app) setAppDetail(app)
mutateApps() mutateApps()
} }
catch (e) { catch {
notify({ type: 'error', message: t('app.editFailed') }) notify({ type: 'error', message: t('app.editFailed') })
} }
}, [appDetail, mutateApps, notify, setAppDetail, t]) }, [appDetail, mutateApps, notify, setAppDetail, t])
@ -112,7 +112,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
onPlanInfoChanged() onPlanInfoChanged()
getRedirection(true, newApp, replace) getRedirection(true, newApp, replace)
} }
catch (e) { catch {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
} }
} }
@ -131,7 +131,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
a.download = `${appDetail.name}.yml` a.download = `${appDetail.name}.yml`
a.click() a.click()
} }
catch (e) { catch {
notify({ type: 'error', message: t('app.exportFailed') }) notify({ type: 'error', message: t('app.exportFailed') })
} }
} }
@ -152,7 +152,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
} }
setSecretEnvList(list) setSecretEnvList(list)
} }
catch (e) { catch {
notify({ type: 'error', message: t('app.exportFailed') }) notify({ type: 'error', message: t('app.exportFailed') })
} }
} }
@ -175,7 +175,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
}) })
} }
setShowConfirmDelete(false) setShowConfirmDelete(false)
}, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t]) }, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, setAppDetail, t])
const { isCurrentWorkspaceEditor } = useAppContext() const { isCurrentWorkspaceEditor } = useAppContext()

@ -1,7 +1,6 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import { RiLayoutRight2Line } from '@remixicon/react' import { RiLayoutLeft2Line, RiLayoutRight2Line } from '@remixicon/react'
import { LayoutRight2LineMod } from '../base/icons/src/public/knowledge'
import NavLink from './navLink' import NavLink from './navLink'
import type { NavIcon } from './navLink' import type { NavIcon } from './navLink'
import AppBasic from './basic' import AppBasic from './basic'
@ -108,13 +107,13 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati
`} `}
> >
<div <div
className='flex h-6 w-6 cursor-pointer items-center justify-center text-gray-500' className='flex h-6 w-6 cursor-pointer items-center justify-center'
onClick={() => handleToggle(appSidebarExpand)} onClick={() => handleToggle(appSidebarExpand)}
> >
{ {
expand expand
? <RiLayoutRight2Line className='h-5 w-5 text-components-menu-item-text' /> ? <RiLayoutRight2Line className='h-5 w-5 text-components-menu-item-text' />
: <LayoutRight2LineMod className='h-5 w-5 text-components-menu-item-text' /> : <RiLayoutLeft2Line className='h-5 w-5 text-components-menu-item-text' />
} }
</div> </div>
</div> </div>

@ -56,7 +56,7 @@ const AddAnnotationModal: FC<Props> = ({
try { try {
await onAdd(payload) await onAdd(payload)
} }
catch (e) { catch {
} }
setIsSaving(false) setIsSaving(false)

@ -90,7 +90,7 @@ const Annotation: FC<Props> = ({
setList(data as AnnotationItem[]) setList(data as AnnotationItem[])
setTotal(total) setTotal(total)
} }
catch (e) { catch {
} }
setIsLoading(false) setIsLoading(false)

@ -55,7 +55,7 @@ const ViewAnnotationModal: FC<Props> = ({
setHitHistoryList(data as HitHistoryItem[]) setHitHistoryList(data as HitHistoryItem[])
setTotal(total) setTotal(total)
} }
catch (e) { catch {
} }
} }

@ -241,7 +241,8 @@ const Prompt: FC<ISimplePromptInput> = ({
selectable: !hasSetBlockStatus.query, selectable: !hasSetBlockStatus.query,
}} }}
onChange={(value) => { onChange={(value) => {
handleChange?.(value, []) if (handleChange)
handleChange(value, [])
}} }}
onBlur={() => { onBlur={() => {
handleChange(promptTemplate, getVars(promptTemplate)) handleChange(promptTemplate, getVars(promptTemplate))

@ -73,7 +73,7 @@ const AgentTools: FC = () => {
formattingChangedDispatcher() formattingChangedDispatcher()
} }
const handleToolAuthSetting = (value: any) => { const handleToolAuthSetting = (value: AgentToolWithMoreInfo) => {
const newModelConfig = produce(modelConfig, (draft) => { const newModelConfig = produce(modelConfig, (draft) => {
const tool = (draft.agentConfig.tools).find((item: any) => item.provider_id === value?.collection?.id && item.tool_name === value?.tool_name) const tool = (draft.agentConfig.tools).find((item: any) => item.provider_id === value?.collection?.id && item.tool_name === value?.tool_name)
if (tool) if (tool)
@ -121,7 +121,7 @@ const AgentTools: FC = () => {
} }
headerRight={ headerRight={
<div className='flex items-center'> <div className='flex items-center'>
<div className='text-xs font-normal leading-[18px] text-text-tertiary'>{tools.filter((item: any) => !!item.enabled).length}/{tools.length}&nbsp;{t('appDebug.agent.tools.enabled')}</div> <div className='text-xs font-normal leading-[18px] text-text-tertiary'>{tools.filter(item => !!item.enabled).length}/{tools.length}&nbsp;{t('appDebug.agent.tools.enabled')}</div>
{tools.length < MAX_TOOLS_NUM && ( {tools.length < MAX_TOOLS_NUM && (
<> <>
<div className='ml-3 mr-1 h-3.5 w-px bg-divider-regular'></div> <div className='ml-3 mr-1 h-3.5 w-px bg-divider-regular'></div>
@ -273,7 +273,7 @@ const AgentTools: FC = () => {
{isShowSettingTool && ( {isShowSettingTool && (
<SettingBuiltInTool <SettingBuiltInTool
toolName={currentTool?.tool_name as string} toolName={currentTool?.tool_name as string}
setting={currentTool?.tool_parameters as any} setting={currentTool?.tool_parameters}
collection={currentTool?.collection as Collection} collection={currentTool?.collection as Collection}
isBuiltIn={currentTool?.collection?.type === CollectionType.builtIn} isBuiltIn={currentTool?.collection?.type === CollectionType.builtIn}
isModel={currentTool?.collection?.type === CollectionType.model} isModel={currentTool?.collection?.type === CollectionType.model}
@ -291,7 +291,7 @@ const AgentTools: FC = () => {
type: 'success', type: 'success',
message: t('common.api.actionSuccess'), message: t('common.api.actionSuccess'),
}) })
handleToolAuthSetting(currentTool as any) handleToolAuthSetting(currentTool)
setShowSettingAuth(false) setShowSettingAuth(false)
}} }}
/> />

@ -56,8 +56,8 @@ const SettingBuiltInTool: FC<Props> = ({
const [tools, setTools] = useState<Tool[]>([]) const [tools, setTools] = useState<Tool[]>([])
const currTool = tools.find(tool => tool.name === toolName) const currTool = tools.find(tool => tool.name === toolName)
const formSchemas = currTool ? toolParametersToFormSchemas(currTool.parameters) : [] const formSchemas = currTool ? toolParametersToFormSchemas(currTool.parameters) : []
const infoSchemas = formSchemas.filter((item: any) => item.form === 'llm') const infoSchemas = formSchemas.filter(item => item.form === 'llm')
const settingSchemas = formSchemas.filter((item: any) => item.form !== 'llm') const settingSchemas = formSchemas.filter(item => item.form !== 'llm')
const hasSetting = settingSchemas.length > 0 const hasSetting = settingSchemas.length > 0
const [tempSetting, setTempSetting] = useState(setting) const [tempSetting, setTempSetting] = useState(setting)
const [currType, setCurrType] = useState('info') const [currType, setCurrType] = useState('info')
@ -88,7 +88,7 @@ const SettingBuiltInTool: FC<Props> = ({
setTempSetting(addDefaultValue(setting, formSchemas)) setTempSetting(addDefaultValue(setting, formSchemas))
} }
} }
catch (e) { } catch { }
setIsLoading(false) setIsLoading(false)
})() })()
}, [collection?.name, collection?.id, collection?.type]) }, [collection?.name, collection?.id, collection?.type])
@ -99,7 +99,7 @@ const SettingBuiltInTool: FC<Props> = ({
const isValid = (() => { const isValid = (() => {
let valid = true let valid = true
settingSchemas.forEach((item: any) => { settingSchemas.forEach((item) => {
if (item.required && !tempSetting[item.name]) if (item.required && !tempSetting[item.name])
valid = false valid = false
}) })
@ -120,7 +120,7 @@ const SettingBuiltInTool: FC<Props> = ({
<div className=''> <div className=''>
{infoSchemas.length > 0 && ( {infoSchemas.length > 0 && (
<div className='space-y-1 py-2'> <div className='space-y-1 py-2'>
{infoSchemas.map((item: any, index) => ( {infoSchemas.map((item, index) => (
<div key={index} className='py-1'> <div key={index} className='py-1'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<div className='code-sm-semibold text-text-secondary'>{item.label[language]}</div> <div className='code-sm-semibold text-text-secondary'>{item.label[language]}</div>
@ -147,7 +147,7 @@ const SettingBuiltInTool: FC<Props> = ({
<Form <Form
value={tempSetting} value={tempSetting}
onChange={setTempSetting} onChange={setTempSetting}
formSchemas={settingSchemas as any} formSchemas={settingSchemas}
isEditMode={false} isEditMode={false}
showOnVariableMap={{}} showOnVariableMap={{}}
validating={false} validating={false}

@ -368,8 +368,8 @@ const ConfigContent: FC<Props> = ({
provider={model?.provider} provider={model?.provider}
completionParams={model?.completion_params} completionParams={model?.completion_params}
modelId={model?.name} modelId={model?.name}
setModel={onSingleRetrievalModelChange as any} setModel={onSingleRetrievalModelChange}
onCompletionParamsChange={onSingleRetrievalModelParamsChange as any} onCompletionParamsChange={onSingleRetrievalModelParamsChange}
hideDebugWithMultipleModel hideDebugWithMultipleModel
debugWithMultipleModel={false} debugWithMultipleModel={false}
/> />

@ -150,7 +150,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
retrieval_model_dict: retrievalConfig, retrieval_model_dict: retrievalConfig,
}) })
} }
catch (e) { catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
} }
finally { finally {

@ -8,6 +8,7 @@ import ModelParameterModal from '@/app/components/header/account-setting/model-p
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' 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 ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
import { import {
type FormValue,
MODEL_STATUS_TEXT, MODEL_STATUS_TEXT,
ModelStatusEnum, ModelStatusEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -45,7 +46,7 @@ const ModelParameterTrigger: FC<ModelParameterTriggerProps> = ({
} }
onMultipleModelConfigsChange(true, newModelConfigs) onMultipleModelConfigsChange(true, newModelConfigs)
} }
const handleParamsChange = (params: any) => { const handleParamsChange = (params: FormValue) => {
const newModelConfigs = [...multipleModelConfigs] const newModelConfigs = [...multipleModelConfigs]
newModelConfigs[index] = { newModelConfigs[index] = {
...newModelConfigs[index], ...newModelConfigs[index],

@ -21,6 +21,7 @@ import { useFeatures } from '@/app/components/base/features/hooks'
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils' import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
import type { InputForm } from '@/app/components/base/chat/chat/type' import type { InputForm } from '@/app/components/base/chat/chat/type'
import { canFindTool } from '@/utils' import { canFindTool } from '@/utils'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
type DebugWithSingleModelProps = { type DebugWithSingleModelProps = {
checkCanSend?: () => boolean checkCanSend?: () => boolean
@ -125,10 +126,14 @@ const DebugWithSingleModel = (
) )
}, [appId, chatList, checkCanSend, completionParams, config, handleSend, inputs, modelConfig.mode, modelConfig.model_id, modelConfig.provider, textGenerationModelList]) }, [appId, chatList, checkCanSend, completionParams, config, handleSend, inputs, modelConfig.mode, modelConfig.model_id, modelConfig.provider, textGenerationModelList])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => { const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)! const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) doSend(editedQuestion ? editedQuestion.message : question.content,
editedQuestion ? editedQuestion.files : question.message_files,
true,
isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null,
)
}, [chatList, doSend]) }, [chatList, doSend])
const allToolIcons = useMemo(() => { const allToolIcons = useMemo(() => {

@ -227,7 +227,7 @@ const Configuration: FC = () => {
}, [modelModeType]) }, [modelModeType])
const [dataSets, setDataSets] = useState<DataSet[]>([]) const [dataSets, setDataSets] = useState<DataSet[]>([])
const contextVar = modelConfig.configs.prompt_variables.find((item: any) => item.is_context_var)?.key const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
const hasSetContextVar = !!contextVar const hasSetContextVar = !!contextVar
const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false) const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false)
const selectedIds = dataSets.map(item => item.id) const selectedIds = dataSets.map(item => item.id)
@ -245,7 +245,7 @@ const Configuration: FC = () => {
formattingChangedDispatcher() formattingChangedDispatcher()
let newDatasets = data let newDatasets = data
if (data.find(item => !item.name)) { // has not loaded selected dataset if (data.find(item => !item.name)) { // has not loaded selected dataset
const newSelected = produce(data, (draft: any) => { const newSelected = produce(data, (draft) => {
data.forEach((item, index) => { data.forEach((item, index) => {
if (!item.name) { // not fetched database if (!item.name) { // not fetched database
const newItem = dataSets.find(i => i.id === item.id) const newItem = dataSets.find(i => i.id === item.id)
@ -513,7 +513,7 @@ const Configuration: FC = () => {
if (modelConfig.chat_prompt_config && modelConfig.chat_prompt_config.prompt.length > 0) if (modelConfig.chat_prompt_config && modelConfig.chat_prompt_config.prompt.length > 0)
setChatPromptConfig(modelConfig.chat_prompt_config) setChatPromptConfig(modelConfig.chat_prompt_config)
else else
setChatPromptConfig(clone(DEFAULT_CHAT_PROMPT_CONFIG) as any) setChatPromptConfig(clone(DEFAULT_CHAT_PROMPT_CONFIG))
setCompletionPromptConfig(modelConfig.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any) setCompletionPromptConfig(modelConfig.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any)
setCanReturnToSimpleMode(false) setCanReturnToSimpleMode(false)
} }

@ -79,7 +79,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
} }
const onClear = () => { const onClear = () => {
const newInputs: Record<string, any> = {} const newInputs: Inputs = {}
promptVariables.forEach((item) => { promptVariables.forEach((item) => {
newInputs[item.key] = '' newInputs[item.key] = ''
}) })

@ -151,7 +151,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
return return
} }
if (localeData.variable && !/[a-zA-Z_][a-zA-Z0-9_]{0,29}/g.test(localeData.variable)) { if (localeData.variable && !/[a-zA-Z_]\w{0,29}/g.test(localeData.variable)) {
notify({ type: 'error', message: t('appDebug.varKeyError.notValid', { key: t('appDebug.feature.tools.modal.variableName.title') }) }) notify({ type: 'error', message: t('appDebug.varKeyError.notValid', { key: t('appDebug.feature.tools.modal.variableName.title') }) })
return return
} }

@ -153,7 +153,7 @@ const Apps = ({
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceEditor, { id: app.app_id!, mode }, push) getRedirection(isCurrentWorkspaceEditor, { id: app.app_id!, mode }, push)
} }
catch (e) { catch {
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
} }
} }

@ -90,7 +90,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps)
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceEditor, app, push) getRedirection(isCurrentWorkspaceEditor, app, push)
} }
catch (e) { catch {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
} }
isCreatingRef.current = false isCreatingRef.current = false
@ -287,7 +287,6 @@ type AppTypeCardProps = {
onClick: () => void onClick: () => void
} }
function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardProps) { function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardProps) {
const { t } = useTranslation()
return <div return <div
className={ className={
cn(`relative box-content h-[84px] w-[191px] cursor-pointer rounded-xl cn(`relative box-content h-[84px] w-[191px] cursor-pointer rounded-xl

@ -30,7 +30,7 @@ const LogAnnotation: FC<Props> = ({
{ value: PageType.log, text: t('appLog.title') }, { value: PageType.log, text: t('appLog.title') },
{ value: PageType.annotation, text: t('appAnnotation.title') }, { value: PageType.annotation, text: t('appAnnotation.title') },
] ]
}, [appDetail]) }, [appDetail?.mode, t])
if (!appDetail) { if (!appDetail) {
return ( return (

@ -547,7 +547,7 @@ const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: st
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
return true return true
} }
catch (err) { catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
return false return false
} }
@ -560,7 +560,7 @@ const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: st
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
return true return true
} }
catch (err) { catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
return false return false
} }
@ -591,7 +591,7 @@ const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
return true return true
} }
catch (err) { catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
return false return false
} }
@ -603,7 +603,7 @@ const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
return true return true
} }
catch (err) { catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
return false return false
} }

@ -110,7 +110,7 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam
} }
const navigateToChromeUrl = () => { const navigateToChromeUrl = () => {
window.open('https://chrome.google.com/webstore/detail/dify-chatbot/ceehdapohffmjmkdcifjofadiaoeggaf', '_blank') window.open('https://chrome.google.com/webstore/detail/dify-chatbot/ceehdapohffmjmkdcifjofadiaoeggaf', '_blank', 'noopener,noreferrer')
} }
useEffect(() => { useEffect(() => {

@ -82,7 +82,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
removeOriginal ? replace : push, removeOriginal ? replace : push,
) )
} }
catch (e) { catch {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
} }
} }

@ -36,7 +36,7 @@ const AgentLogDetail: FC<AgentLogDetailProps> = ({
const [list, setList] = useState<AgentIteration[]>([]) const [list, setList] = useState<AgentIteration[]>([])
const tools = useMemo(() => { const tools = useMemo(() => {
const res = uniq(flatten(runDetail?.iterations.map((iteration: any) => { const res = uniq(flatten(runDetail?.iterations.map((iteration) => {
return iteration.tool_calls.map((tool: any) => tool.tool_name).filter(Boolean) return iteration.tool_calls.map((tool: any) => tool.tool_name).filter(Boolean)
})).filter(Boolean)) })).filter(Boolean))
return res return res

@ -33,7 +33,7 @@ export class AudioPlayerManager {
this.audioPlayers.cacheBuffers = [] this.audioPlayers.cacheBuffers = []
this.audioPlayers.sourceBuffer?.abort() this.audioPlayers.sourceBuffer?.abort()
} }
catch (e) { catch {
} }
} }

@ -125,7 +125,7 @@ export default class AudioPlayer {
this.receiveAudioData(value) this.receiveAudioData(value)
} }
} }
catch (error) { catch {
this.isLoadData = false this.isLoadData = false
this.callback && this.callback('error') this.callback && this.callback('error')
} }

@ -29,7 +29,7 @@ const AudioBtn = ({
const params = useParams() const params = useParams()
const pathname = usePathname() const pathname = usePathname()
const audio_finished_call = (event: string): any => { const audio_finished_call = (event: string): void => {
switch (event) { switch (event) {
case 'ended': case 'ended':
setAudioState('ended') setAudioState('ended')
@ -97,7 +97,7 @@ const AudioBtn = ({
</div> </div>
) )
: ( : (
<div className={`flex h-full w-full items-center justify-center rounded-md ${!isAudition ? 'hover:bg-gray-50' : 'hover:bg-gray-50'}`}> <div className={'flex h-full w-full items-center justify-center rounded-md hover:bg-gray-50'}>
<div className={`h-4 w-4 ${(audioState === 'playing') ? s.pauseIcon : s.playIcon}`}></div> <div className={`h-4 w-4 ${(audioState === 'playing') ? s.pauseIcon : s.playIcon}`}></div>
</div> </div>
)} )}

@ -124,7 +124,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
setWaveformData(normalizedWaveform) setWaveformData(normalizedWaveform)
setIsAudioAvailable(true) setIsAudioAvailable(true)
} }
catch (error) { catch {
const waveform: number[] = [] const waveform: number[] = []
let prevValue = Math.random() let prevValue = Math.random()

@ -22,6 +22,7 @@ import AnswerIcon from '@/app/components/base/answer-icon'
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions' import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types'
const ChatWrapper = () => { const ChatWrapper = () => {
const { const {
@ -139,22 +140,16 @@ const ChatWrapper = () => {
isPublicAPI: !isInstalledApp, isPublicAPI: !isInstalledApp,
}, },
) )
}, [ }, [chatList, handleNewConversationCompleted, handleSend, currentConversationId, currentConversationInputs, newConversationInputs, isInstalledApp, appId])
chatList,
handleNewConversationCompleted,
handleSend,
currentConversationId,
currentConversationItem,
currentConversationInputs,
newConversationInputs,
isInstalledApp,
appId,
])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => { const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)! const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) doSend(editedQuestion ? editedQuestion.message : question.content,
editedQuestion ? editedQuestion.files : question.message_files,
true,
isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null,
)
}, [chatList, doSend]) }, [chatList, doSend])
const messageList = useMemo(() => { const messageList = useMemo(() => {

@ -2,7 +2,7 @@ import type {
FC, FC,
ReactNode, ReactNode,
} from 'react' } from 'react'
import { memo, useEffect, useRef, useState } from 'react' import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { import type {
ChatConfig, ChatConfig,
@ -19,9 +19,9 @@ import Citation from '@/app/components/base/chat/chat/citation'
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item' import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
import type { AppData } from '@/models/share' import type { AppData } from '@/models/share'
import AnswerIcon from '@/app/components/base/answer-icon' import AnswerIcon from '@/app/components/base/answer-icon'
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { FileList } from '@/app/components/base/file-uploader' import { FileList } from '@/app/components/base/file-uploader'
import ContentSwitch from '../content-switch'
type AnswerProps = { type AnswerProps = {
item: ChatItem item: ChatItem
@ -100,12 +100,19 @@ const Answer: FC<AnswerProps> = ({
} }
}, []) }, [])
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
if (direction === 'prev')
item.prevSibling && switchSibling?.(item.prevSibling)
else
item.nextSibling && switchSibling?.(item.nextSibling)
}, [switchSibling, item.prevSibling, item.nextSibling])
return ( return (
<div className='mb-2 flex last:mb-0'> <div className='mb-2 flex last:mb-0'>
<div className='relative h-10 w-10 shrink-0'> <div className='relative h-10 w-10 shrink-0'>
{answerIcon || <AnswerIcon />} {answerIcon || <AnswerIcon />}
{responding && ( {responding && (
<div className='absolute -left-[3px] -top-[3px] flex h-4 w-4 items-center rounded-full border-[0.5px] border-divider-subtle bg-background-section-burn pl-[6px] shadow-xs'> <div className='absolute left-[-3px] top-[-3px] flex h-4 w-4 items-center rounded-full border-[0.5px] border-divider-subtle bg-background-section-burn pl-[6px] shadow-xs'>
<LoadingAnim type='avatar' /> <LoadingAnim type='avatar' />
</div> </div>
)} )}
@ -208,23 +215,17 @@ const Answer: FC<AnswerProps> = ({
<Citation data={citation} showHitInfo={config?.supportCitationHitInfo} /> <Citation data={citation} showHitInfo={config?.supportCitationHitInfo} />
) )
} }
{item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && <div className="flex items-center justify-center pt-3.5 text-sm"> {
<button item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && (
className={`${item.prevSibling ? 'opacity-100' : 'opacity-30'}`} <ContentSwitch
disabled={!item.prevSibling} count={item.siblingCount}
onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)} currentIndex={item.siblingIndex}
> prevDisabled={!item.prevSibling}
<ChevronRight className="h-[14px] w-[14px] rotate-180 text-text-primary" /> nextDisabled={!item.nextSibling}
</button> switchSibling={handleSwitchSibling}
<span className="px-2 text-xs text-text-primary">{item.siblingIndex + 1} / {item.siblingCount}</span> />
<button )
className={`${item.nextSibling ? 'opacity-100' : 'opacity-30'}`} }
disabled={!item.nextSibling}
onClick={() => item.nextSibling && switchSibling?.(item.nextSibling)}
>
<ChevronRight className="h-[14px] w-[14px] text-text-primary" />
</button>
</div>}
</div> </div>
</div> </div>
<More more={more} /> <More more={more} />

@ -0,0 +1,39 @@
import { ChevronRight } from '../../icons/src/vender/line/arrows'
export default function ContentSwitch({
count,
currentIndex,
prevDisabled,
nextDisabled,
switchSibling,
}: {
count?: number
currentIndex?: number
prevDisabled: boolean
nextDisabled: boolean
switchSibling: (direction: 'prev' | 'next') => void
}) {
return (
count && count > 1 && currentIndex !== undefined && (
<div className="flex items-center justify-center pt-3.5 text-sm">
<button
className={`${prevDisabled ? 'opacity-30' : 'opacity-100'}`}
disabled={prevDisabled}
onClick={() => !prevDisabled && switchSibling('prev')}
>
<ChevronRight className="h-[14px] w-[14px] rotate-180 text-text-primary" />
</button>
<span className="px-2 text-xs text-text-primary">
{currentIndex + 1} / {count}
</span>
<button
className={`${nextDisabled ? 'opacity-30' : 'opacity-100'}`}
disabled={nextDisabled}
onClick={() => !nextDisabled && switchSibling('next')}
>
<ChevronRight className="h-[14px] w-[14px] text-text-primary" />
</button>
</div>
)
)
}

@ -512,7 +512,7 @@ export const useChat = (
responseItem.workflowProcess!.tracing!.push({ responseItem.workflowProcess!.tracing!.push({
...iterationStartedData, ...iterationStartedData,
status: WorkflowRunningStatus.Running, status: WorkflowRunningStatus.Running,
} as any) })
updateCurrentQAOnTree({ updateCurrentQAOnTree({
placeholderQuestionId, placeholderQuestionId,
questionItem, questionItem,
@ -528,7 +528,7 @@ export const useChat = (
...tracing[iterationIndex], ...tracing[iterationIndex],
...iterationFinishedData, ...iterationFinishedData,
status: WorkflowRunningStatus.Succeeded, status: WorkflowRunningStatus.Succeeded,
} as any }
updateCurrentQAOnTree({ updateCurrentQAOnTree({
placeholderQuestionId, placeholderQuestionId,
@ -547,7 +547,7 @@ export const useChat = (
responseItem.workflowProcess!.tracing!.push({ responseItem.workflowProcess!.tracing!.push({
...nodeStartedData, ...nodeStartedData,
status: WorkflowRunningStatus.Running, status: WorkflowRunningStatus.Running,
} as any) })
updateCurrentQAOnTree({ updateCurrentQAOnTree({
placeholderQuestionId, placeholderQuestionId,
questionItem, questionItem,
@ -590,7 +590,7 @@ export const useChat = (
responseItem.workflowProcess!.tracing!.push({ responseItem.workflowProcess!.tracing!.push({
...loopStartedData, ...loopStartedData,
status: WorkflowRunningStatus.Running, status: WorkflowRunningStatus.Running,
} as any) })
updateCurrentQAOnTree({ updateCurrentQAOnTree({
placeholderQuestionId, placeholderQuestionId,
questionItem, questionItem,
@ -606,7 +606,7 @@ export const useChat = (
...tracing[loopIndex], ...tracing[loopIndex],
...loopFinishedData, ...loopFinishedData,
status: WorkflowRunningStatus.Succeeded, status: WorkflowRunningStatus.Succeeded,
} as any }
updateCurrentQAOnTree({ updateCurrentQAOnTree({
placeholderQuestionId, placeholderQuestionId,

@ -196,7 +196,8 @@ const Chat: FC<ChatProps> = ({
const chatContainer = chatContainerRef.current const chatContainer = chatContainerRef.current
if (chatContainer) { if (chatContainer) {
const setUserScrolled = () => { const setUserScrolled = () => {
if (chatContainer) // eslint-disable-next-line sonarjs/no-gratuitous-expressions
if (chatContainer) // its in event callback, chatContainer may be null
userScrolledRef.current = chatContainer.scrollHeight - chatContainer.scrollTop > chatContainer.clientHeight userScrolledRef.current = chatContainer.scrollHeight - chatContainer.scrollTop > chatContainer.clientHeight
} }
chatContainer.addEventListener('scroll', setUserScrolled) chatContainer.addEventListener('scroll', setUserScrolled)
@ -207,7 +208,7 @@ const Chat: FC<ChatProps> = ({
useEffect(() => { useEffect(() => {
if (!sidebarCollapseState) if (!sidebarCollapseState)
setTimeout(() => handleWindowResize(), 200) setTimeout(() => handleWindowResize(), 200)
}, [sidebarCollapseState]) }, [handleWindowResize, sidebarCollapseState])
const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
@ -264,6 +265,7 @@ const Chat: FC<ChatProps> = ({
item={item} item={item}
questionIcon={questionIcon} questionIcon={questionIcon}
theme={themeBuilder?.theme} theme={themeBuilder?.theme}
switchSibling={switchSibling}
/> />
) )
}) })

@ -4,46 +4,137 @@ import type {
} from 'react' } from 'react'
import { import {
memo, memo,
useCallback,
useState,
} from 'react' } from 'react'
import type { ChatItem } from '../types' import type { ChatItem } from '../types'
import type { Theme } from '../embedded-chatbot/theme/theme-context' import type { Theme } from '../embedded-chatbot/theme/theme-context'
import { CssTransform } from '../embedded-chatbot/theme/utils' import { CssTransform } from '../embedded-chatbot/theme/utils'
import ContentSwitch from './content-switch'
import { User } from '@/app/components/base/icons/src/public/avatar' import { User } from '@/app/components/base/icons/src/public/avatar'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import { FileList } from '@/app/components/base/file-uploader' import { FileList } from '@/app/components/base/file-uploader'
import ActionButton from '../../action-button'
import { RiClipboardLine, RiEditLine } from '@remixicon/react'
import Toast from '../../toast'
import copy from 'copy-to-clipboard'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import Textarea from 'react-textarea-autosize'
import Button from '../../button'
import { useChatContext } from './context'
type QuestionProps = { type QuestionProps = {
item: ChatItem item: ChatItem
questionIcon?: ReactNode questionIcon?: ReactNode
theme: Theme | null | undefined theme: Theme | null | undefined
switchSibling?: (siblingMessageId: string) => void
} }
const Question: FC<QuestionProps> = ({ const Question: FC<QuestionProps> = ({
item, item,
questionIcon, questionIcon,
theme, theme,
switchSibling,
}) => { }) => {
const { t } = useTranslation()
const { const {
content, content,
message_files, message_files,
} = item } = item
const {
onRegenerate,
} = useChatContext()
const [isEditing, setIsEditing] = useState(false)
const [editedContent, setEditedContent] = useState(content)
const handleEdit = useCallback(() => {
setIsEditing(true)
setEditedContent(content)
}, [content])
const handleResend = useCallback(() => {
setIsEditing(false)
onRegenerate?.(item, { message: editedContent, files: message_files })
}, [editedContent, message_files, item, onRegenerate])
const handleCancelEditing = useCallback(() => {
setIsEditing(false)
setEditedContent(content)
}, [content])
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
if (direction === 'prev')
item.prevSibling && switchSibling?.(item.prevSibling)
else
item.nextSibling && switchSibling?.(item.nextSibling)
}, [switchSibling, item.prevSibling, item.nextSibling])
return ( return (
<div className='mb-2 flex justify-end pl-14 last:mb-0'> <div className='mb-2 flex justify-end pl-14 last:mb-0'>
<div className='group relative mr-4 max-w-full'> <div className={cn('group relative mr-4 flex max-w-full items-start', isEditing && 'flex-1')}>
<div className={cn('mr-2 gap-1', isEditing ? 'hidden' : 'flex')}>
<div className="
absolutegap-0.5 hidden rounded-[10px] border-[0.5px] border-components-actionbar-border
bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex
">
<ActionButton onClick={() => {
copy(content)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<RiClipboardLine className='h-4 w-4' />
</ActionButton>
<ActionButton onClick={handleEdit}>
<RiEditLine className='h-4 w-4' />
</ActionButton>
</div>
</div>
<div <div
className='rounded-2xl bg-[#D1E9FF]/50 px-4 py-3 text-sm text-gray-900' className='w-full rounded-2xl bg-[#D1E9FF]/50 px-4 py-3 text-sm text-gray-900'
style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}} style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}}
> >
{ {
!!message_files?.length && ( !!message_files?.length && (
<FileList <FileList
className='mb-2'
files={message_files} files={message_files}
showDeleteAction={false} showDeleteAction={false}
showDownloadAction={true} showDownloadAction={true}
/> />
) )
} }
<Markdown content={content} /> { !isEditing
? <Markdown content={content} />
: <div className="
flex flex-col gap-2 rounded-xl
border border-components-chat-input-border bg-components-panel-bg-blur p-[9px] shadow-md
">
<div className="max-h-[158px] overflow-y-auto overflow-x-hidden">
<Textarea
className={cn(
'body-lg-regular w-full p-1 leading-6 text-text-tertiary outline-none',
)}
autoFocus
minRows={1}
value={editedContent}
onChange={e => setEditedContent(e.target.value)}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant='ghost' onClick={handleCancelEditing}>{t('common.operation.cancel')}</Button>
<Button variant='primary' onClick={handleResend}>{t('common.chat.resend')}</Button>
</div>
</div> }
{ !isEditing && <ContentSwitch
count={item.siblingCount}
currentIndex={item.siblingIndex}
prevDisabled={!item.prevSibling}
nextDisabled={!item.nextSibling}
switchSibling={handleSwitchSibling}
/>}
</div> </div>
<div className='mt-1 h-[18px]' /> <div className='mt-1 h-[18px]' />
</div> </div>

@ -14,7 +14,7 @@ function getValue(value: string, isValueArray: boolean, index: number) {
try { try {
return JSON.parse(value)[index] return JSON.parse(value)[index]
} }
catch (e) { catch {
} }
} }
return value return value
@ -29,7 +29,7 @@ const Thought: FC<IThoughtProps> = ({
if (Array.isArray(JSON.parse(thought.tool))) if (Array.isArray(JSON.parse(thought.tool)))
return [JSON.parse(thought.tool), true] return [JSON.parse(thought.tool), true]
} }
catch (e) { catch {
} }
return [[thought.tool], false] return [[thought.tool], false]
})() })()

@ -24,6 +24,7 @@ import AnswerIcon from '@/app/components/base/answer-icon'
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions' import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types'
const ChatWrapper = () => { const ChatWrapper = () => {
const { const {
@ -138,21 +139,16 @@ const ChatWrapper = () => {
isPublicAPI: !isInstalledApp, isPublicAPI: !isInstalledApp,
}, },
) )
}, [ }, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
chatList,
handleNewConversationCompleted,
handleSend,
currentConversationId,
currentConversationItem,
newConversationInputs,
isInstalledApp,
appId,
])
const doRegenerate = useCallback((chatItem: ChatItemInTree) => { const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = chatList.find(item => item.id === chatItem.parentMessageId)! const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) doSend(editedQuestion ? editedQuestion.message : question.content,
editedQuestion ? editedQuestion.files : question.message_files,
true,
isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null,
)
}, [chatList, doSend]) }, [chatList, doSend])
const messageList = useMemo(() => { const messageList = useMemo(() => {

@ -24,7 +24,6 @@ const InputsFormContent = ({ showTip }: Props) => {
handleNewConversationInputsChange, handleNewConversationInputsChange,
} = useEmbeddedChatbotContext() } = useEmbeddedChatbotContext()
const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputs const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputs
const readonly = !!currentConversationId
const handleFormChange = useCallback((variable: string, value: any) => { const handleFormChange = useCallback((variable: string, value: any) => {
setCurrentConversationInputs({ setCurrentConversationInputs({

@ -12,6 +12,7 @@ import useAnnotationConfig from '@/app/components/base/features/new-feature-pane
import ConfigParamModal from '@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal' import ConfigParamModal from '@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal'
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal' import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
import { ANNOTATION_DEFAULT } from '@/config' import { ANNOTATION_DEFAULT } from '@/config'
import type { AnnotationReplyConfig } from '@/models/debug'
type Props = { type Props = {
disabled?: boolean disabled?: boolean
@ -30,7 +31,7 @@ const AnnotationReply = ({
const featuresStore = useFeaturesStore() const featuresStore = useFeaturesStore()
const annotationReply = useFeatures(s => s.features.annotationReply) const annotationReply = useFeatures(s => s.features.annotationReply)
const updateAnnotationReply = useCallback((newConfig: any) => { const updateAnnotationReply = useCallback((newConfig: AnnotationReplyConfig) => {
const { const {
features, features,
setFeatures, setFeatures,

@ -245,7 +245,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
> >
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<div className='title-2xl-semi-bold text-text-primary'>{t('appDebug.feature.moderation.modal.title')}</div> <div className='title-2xl-semi-bold text-text-primary'>{t('appDebug.feature.moderation.modal.title')}</div>
<div className='cursor-pointer p-1' onClick={onCancel}><RiCloseLine className='h-4 w-4 text-text-tertiary'/></div> <div className='cursor-pointer p-1' onClick={onCancel}><RiCloseLine className='h-4 w-4 text-text-tertiary' /></div>
</div> </div>
<div className='py-2'> <div className='py-2'>
<div className='text-sm font-medium leading-9 text-text-primary'> <div className='text-sm font-medium leading-9 text-text-primary'>
@ -348,14 +348,14 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }} config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('inputs_config', config)} onConfigChange={config => handleDataContentChange('inputs_config', config)}
info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''} info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''}
showPreset={!(localeData.type === 'api')} showPreset={localeData.type !== 'api'}
/> />
<ModerationContent <ModerationContent
title={t('appDebug.feature.moderation.modal.content.output') || ''} title={t('appDebug.feature.moderation.modal.content.output') || ''}
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }} config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('outputs_config', config)} onConfigChange={config => handleDataContentChange('outputs_config', config)}
info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''} info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''}
showPreset={!(localeData.type === 'api')} showPreset={localeData.type !== 'api'}
/> />
<div className='mb-8 mt-1 text-xs font-medium text-text-tertiary'>{t('appDebug.feature.moderation.modal.content.condition')}</div> <div className='mb-8 mt-1 text-xs font-medium text-text-tertiary'>{t('appDebug.feature.moderation.modal.content.condition')}</div>
<div className='flex items-center justify-end'> <div className='flex items-center justify-end'>

@ -128,7 +128,7 @@ const CodeBlock: any = memo(({ inline, className, children, ...props }: any) =>
try { try {
return JSON.parse(String(children).replace(/\n$/, '')) return JSON.parse(String(children).replace(/\n$/, ''))
} }
catch (error) { } catch { }
} }
return JSON.parse('{"title":{"text":"ECharts error - Wrong JSON format."}}') return JSON.parse('{"title":{"text":"ECharts error - Wrong JSON format."}}')
}, [language, children]) }, [language, children])

@ -1,6 +0,0 @@
.default-page-icon {
width: 20px;
height: 20px;
background: url(../notion-page-selector/assets/notion-page.svg) center center no-repeat;
background-size: cover;
}

@ -1,4 +1,4 @@
import s from './index.module.css' import { RiFileTextLine } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { DataSourceNotionPage } from '@/models/common' import type { DataSourceNotionPage } from '@/models/common'
@ -51,7 +51,7 @@ const NotionIcon = ({
} }
return ( return (
<div className={cn(s['default-page-icon'], className)} /> <RiFileTextLine className={cn('h-5 w-5 text-text-tertiary', className)} />
) )
} }

@ -47,7 +47,7 @@ const WorkflowVariableBlockReplacementBlock = ({
} }
}, []) }, [])
const transformListener = useCallback((textNode: any) => { const transformListener = useCallback((textNode: CustomTextNode) => {
resetReg() resetReg()
return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode) return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode)
}, [createWorkflowVariableBlockNode, getMatch]) }, [createWorkflowVariableBlockNode, getMatch])

@ -51,7 +51,7 @@ export const SVGRenderer = ({ content }: { content: string }) => {
setImagePreview(svgToDataURL(svgElement as Element)) setImagePreview(svgToDataURL(svgElement as Element))
}) })
} }
catch (error) { catch {
if (svgRef.current) if (svgRef.current)
svgRef.current.innerHTML = '<span style="padding: 1rem;">Error rendering SVG. Wait for the image content to complete.</span>' svgRef.current.innerHTML = '<span style="padding: 1rem;">Error rendering SVG. Wait for the image content to complete.</span>'
} }

@ -48,7 +48,7 @@ const TagManagementModal = ({ show, type }: TagManagementModalProps) => {
setName('') setName('')
setPending(false) setPending(false)
} }
catch (e: any) { catch {
notify({ type: 'error', message: t('common.tag.failed') }) notify({ type: 'error', message: t('common.tag.failed') })
setPending(false) setPending(false)
} }

@ -73,7 +73,7 @@ const Panel = (props: PanelProps) => {
setCreating(false) setCreating(false)
onCreate() onCreate()
} }
catch (e: any) { catch {
notify({ type: 'error', message: t('common.tag.failed') }) notify({ type: 'error', message: t('common.tag.failed') })
setCreating(false) setCreating(false)
} }
@ -83,7 +83,7 @@ const Panel = (props: PanelProps) => {
await bindTag(tagIDs, targetID, type) await bindTag(tagIDs, targetID, type)
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
} }
catch (e: any) { catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
} }
} }
@ -92,7 +92,7 @@ const Panel = (props: PanelProps) => {
await unBindTag(tagID, targetID, type) await unBindTag(tagID, targetID, type)
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
} }
catch (e: any) { catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
} }
} }

@ -59,7 +59,7 @@ const TagItemEditor: FC<TagItemEditorProps> = ({
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
setName(name) setName(name)
} }
catch (e: any) { catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
setName(tag.name) setName(tag.name)
const recoverList = tagList.map((tag) => { const recoverList = tagList.map((tag) => {
@ -92,7 +92,7 @@ const TagItemEditor: FC<TagItemEditorProps> = ({
]) ])
setPending(false) setPending(false)
} }
catch (e: any) { catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
setPending(false) setPending(false)
} }

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

Loading…
Cancel
Save