mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat/structured-output
This commit is contained in:
commit
5aa541fe37
|
@ -2,10 +2,10 @@
|
|||
|
||||
npm add -g pnpm@10.8.0
|
||||
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-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-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 && 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-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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/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:
|
||||
name: API Tests
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
|
@ -27,40 +30,44 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Poetry and Python ${{ matrix.python-version }}
|
||||
uses: ./.github/actions/setup-poetry
|
||||
- name: Setup UV and Python
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-lockfile: api/poetry.lock
|
||||
uv-lockfile: api/uv.lock
|
||||
|
||||
- name: Check Poetry lockfile
|
||||
run: |
|
||||
poetry check -C api --lock
|
||||
poetry show -C api
|
||||
- name: Check UV lockfile
|
||||
run: uv lock --project api --check
|
||||
|
||||
- name: Install dependencies
|
||||
run: poetry install -C api --with dev
|
||||
|
||||
- name: Check dependencies in pyproject.toml
|
||||
run: poetry run -P api bash dev/pytest/pytest_artifacts.sh
|
||||
run: uv sync --project api --dev
|
||||
|
||||
- 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
|
||||
# Extract coverage percentage and create a summary
|
||||
TOTAL_COVERAGE=$(python -c 'import json; print(json.load(open("coverage.json"))["totals"]["percent_covered_display"])')
|
||||
|
||||
# Create a detailed coverage summary
|
||||
echo "### Test Coverage Summary :test_tube:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Total Coverage: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
uv run --project api coverage report >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- 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
|
||||
with:
|
||||
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
|
||||
run: dev/run-mypy
|
||||
- name: Run MyPy Checks
|
||||
run: dev/mypy-check
|
||||
|
||||
- name: Set up dotenvs
|
||||
run: |
|
||||
|
@ -80,4 +87,4 @@ jobs:
|
|||
ssrf_proxy
|
||||
|
||||
- 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
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Poetry and Python
|
||||
uses: ./.github/actions/setup-poetry
|
||||
- name: Setup UV and Python
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
poetry-lockfile: api/poetry.lock
|
||||
uv-lockfile: api/uv.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: poetry install -C api
|
||||
run: uv sync --project api
|
||||
|
||||
- name: Prepare middleware env
|
||||
run: |
|
||||
|
@ -54,6 +54,4 @@ jobs:
|
|||
- name: Run DB Migration
|
||||
env:
|
||||
DEBUG: true
|
||||
run: |
|
||||
cd api
|
||||
poetry run python -m flask upgrade-db
|
||||
run: uv run --directory api flask upgrade-db
|
||||
|
|
|
@ -42,6 +42,7 @@ jobs:
|
|||
with:
|
||||
push: false
|
||||
context: "{{defaultContext}}:${{ matrix.context }}"
|
||||
file: "${{ matrix.file }}"
|
||||
platforms: ${{ matrix.platform }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
|
@ -18,7 +18,6 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check changed files
|
||||
|
@ -29,24 +28,27 @@ jobs:
|
|||
api/**
|
||||
.github/workflows/style.yml
|
||||
|
||||
- name: Setup Poetry and Python
|
||||
- name: Setup UV and Python
|
||||
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
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: poetry install -C api --only lint
|
||||
run: uv sync --project api --dev
|
||||
|
||||
- name: Ruff check
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run -C api ruff --version
|
||||
poetry run -C api ruff check ./
|
||||
poetry run -C api ruff format --check ./
|
||||
uv run --directory api ruff --version
|
||||
uv run --directory api ruff check ./
|
||||
uv run --directory api ruff format --check ./
|
||||
|
||||
- name: Dotenv check
|
||||
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
|
||||
if: failure()
|
||||
|
@ -63,7 +65,6 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check changed files
|
||||
|
@ -102,7 +103,6 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check changed files
|
||||
|
@ -133,7 +133,6 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check changed files
|
||||
|
|
|
@ -27,7 +27,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
|
|
|
@ -8,7 +8,7 @@ on:
|
|||
- api/core/rag/datasource/**
|
||||
- docker/**
|
||||
- .github/workflows/vdb-tests.yml
|
||||
- api/poetry.lock
|
||||
- api/uv.lock
|
||||
- api/pyproject.toml
|
||||
|
||||
concurrency:
|
||||
|
@ -29,22 +29,19 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Poetry and Python ${{ matrix.python-version }}
|
||||
uses: ./.github/actions/setup-poetry
|
||||
- name: Setup UV and Python
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-lockfile: api/poetry.lock
|
||||
uv-lockfile: api/uv.lock
|
||||
|
||||
- name: Check Poetry lockfile
|
||||
run: |
|
||||
poetry check -C api --lock
|
||||
poetry show -C api
|
||||
- name: Check UV lockfile
|
||||
run: uv lock --project api --check
|
||||
|
||||
- name: Install dependencies
|
||||
run: poetry install -C api --with dev
|
||||
run: uv sync --project api --dev
|
||||
|
||||
- name: Set up dotenvs
|
||||
run: |
|
||||
|
@ -80,7 +77,7 @@ jobs:
|
|||
elasticsearch
|
||||
|
||||
- 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
|
||||
run: poetry run -P api bash dev/pytest/pytest_vdb.sh
|
||||
run: uv run --project api bash dev/pytest/pytest_vdb.sh
|
||||
|
|
|
@ -23,7 +23,6 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check changed files
|
||||
|
|
|
@ -46,6 +46,7 @@ htmlcov/
|
|||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
coverage.json
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
|
|
|
@ -3,20 +3,11 @@ FROM python:3.12-slim-bookworm AS base
|
|||
|
||||
WORKDIR /app/api
|
||||
|
||||
# Install Poetry
|
||||
ENV POETRY_VERSION=2.0.1
|
||||
# Install uv
|
||||
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 poetry==${POETRY_VERSION} -i https://mirrors.aliyun.com/pypi/simple/
|
||||
RUN pip install --no-cache-dir uv==${UV_VERSION}
|
||||
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
|
||||
# Install Python dependencies
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
RUN poetry install --sync --no-cache --no-root
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN uv sync --locked
|
||||
|
||||
# production stage
|
||||
FROM base AS production
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
## Usage
|
||||
|
||||
> [!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
|
||||
|
||||
|
@ -37,19 +40,19 @@
|
|||
|
||||
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
|
||||
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
|
||||
|
||||
```bash
|
||||
poetry env use 3.12
|
||||
poetry install
|
||||
uv sync --dev
|
||||
```
|
||||
|
||||
6. Run migrate
|
||||
|
@ -57,21 +60,21 @@
|
|||
Before the first launch, migrate the database to the latest version.
|
||||
|
||||
```bash
|
||||
poetry run python -m flask db upgrade
|
||||
uv run flask db upgrade
|
||||
```
|
||||
|
||||
7. Start backend
|
||||
|
||||
```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.
|
||||
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.
|
||||
|
||||
```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
|
||||
|
@ -79,11 +82,11 @@
|
|||
1. Install dependencies for both the backend and the test environment
|
||||
|
||||
```bash
|
||||
poetry install -C api --with dev
|
||||
uv sync --dev
|
||||
```
|
||||
|
||||
2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml`
|
||||
|
||||
```bash
|
||||
poetry run -P api bash dev/pytest/pytest_all_tests.sh
|
||||
uv run -P api bash dev/pytest/pytest_all_tests.sh
|
||||
```
|
||||
|
|
|
@ -139,7 +139,9 @@ class DatasetListApi(DatasetApiResource):
|
|||
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"]),
|
||||
retrieval_model=RetrievalModel(**args["retrieval_model"])
|
||||
if args["retrieval_model"] is not None
|
||||
else None,
|
||||
)
|
||||
except services.errors.dataset.DatasetNameDuplicateError:
|
||||
raise DatasetNameDuplicateError()
|
||||
|
|
|
@ -122,6 +122,8 @@ class SegmentApi(DatasetApiResource):
|
|||
tenant_id=current_user.current_tenant_id,
|
||||
status_list=args["status"],
|
||||
keyword=args["keyword"],
|
||||
page=page,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
response = {
|
||||
|
|
|
@ -191,7 +191,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||
# action is final answer, return final answer directly
|
||||
try:
|
||||
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):
|
||||
final_answer = scratchpad.action.action_input
|
||||
else:
|
||||
|
|
|
@ -52,6 +52,7 @@ class AgentStrategyParameter(PluginParameter):
|
|||
return cast_parameter_value(self, value)
|
||||
|
||||
type: AgentStrategyParameterType = Field(..., description="The type of the parameter")
|
||||
help: Optional[I18nObject] = None
|
||||
|
||||
def init_frontend_parameter(self, value: Any):
|
||||
return init_frontend_parameter(self, self.type, value)
|
||||
|
|
|
@ -48,25 +48,26 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
|
|||
write=dify_config.SSRF_DEFAULT_WRITE_TIME_OUT,
|
||||
)
|
||||
|
||||
if "ssl_verify" not in kwargs:
|
||||
kwargs["ssl_verify"] = HTTP_REQUEST_NODE_SSL_VERIFY
|
||||
|
||||
ssl_verify = kwargs.pop("ssl_verify")
|
||||
|
||||
retries = 0
|
||||
while retries <= max_retries:
|
||||
try:
|
||||
if dify_config.SSRF_PROXY_ALL_URL:
|
||||
with httpx.Client(proxy=dify_config.SSRF_PROXY_ALL_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client:
|
||||
with httpx.Client(proxy=dify_config.SSRF_PROXY_ALL_URL, verify=ssl_verify) as client:
|
||||
response = client.request(method=method, url=url, **kwargs)
|
||||
elif dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL:
|
||||
proxy_mounts = {
|
||||
"http://": httpx.HTTPTransport(
|
||||
proxy=dify_config.SSRF_PROXY_HTTP_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY
|
||||
),
|
||||
"https://": httpx.HTTPTransport(
|
||||
proxy=dify_config.SSRF_PROXY_HTTPS_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY
|
||||
),
|
||||
"http://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTP_URL, verify=ssl_verify),
|
||||
"https://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTPS_URL, verify=ssl_verify),
|
||||
}
|
||||
with httpx.Client(mounts=proxy_mounts, verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client:
|
||||
with httpx.Client(mounts=proxy_mounts, verify=ssl_verify) as client:
|
||||
response = client.request(method=method, url=url, **kwargs)
|
||||
else:
|
||||
with httpx.Client(verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client:
|
||||
with httpx.Client(verify=ssl_verify) as client:
|
||||
response = client.request(method=method, url=url, **kwargs)
|
||||
|
||||
if response.status_code not in STATUS_FORCELIST:
|
||||
|
|
|
@ -131,7 +131,7 @@ def cast_parameter_value(typ: enum.StrEnum, value: Any, /):
|
|||
raise ValueError("The selector must be a dictionary.")
|
||||
return value
|
||||
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.")
|
||||
return value
|
||||
case _:
|
||||
|
@ -147,7 +147,7 @@ def init_frontend_parameter(rule: PluginParameter, type: enum.StrEnum, value: An
|
|||
init frontend parameter by rule
|
||||
"""
|
||||
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
|
||||
parameter_value = rule.default
|
||||
if not parameter_value and rule.required:
|
||||
|
|
|
@ -82,7 +82,7 @@ class BasePluginManager:
|
|||
Make a stream request to the plugin daemon inner API
|
||||
"""
|
||||
response = self._request(method, path, headers, data, params, files, stream=True)
|
||||
for line in response.iter_lines():
|
||||
for line in response.iter_lines(chunk_size=1024 * 8):
|
||||
line = line.decode("utf-8").strip()
|
||||
if line.startswith("data:"):
|
||||
line = line[5:].strip()
|
||||
|
|
|
@ -110,7 +110,62 @@ class PluginToolManager(BasePluginManager):
|
|||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
return response
|
||||
|
||||
class FileChunk:
|
||||
"""
|
||||
Only used for internal processing.
|
||||
"""
|
||||
|
||||
bytes_written: int
|
||||
total_length: int
|
||||
data: bytearray
|
||||
|
||||
def __init__(self, total_length: int):
|
||||
self.bytes_written = 0
|
||||
self.total_length = total_length
|
||||
self.data = bytearray(total_length)
|
||||
|
||||
files: dict[str, FileChunk] = {}
|
||||
for resp in response:
|
||||
if resp.type == ToolInvokeMessage.MessageType.BLOB_CHUNK:
|
||||
assert isinstance(resp.message, ToolInvokeMessage.BlobChunkMessage)
|
||||
# Get blob chunk information
|
||||
chunk_id = resp.message.id
|
||||
total_length = resp.message.total_length
|
||||
blob_data = resp.message.blob
|
||||
is_end = resp.message.end
|
||||
|
||||
# Initialize buffer for this file if it doesn't exist
|
||||
if chunk_id not in files:
|
||||
files[chunk_id] = FileChunk(total_length)
|
||||
|
||||
# If this is the final chunk, yield a complete blob message
|
||||
if is_end:
|
||||
yield ToolInvokeMessage(
|
||||
type=ToolInvokeMessage.MessageType.BLOB,
|
||||
message=ToolInvokeMessage.BlobMessage(blob=files[chunk_id].data),
|
||||
meta=resp.meta,
|
||||
)
|
||||
else:
|
||||
# Check if file is too large (30MB limit)
|
||||
if files[chunk_id].bytes_written + len(blob_data) > 30 * 1024 * 1024:
|
||||
# Delete the file if it's too large
|
||||
del files[chunk_id]
|
||||
# Skip yielding this message
|
||||
raise ValueError("File is too large which reached the limit of 30MB")
|
||||
|
||||
# Check if single chunk is too large (8KB limit)
|
||||
if len(blob_data) > 8192:
|
||||
# Skip yielding this message
|
||||
raise ValueError("File chunk is too large which reached the limit of 8KB")
|
||||
|
||||
# Append the blob data to the buffer
|
||||
files[chunk_id].data[
|
||||
files[chunk_id].bytes_written : files[chunk_id].bytes_written + len(blob_data)
|
||||
] = blob_data
|
||||
files[chunk_id].bytes_written += len(blob_data)
|
||||
else:
|
||||
yield resp
|
||||
|
||||
def validate_provider_credentials(
|
||||
self, tenant_id: str, user_id: str, provider: str, credentials: dict[str, Any]
|
||||
|
|
|
@ -139,13 +139,17 @@ class AnalyticdbVectorBySql:
|
|||
)
|
||||
if embedding_dimension is not None:
|
||||
index_name = f"{self._collection_name}_embedding_idx"
|
||||
cur.execute(f"ALTER TABLE {self.table_name} ALTER COLUMN vector SET STORAGE PLAIN")
|
||||
cur.execute(
|
||||
f"CREATE INDEX {index_name} ON {self.table_name} USING ann(vector) "
|
||||
f"WITH(dim='{embedding_dimension}', distancemeasure='{self.config.metrics}', "
|
||||
f"pq_enable=0, external_storage=0)"
|
||||
)
|
||||
cur.execute(f"CREATE INDEX ON {self.table_name} USING gin(to_tsvector)")
|
||||
try:
|
||||
cur.execute(f"ALTER TABLE {self.table_name} ALTER COLUMN vector SET STORAGE PLAIN")
|
||||
cur.execute(
|
||||
f"CREATE INDEX {index_name} ON {self.table_name} USING ann(vector) "
|
||||
f"WITH(dim='{embedding_dimension}', distancemeasure='{self.config.metrics}', "
|
||||
f"pq_enable=0, external_storage=0)"
|
||||
)
|
||||
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)
|
||||
|
||||
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
|
||||
|
@ -177,9 +181,11 @@ class AnalyticdbVectorBySql:
|
|||
return cur.fetchone() is not None
|
||||
|
||||
def delete_by_ids(self, ids: list[str]) -> None:
|
||||
if not ids:
|
||||
return
|
||||
with self._get_cursor() as cur:
|
||||
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:
|
||||
if "does not exist" not in str(e):
|
||||
raise e
|
||||
|
@ -240,7 +246,7 @@ class AnalyticdbVectorBySql:
|
|||
ts_rank(to_tsvector, to_tsquery_from_text(%s, 'zh_cn'), 32) AS score
|
||||
FROM {self.table_name}
|
||||
WHERE to_tsvector@@to_tsquery_from_text(%s, 'zh_cn') {where_clause}
|
||||
ORDER BY score DESC
|
||||
ORDER BY (score,id) DESC
|
||||
LIMIT {top_k}""",
|
||||
(f"'{query}'", f"'{query}'"),
|
||||
)
|
||||
|
|
|
@ -39,6 +39,12 @@ class EnhanceRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter):
|
|||
else:
|
||||
return [GPT2Tokenizer.get_num_tokens(text) for text in texts]
|
||||
|
||||
def _character_encoder(texts: list[str]) -> list[int]:
|
||||
if not texts:
|
||||
return []
|
||||
|
||||
return [len(text) for text in texts]
|
||||
|
||||
if issubclass(cls, TokenTextSplitter):
|
||||
extra_kwargs = {
|
||||
"model_name": embedding_model_instance.model if embedding_model_instance else "gpt2",
|
||||
|
@ -47,7 +53,7 @@ class EnhanceRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter):
|
|||
}
|
||||
kwargs = {**kwargs, **extra_kwargs}
|
||||
|
||||
return cls(length_function=_token_encoder, **kwargs)
|
||||
return cls(length_function=_character_encoder, **kwargs)
|
||||
|
||||
|
||||
class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter):
|
||||
|
@ -103,7 +109,7 @@ class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter)
|
|||
_good_splits_lengths = [] # cache the lengths of the splits
|
||||
_separator = "" if self._keep_separator else separator
|
||||
s_lens = self._length_function(splits)
|
||||
if _separator != "":
|
||||
if separator != "":
|
||||
for s, s_len in zip(splits, s_lens):
|
||||
if s_len < self._chunk_size:
|
||||
_good_splits.append(s)
|
||||
|
|
|
@ -120,6 +120,13 @@ class ToolInvokeMessage(BaseModel):
|
|||
class BlobMessage(BaseModel):
|
||||
blob: bytes
|
||||
|
||||
class BlobChunkMessage(BaseModel):
|
||||
id: str = Field(..., description="The id of the blob")
|
||||
sequence: int = Field(..., description="The sequence of the chunk")
|
||||
total_length: int = Field(..., description="The total length of the blob")
|
||||
blob: bytes = Field(..., description="The blob data of the chunk")
|
||||
end: bool = Field(..., description="Whether the chunk is the last chunk")
|
||||
|
||||
class FileMessage(BaseModel):
|
||||
pass
|
||||
|
||||
|
@ -180,12 +187,15 @@ class ToolInvokeMessage(BaseModel):
|
|||
VARIABLE = "variable"
|
||||
FILE = "file"
|
||||
LOG = "log"
|
||||
BLOB_CHUNK = "blob_chunk"
|
||||
|
||||
type: MessageType = MessageType.TEXT
|
||||
"""
|
||||
plain text, image url or link url
|
||||
"""
|
||||
message: JsonMessage | TextMessage | BlobMessage | LogMessage | FileMessage | None | VariableMessage
|
||||
message: (
|
||||
JsonMessage | TextMessage | BlobChunkMessage | BlobMessage | LogMessage | FileMessage | None | VariableMessage
|
||||
)
|
||||
meta: dict[str, Any] | None = None
|
||||
|
||||
@field_validator("message", mode="before")
|
||||
|
|
|
@ -90,6 +90,7 @@ class HttpRequestNodeData(BaseNodeData):
|
|||
params: str
|
||||
body: Optional[HttpRequestNodeBody] = None
|
||||
timeout: Optional[HttpRequestNodeTimeout] = None
|
||||
ssl_verify: Optional[bool] = dify_config.HTTP_REQUEST_NODE_SSL_VERIFY
|
||||
|
||||
|
||||
class Response:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import base64
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
|
@ -87,6 +88,7 @@ class Executor:
|
|||
self.method = node_data.method
|
||||
self.auth = node_data.authorization
|
||||
self.timeout = timeout
|
||||
self.ssl_verify = node_data.ssl_verify
|
||||
self.params = []
|
||||
self.headers = {}
|
||||
self.content = None
|
||||
|
@ -259,7 +261,9 @@ class Executor:
|
|||
if self.auth.config.type == "bearer":
|
||||
headers[authorization.config.header] = f"Bearer {authorization.config.api_key}"
|
||||
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":
|
||||
headers[authorization.config.header] = authorization.config.api_key or ""
|
||||
|
||||
|
@ -313,6 +317,7 @@ class Executor:
|
|||
"headers": headers,
|
||||
"params": self.params,
|
||||
"timeout": (self.timeout.connect, self.timeout.read, self.timeout.write),
|
||||
"ssl_verify": self.ssl_verify,
|
||||
"follow_redirects": True,
|
||||
"max_retries": self.max_retries,
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ class HttpRequestNode(BaseNode[HttpRequestNodeData]):
|
|||
"max_read_timeout": dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
|
||||
"max_write_timeout": dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
|
||||
},
|
||||
"ssl_verify": dify_config.HTTP_REQUEST_NODE_SSL_VERIFY,
|
||||
},
|
||||
"retry_config": {
|
||||
"max_retries": dify_config.SSRF_DEFAULT_MAX_RETRIES,
|
||||
|
|
|
@ -149,7 +149,10 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]):
|
|||
def _extract_slice(
|
||||
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]
|
||||
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]:
|
||||
value = int(self.graph_runtime_state.variable_pool.convert_template(self.node_data.extract_by.serial).text) - 1
|
||||
value = int(self.graph_runtime_state.variable_pool.convert_template(self.node_data.extract_by.serial).text)
|
||||
if value < 1:
|
||||
raise ValueError(f"Invalid serial index: must be >= 1, got {value}")
|
||||
value -= 1
|
||||
if len(variable.value) > int(value):
|
||||
result = variable.value[value]
|
||||
else:
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
import sys
|
||||
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 opentelemetry import trace
|
||||
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
||||
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.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.propagators.b3 import B3Format
|
||||
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.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.status import StatusCode
|
||||
|
||||
|
@ -96,22 +100,37 @@ def init_app(app: DifyApp):
|
|||
export_timeout_millis=dify_config.OTEL_METRIC_EXPORT_TIMEOUT,
|
||||
)
|
||||
set_meter_provider(MeterProvider(resource=resource, metric_readers=[reader]))
|
||||
|
||||
def response_hook(span: Span, status: str, response_headers: list):
|
||||
if span and span.is_recording():
|
||||
if status.startswith("2"):
|
||||
span.set_status(StatusCode.OK)
|
||||
else:
|
||||
span.set_status(StatusCode.ERROR, status)
|
||||
|
||||
instrumentor = FlaskInstrumentor()
|
||||
instrumentor.instrument_app(app, response_hook=response_hook)
|
||||
with app.app_context():
|
||||
engines = list(app.extensions["sqlalchemy"].engines.values())
|
||||
SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines)
|
||||
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):
|
||||
if span and span.is_recording():
|
||||
if status.startswith("2"):
|
||||
span.set_status(StatusCode.OK)
|
||||
else:
|
||||
span.set_status(StatusCode.ERROR, status)
|
||||
|
||||
instrumentor = FlaskInstrumentor()
|
||||
if dify_config.DEBUG:
|
||||
logging.info("Initializing Flask instrumentor")
|
||||
instrumentor.instrument_app(app, response_hook=response_hook)
|
||||
|
||||
|
||||
def init_sqlalchemy_instrumentor(app: DifyApp):
|
||||
with app.app_context():
|
||||
engines = list(app.extensions["sqlalchemy"].engines.values())
|
||||
SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines)
|
||||
|
||||
|
||||
def setup_context_propagation():
|
||||
# Configure propagators
|
||||
set_global_textmap(
|
||||
|
@ -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():
|
||||
provider = trace.get_tracer_provider()
|
||||
if hasattr(provider, "force_flush"):
|
||||
|
|
|
@ -42,6 +42,7 @@ message_file_fields = {
|
|||
"size": fields.Integer,
|
||||
"transfer_method": fields.String,
|
||||
"belongs_to": fields.String(default="user"),
|
||||
"upload_file_id": fields.String(default=None),
|
||||
}
|
||||
|
||||
agent_thought_fields = {
|
||||
|
|
|
@ -1155,7 +1155,7 @@ class Message(db.Model): # type: ignore[name-defined]
|
|||
files.append(file)
|
||||
|
||||
result = [
|
||||
{"belongs_to": message_file.belongs_to, **file.to_dict()}
|
||||
{"belongs_to": message_file.belongs_to, "upload_file_id": message_file.upload_file_id, **file.to_dict()}
|
||||
for (file, message_file) in zip(files, message_files)
|
||||
]
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +0,0 @@
|
|||
[virtualenvs]
|
||||
in-project = true
|
||||
create = true
|
||||
prefer-active-python = true
|
|
@ -1,225 +1,194 @@
|
|||
[project]
|
||||
name = "dify-api"
|
||||
version = "1.2.0"
|
||||
requires-python = ">=3.11,<3.13"
|
||||
dynamic = ["dependencies"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
dependencies = [
|
||||
"authlib==1.3.1",
|
||||
"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]
|
||||
package-mode = false
|
||||
[tool.uv]
|
||||
default-groups = ["storage", "tools", "vdb"]
|
||||
|
||||
############################################################
|
||||
# [ Main ] Dependency group
|
||||
############################################################
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
authlib = "1.3.1"
|
||||
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 = { version = "~0.27.0", extras = ["socks"] }
|
||||
jieba = "0.42.1"
|
||||
json-repair = "~0.40.0"
|
||||
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"
|
||||
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-flask = "0.48b0"
|
||||
opentelemetry-instrumentation-sqlalchemy = "0.48b0"
|
||||
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
|
||||
############################################################
|
||||
[tool.poetry.group.tools.dependencies]
|
||||
cloudscraper = "1.2.71"
|
||||
nltk = "3.9.1"
|
||||
|
||||
############################################################
|
||||
# [ Storage ] dependency group
|
||||
# Required for storage clients
|
||||
############################################################
|
||||
[tool.poetry.group.storage.dependencies]
|
||||
azure-storage-blob = "12.13.0"
|
||||
bce-python-sdk = "~0.9.23"
|
||||
cos-python-sdk-v5 = "1.9.30"
|
||||
esdk-obs-python = "3.24.6.1"
|
||||
google-cloud-storage = "2.16.0"
|
||||
opendal = "~0.45.16"
|
||||
oss2 = "2.18.5"
|
||||
supabase = "~2.8.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"
|
||||
[dependency-groups]
|
||||
|
||||
############################################################
|
||||
# [ Dev ] dependency group
|
||||
# Required for development and running tests
|
||||
############################################################
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
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"
|
||||
dev = [
|
||||
"coverage~=7.2.4",
|
||||
"dotenv-linter~=0.5.0",
|
||||
"faker~=32.1.0",
|
||||
"lxml-stubs~=0.5.1",
|
||||
"mypy~=1.15.0",
|
||||
"ruff~=0.11.5",
|
||||
"pytest~=8.3.2",
|
||||
"pytest-benchmark~=4.0.0",
|
||||
"pytest-cov~=4.1.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
|
||||
# Required for code style linting
|
||||
# [ Storage ] dependency group
|
||||
# Required for storage clients
|
||||
############################################################
|
||||
[tool.poetry.group.lint]
|
||||
optional = true
|
||||
[tool.poetry.group.lint.dependencies]
|
||||
dotenv-linter = "~0.5.0"
|
||||
ruff = "~0.11.0"
|
||||
storage = [
|
||||
"azure-storage-blob==12.13.0",
|
||||
"bce-python-sdk~=0.9.23",
|
||||
"cos-python-sdk-v5==1.9.30",
|
||||
"esdk-obs-python==3.24.6.1",
|
||||
"google-cloud-storage==2.16.0",
|
||||
"opendal~=0.45.16",
|
||||
"oss2==2.18.5",
|
||||
"supabase~=2.8.1",
|
||||
"tos~=2.7.1",
|
||||
]
|
||||
|
||||
############################################################
|
||||
# [ Tools ] dependency group
|
||||
############################################################
|
||||
tools = ["cloudscraper~=1.2.71", "nltk~=3.9.1"]
|
||||
|
||||
############################################################
|
||||
# [ VDB ] dependency group
|
||||
# Required by vector store clients
|
||||
############################################################
|
||||
vdb = [
|
||||
"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[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",
|
||||
]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[pytest]
|
||||
continue-on-collection-errors = true
|
||||
addopts = --cov=./api --cov-report=json --cov-report=xml
|
||||
env =
|
||||
ANTHROPIC_API_KEY = sk-ant-api11-IamNotARealKeyJustForMockTestKawaiiiiiiiiii-NotBaka-ASkksz
|
||||
AZURE_OPENAI_API_BASE = https://difyai-openai.openai.azure.com
|
||||
|
|
|
@ -553,7 +553,7 @@ class DocumentService:
|
|||
{"id": "remove_extra_spaces", "enabled": True},
|
||||
{"id": "remove_urls_emails", "enabled": False},
|
||||
],
|
||||
"segmentation": {"delimiter": "\n", "max_tokens": 500, "chunk_overlap": 50},
|
||||
"segmentation": {"delimiter": "\n", "max_tokens": 1024, "chunk_overlap": 50},
|
||||
},
|
||||
"limits": {
|
||||
"indexing_max_segmentation_tokens_length": dify_config.INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH,
|
||||
|
@ -2175,7 +2175,13 @@ class SegmentService:
|
|||
|
||||
@classmethod
|
||||
def get_segments(
|
||||
cls, document_id: str, tenant_id: str, status_list: list[str] | None = None, keyword: str | None = None
|
||||
cls,
|
||||
document_id: str,
|
||||
tenant_id: str,
|
||||
status_list: list[str] | None = None,
|
||||
keyword: str | None = None,
|
||||
page: int = 1,
|
||||
limit: int = 20,
|
||||
):
|
||||
"""Get segments for a document with optional filtering."""
|
||||
query = DocumentSegment.query.filter(
|
||||
|
@ -2188,10 +2194,11 @@ class SegmentService:
|
|||
if keyword:
|
||||
query = query.filter(DocumentSegment.content.ilike(f"%{keyword}%"))
|
||||
|
||||
segments = query.order_by(DocumentSegment.position.asc()).all()
|
||||
total = len(segments)
|
||||
paginated_segments = query.order_by(DocumentSegment.position.asc()).paginate(
|
||||
page=page, per_page=limit, max_per_page=100, error_out=False
|
||||
)
|
||||
|
||||
return segments, total
|
||||
return paginated_segments.items, paginated_segments.total
|
||||
|
||||
@classmethod
|
||||
def update_segment_by_id(
|
||||
|
|
|
@ -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 --dev \
|
||||
python -m mypy --install-types --non-interactive .
|
14
dev/reformat
14
dev/reformat
|
@ -2,20 +2,14 @@
|
|||
|
||||
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
|
||||
poetry run -C api ruff check --fix ./
|
||||
uv run --directory api --dev ruff check --fix ./
|
||||
|
||||
# run ruff formatter
|
||||
poetry run -C api ruff format ./
|
||||
uv run --directory api --dev ruff format ./
|
||||
|
||||
# run dotenv-linter linter
|
||||
poetry run -P api dotenv-linter ./api/.env.example ./web/.env.example
|
||||
uv run --project api --dev dotenv-linter ./api/.env.example ./web/.env.example
|
||||
|
||||
# run mypy check
|
||||
dev/run-mypy
|
||||
dev/mypy-check
|
||||
|
|
11
dev/run-mypy
11
dev/run-mypy
|
@ -1,11 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
if ! command -v mypy &> /dev/null; then
|
||||
poetry install -C api --with dev
|
||||
fi
|
||||
|
||||
# run mypy checks
|
||||
poetry run -C api \
|
||||
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
|
|
@ -174,6 +174,12 @@ CELERY_MIN_WORKERS=
|
|||
API_TOOL_DEFAULT_CONNECT_TIMEOUT=10
|
||||
API_TOOL_DEFAULT_READ_TIMEOUT=60
|
||||
|
||||
# -------------------------------
|
||||
# Datasource Configuration
|
||||
# --------------------------------
|
||||
ENABLE_WEBSITE_JINAREADER=true
|
||||
ENABLE_WEBSITE_FIRECRAWL=true
|
||||
ENABLE_WEBSITE_WATERCRAWL=true
|
||||
|
||||
# ------------------------------
|
||||
# Database Configuration
|
||||
|
|
|
@ -17,8 +17,10 @@ services:
|
|||
PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800}
|
||||
INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
volumes:
|
||||
# Mount the storage directory to the container, for storing user files.
|
||||
- ./volumes/app/storage:/app/api/storage
|
||||
|
@ -42,8 +44,10 @@ services:
|
|||
PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800}
|
||||
INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
volumes:
|
||||
# Mount the storage directory to the container, for storing user files.
|
||||
- ./volumes/app/storage:/app/api/storage
|
||||
|
@ -71,7 +75,9 @@ services:
|
|||
MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10}
|
||||
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
|
||||
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5}
|
||||
|
||||
ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true}
|
||||
ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true}
|
||||
ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true}
|
||||
# The postgres database.
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
|
@ -163,7 +169,7 @@ services:
|
|||
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
|
||||
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
|
||||
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:-}
|
||||
AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-}
|
||||
AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-}
|
||||
|
|
|
@ -107,7 +107,7 @@ services:
|
|||
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
|
||||
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
|
||||
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:-}
|
||||
AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-}
|
||||
AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-}
|
||||
|
|
|
@ -43,6 +43,9 @@ x-shared-env: &shared-api-worker-env
|
|||
CELERY_MIN_WORKERS: ${CELERY_MIN_WORKERS:-}
|
||||
API_TOOL_DEFAULT_CONNECT_TIMEOUT: ${API_TOOL_DEFAULT_CONNECT_TIMEOUT:-10}
|
||||
API_TOOL_DEFAULT_READ_TIMEOUT: ${API_TOOL_DEFAULT_READ_TIMEOUT:-60}
|
||||
ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true}
|
||||
ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true}
|
||||
ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true}
|
||||
DB_USERNAME: ${DB_USERNAME:-postgres}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-difyai123456}
|
||||
DB_HOST: ${DB_HOST:-db}
|
||||
|
@ -486,8 +489,10 @@ services:
|
|||
PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800}
|
||||
INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
volumes:
|
||||
# Mount the storage directory to the container, for storing user files.
|
||||
- ./volumes/app/storage:/app/api/storage
|
||||
|
@ -511,8 +516,10 @@ services:
|
|||
PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800}
|
||||
INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
volumes:
|
||||
# Mount the storage directory to the container, for storing user files.
|
||||
- ./volumes/app/storage:/app/api/storage
|
||||
|
@ -540,7 +547,9 @@ services:
|
|||
MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10}
|
||||
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
|
||||
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5}
|
||||
|
||||
ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true}
|
||||
ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true}
|
||||
ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true}
|
||||
# The postgres database.
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
|
@ -632,7 +641,7 @@ services:
|
|||
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
|
||||
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
|
||||
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:-}
|
||||
AZURE_BLOB_STORAGE_CONNECTION_STRING: ${PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING:-}
|
||||
AZURE_BLOB_STORAGE_CONTAINER_NAME: ${PLUGIN_AZURE_BLOB_STORAGE_CONTAINER_NAME:-}
|
||||
|
|
|
@ -49,3 +49,8 @@ NEXT_PUBLIC_MAX_PARALLEL_LIMIT=10
|
|||
|
||||
# The maximum number of iterations for agent setting
|
||||
NEXT_PUBLIC_MAX_ITERATIONS_NUM=5
|
||||
|
||||
NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER=true
|
||||
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL=true
|
||||
NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL=true
|
||||
|
||||
|
|
|
@ -27,17 +27,11 @@ done
|
|||
if $api_modified; then
|
||||
echo "Running Ruff linter on api module"
|
||||
|
||||
# python style checks rely on `ruff` in path
|
||||
if ! command -v ruff > /dev/null 2>&1; then
|
||||
echo "Installing linting tools (Ruff, dotenv-linter ...) ..."
|
||||
poetry install -C api --only lint
|
||||
fi
|
||||
|
||||
# run Ruff linter auto-fixing
|
||||
ruff check --fix ./api
|
||||
uv run --project api --dev ruff check --fix ./api
|
||||
|
||||
# run Ruff linter checks
|
||||
ruff check ./api || status=$?
|
||||
uv run --project api --dev ruff check ./api || status=$?
|
||||
|
||||
status=${status:-0}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import AppsContext, { useAppContext } from '@/context/app-context'
|
|||
import type { HtmlContentProps } from '@/app/components/base/popover'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
|
@ -216,7 +217,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
try {
|
||||
const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
|
||||
if (installed_apps?.length > 0)
|
||||
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||
window.open(`${basePath}/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||
else
|
||||
throw new Error('No app found in Explore')
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useQuery } from '@tanstack/react-query'
|
|||
import ExternalAPIPanel from '../../components/datasets/external-api/external-api-panel'
|
||||
import Datasets from './Datasets'
|
||||
import DatasetFooter from './DatasetFooter'
|
||||
import ApiServer from './ApiServer'
|
||||
import ApiServer from '../../components/develop/ApiServer'
|
||||
import Doc from './Doc'
|
||||
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||
import TagManagementModal from '@/app/components/base/tag-management'
|
||||
|
|
|
@ -9,6 +9,9 @@ import TemplateZh from './template/template.zh.mdx'
|
|||
import TemplateJa from './template/template.ja.mdx'
|
||||
import I18n from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type DocProps = {
|
||||
apiBaseUrl: string
|
||||
|
@ -19,6 +22,7 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
|
|||
const { t } = useTranslation()
|
||||
const [toc, setToc] = useState<Array<{ href: string; text: string }>>([])
|
||||
const [isTocExpanded, setIsTocExpanded] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
|
||||
// Set initial TOC expanded state based on screen width
|
||||
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'}`}>
|
||||
{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">
|
||||
<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
|
||||
onClick={() => setIsTocExpanded(false)}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
className="text-text-tertiary hover:text-text-secondary"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
@ -98,7 +102,7 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
|
|||
<li key={index}>
|
||||
<a
|
||||
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)}
|
||||
>
|
||||
{item.text}
|
||||
|
@ -111,13 +115,13 @@ const Doc = ({ apiBaseUrl }: DocProps) => {
|
|||
: (
|
||||
<button
|
||||
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>
|
||||
)}
|
||||
</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}
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { basePath } from '@/utils/var'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiArrowRightLine,
|
||||
|
@ -17,7 +18,7 @@ const CreateAppCard = (
|
|||
<div className='bg-background-default-dimm flex min-h-[160px] flex-col rounded-xl border-[0.5px]
|
||||
border-components-panel-border transition-all duration-200 ease-in-out'
|
||||
>
|
||||
<a ref={ref} className='group flex grow cursor-pointer items-start p-4' href='/datasets/create'>
|
||||
<a ref={ref} className='group flex grow cursor-pointer items-start p-4' href={`${basePath}/datasets/create`}>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-regular bg-background-default-lighter
|
||||
p-2 group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge'
|
||||
|
@ -28,7 +29,7 @@ const CreateAppCard = (
|
|||
</div>
|
||||
</a>
|
||||
<div className='system-xs-regular p-4 pt-0 text-text-tertiary'>{t('dataset.createDatasetIntro')}</div>
|
||||
<a className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href='/datasets/connect'>
|
||||
<a className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href={`${basePath}/datasets/connect`}>
|
||||
<div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div>
|
||||
<RiArrowRightLine className='h-3.5 w-3.5 text-text-tertiary group-hover:text-text-accent' />
|
||||
</a>
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { fetchInstalledAppList } from '@/service/explore'
|
||||
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
@ -75,7 +76,7 @@ const AppPublisher = ({
|
|||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
|
||||
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
|
||||
const appURL = `${appBaseURL}/${appMode}/${accessToken}`
|
||||
const appURL = `${appBaseURL}/${basePath}/${appMode}/${accessToken}`
|
||||
const isChatApp = ['chat', 'agent-chat', 'completion'].includes(appDetail?.mode || '')
|
||||
|
||||
const language = useGetLanguage()
|
||||
|
@ -120,7 +121,7 @@ const AppPublisher = ({
|
|||
try {
|
||||
const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
|
||||
if (installed_apps?.length > 0)
|
||||
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||
window.open(`${basePath}/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||
else
|
||||
throw new Error('No app found in Explore')
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import { useFeatures } from '@/app/components/base/features/hooks'
|
|||
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import { canFindTool } from '@/utils'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
|
||||
type DebugWithSingleModelProps = {
|
||||
checkCanSend?: () => boolean
|
||||
|
@ -125,10 +126,14 @@ const DebugWithSingleModel = (
|
|||
)
|
||||
}, [appId, chatList, checkCanSend, completionParams, config, handleSend, inputs, modelConfig.mode, modelConfig.model_id, modelConfig.provider, textGenerationModelList])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
|
||||
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
|
||||
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.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])
|
||||
|
||||
const allToolIcons = useMemo(() => {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
@ -503,6 +504,12 @@ const Configuration: FC = () => {
|
|||
useEffect(() => {
|
||||
(async () => {
|
||||
const collectionList = await fetchCollectionList()
|
||||
if (basePath) {
|
||||
collectionList.forEach((item) => {
|
||||
if (typeof item.icon == 'string' && !item.icon.includes(basePath))
|
||||
item.icon = `${basePath}${item.icon}`
|
||||
})
|
||||
}
|
||||
setCollectionList(collectionList)
|
||||
fetchAppDetail({ url: '/apps', id: appId }).then(async (res: any) => {
|
||||
setMode(res.mode)
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { AppIconSelection } from '../../base/app-icon-picker'
|
|||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import cn from '@/utils/classnames'
|
||||
import { basePath } from '@/utils/var'
|
||||
import AppsContext, { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
|
@ -352,11 +353,11 @@ function AppScreenShot({ mode, show }: { mode: AppMode; show: boolean }) {
|
|||
'workflow': 'Workflow',
|
||||
}
|
||||
return <picture>
|
||||
<source media="(resolution: 1x)" srcSet={`/screenshots/${theme}/${modeToImageMap[mode]}.png`} />
|
||||
<source media="(resolution: 2x)" srcSet={`/screenshots/${theme}/${modeToImageMap[mode]}@2x.png`} />
|
||||
<source media="(resolution: 3x)" srcSet={`/screenshots/${theme}/${modeToImageMap[mode]}@3x.png`} />
|
||||
<source media="(resolution: 1x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}.png`} />
|
||||
<source media="(resolution: 2x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}@2x.png`} />
|
||||
<source media="(resolution: 3x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}@3x.png`} />
|
||||
<Image className={show ? '' : 'hidden'}
|
||||
src={`/screenshots/${theme}/${modeToImageMap[mode]}.png`}
|
||||
src={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}.png`}
|
||||
alt='App Screen Shot'
|
||||
width={664} height={448} />
|
||||
</picture>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { usePathname } from 'next/navigation'
|
|||
import { useDebounce } from 'ahooks'
|
||||
import { omit } from 'lodash-es'
|
||||
import dayjs from 'dayjs'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import List from './list'
|
||||
import Filter, { TIME_PERIOD_MAPPING } from './filter'
|
||||
|
@ -109,7 +110,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
|
|||
? <Loading type='app' />
|
||||
: total > 0
|
||||
? <List logs={isChatMode ? chatConversations : completionConversations} appDetail={appDetail} onRefresh={isChatMode ? mutateChatList : mutateCompletionList} />
|
||||
: <EmptyElement appUrl={`${appDetail.site.app_base_url}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} />
|
||||
: <EmptyElement appUrl={`${appDetail.site.app_base_url}${basePath}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} />
|
||||
}
|
||||
{/* Show Pagination only if the total is more than the limit */}
|
||||
{(total && total > APP_PAGE_LIMIT)
|
||||
|
|
|
@ -17,6 +17,7 @@ import type { ConfigParams } from './settings'
|
|||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import AppBasic from '@/app/components/app-sidebar/basic'
|
||||
import { asyncRunSafe, randomString } from '@/utils'
|
||||
import { basePath } from '@/utils/var'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
@ -88,7 +89,7 @@ function AppCard({
|
|||
const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api
|
||||
const { app_base_url, access_token } = appInfo.site ?? {}
|
||||
const appMode = (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow') ? 'chat' : appInfo.mode
|
||||
const appUrl = `${app_base_url}/${appMode}/${access_token}`
|
||||
const appUrl = `${app_base_url}${basePath}/${appMode}/${access_token}`
|
||||
const apiUrl = appInfo?.api_base_url
|
||||
|
||||
const genClickFuncByName = (opName: string) => {
|
||||
|
|
|
@ -13,6 +13,7 @@ import { IS_CE_EDITION } from '@/config'
|
|||
import type { SiteInfo } from '@/models/share'
|
||||
import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { basePath } from '@/utils/var'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
|
@ -28,7 +29,7 @@ const OPTION_MAP = {
|
|||
iframe: {
|
||||
getContent: (url: string, token: string) =>
|
||||
`<iframe
|
||||
src="${url}/chatbot/${token}"
|
||||
src="${url}${basePath}/chatbot/${token}"
|
||||
style="width: 100%; height: 100%; min-height: 700px"
|
||||
frameborder="0"
|
||||
allow="microphone">
|
||||
|
@ -41,17 +42,17 @@ const OPTION_MAP = {
|
|||
token: '${token}'${isTestEnv
|
||||
? `,
|
||||
isDev: true`
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
baseUrl: '${url}'`
|
||||
: ''},
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
baseUrl: '${url}${basePath}'`
|
||||
: ''},
|
||||
systemVariables: {
|
||||
// user_id: 'YOU CAN DEFINE USER ID HERE',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<script
|
||||
src="${url}/embed.min.js"
|
||||
src="${url}${basePath}/embed.min.js"
|
||||
id="${token}"
|
||||
defer>
|
||||
</script>
|
||||
|
@ -66,7 +67,7 @@ const OPTION_MAP = {
|
|||
</style>`,
|
||||
},
|
||||
chromePlugin: {
|
||||
getContent: (url: string, token: string) => `ChatBot URL: ${url}/chatbot/${token}`,
|
||||
getContent: (url: string, token: string) => `ChatBot URL: ${url}${basePath}/chatbot/${token}`,
|
||||
},
|
||||
}
|
||||
const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
|
||||
|
|
|
@ -11,6 +11,7 @@ import timezone from 'dayjs/plugin/timezone'
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import List from './list'
|
||||
import { basePath } from '@/utils/var'
|
||||
import Filter, { TIME_PERIOD_MAPPING } from './filter'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
@ -100,7 +101,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
|
|||
? <Loading type='app' />
|
||||
: total > 0
|
||||
? <List logs={workflowLogs} appDetail={appDetail} onRefresh={mutate} />
|
||||
: <EmptyElement appUrl={`${appDetail.site.app_base_url}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} />
|
||||
: <EmptyElement appUrl={`${appDetail.site.app_base_url}${basePath}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} />
|
||||
}
|
||||
{/* Show Pagination only if the total is more than the limit */}
|
||||
{(total && total > APP_PAGE_LIMIT)
|
||||
|
|
|
@ -22,6 +22,7 @@ import AnswerIcon from '@/app/components/base/answer-icon'
|
|||
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FileEntity } from '../../file-uploader/types'
|
||||
|
||||
const ChatWrapper = () => {
|
||||
const {
|
||||
|
@ -139,22 +140,16 @@ const ChatWrapper = () => {
|
|||
isPublicAPI: !isInstalledApp,
|
||||
},
|
||||
)
|
||||
}, [
|
||||
chatList,
|
||||
handleNewConversationCompleted,
|
||||
handleSend,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
currentConversationInputs,
|
||||
newConversationInputs,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
])
|
||||
}, [chatList, handleNewConversationCompleted, handleSend, currentConversationId, currentConversationInputs, newConversationInputs, isInstalledApp, appId])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
|
||||
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
|
||||
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.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])
|
||||
|
||||
const messageList = useMemo(() => {
|
||||
|
|
|
@ -52,7 +52,7 @@ function getFormattedChatList(messages: any[]) {
|
|||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id, upload_file_id: item.upload_file_id }))),
|
||||
parentMessageId: item.parent_message_id || undefined,
|
||||
})
|
||||
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||
|
@ -63,7 +63,7 @@ function getFormattedChatList(messages: any[]) {
|
|||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.retriever_resources,
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id, upload_file_id: item.upload_file_id }))),
|
||||
parentMessageId: `question-${item.id}`,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ import type {
|
|||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
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 type { AppData } from '@/models/share'
|
||||
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 { FileList } from '@/app/components/base/file-uploader'
|
||||
import ContentSwitch from '../content-switch'
|
||||
|
||||
type AnswerProps = {
|
||||
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 (
|
||||
<div className='mb-2 flex last:mb-0'>
|
||||
<div className='relative h-10 w-10 shrink-0'>
|
||||
{answerIcon || <AnswerIcon />}
|
||||
{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' />
|
||||
</div>
|
||||
)}
|
||||
|
@ -208,23 +215,17 @@ const Answer: FC<AnswerProps> = ({
|
|||
<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
|
||||
className={`${item.prevSibling ? 'opacity-100' : 'opacity-30'}`}
|
||||
disabled={!item.prevSibling}
|
||||
onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)}
|
||||
>
|
||||
<ChevronRight className="h-[14px] w-[14px] rotate-180 text-text-primary" />
|
||||
</button>
|
||||
<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>}
|
||||
{
|
||||
item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && (
|
||||
<ContentSwitch
|
||||
count={item.siblingCount}
|
||||
currentIndex={item.siblingIndex}
|
||||
prevDisabled={!item.prevSibling}
|
||||
nextDisabled={!item.nextSibling}
|
||||
switchSibling={handleSwitchSibling}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<More more={more} />
|
||||
|
|
|
@ -2,8 +2,6 @@ import type { FC } from 'react'
|
|||
import { memo } from 'react'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
import Button from '@/app/components/base/button'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
type SuggestedQuestionsProps = {
|
||||
item: ChatItem
|
||||
|
@ -12,9 +10,6 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
|
|||
item,
|
||||
}) => {
|
||||
const { onSend } = useChatContext()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const klassName = `mr-1 mt-1 ${isMobile ? 'block overflow-hidden text-ellipsis' : ''} max-w-full shrink-0 last:mr-0`
|
||||
|
||||
const {
|
||||
isOpeningStatement,
|
||||
|
@ -27,14 +22,13 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
|
|||
return (
|
||||
<div className='flex flex-wrap'>
|
||||
{suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
|
||||
<Button
|
||||
<div
|
||||
key={index}
|
||||
variant='secondary-accent'
|
||||
className={klassName}
|
||||
className='system-sm-medium mr-1 mt-1 inline-flex max-w-full shrink-0 cursor-pointer flex-wrap rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 py-2 text-components-button-secondary-accent-text shadow-xs last:mr-0 hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover'
|
||||
onClick={() => onSend?.(question)}
|
||||
>
|
||||
{question}
|
||||
</Button>),
|
||||
</div>),
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
)
|
||||
}
|
|
@ -208,7 +208,7 @@ const Chat: FC<ChatProps> = ({
|
|||
useEffect(() => {
|
||||
if (!sidebarCollapseState)
|
||||
setTimeout(() => handleWindowResize(), 200)
|
||||
}, [sidebarCollapseState])
|
||||
}, [handleWindowResize, sidebarCollapseState])
|
||||
|
||||
const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
|
||||
|
||||
|
@ -265,6 +265,7 @@ const Chat: FC<ChatProps> = ({
|
|||
item={item}
|
||||
questionIcon={questionIcon}
|
||||
theme={themeBuilder?.theme}
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -4,46 +4,137 @@ import type {
|
|||
} from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { ChatItem } from '../types'
|
||||
import type { Theme } from '../embedded-chatbot/theme/theme-context'
|
||||
import { CssTransform } from '../embedded-chatbot/theme/utils'
|
||||
import ContentSwitch from './content-switch'
|
||||
import { User } from '@/app/components/base/icons/src/public/avatar'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
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 = {
|
||||
item: ChatItem
|
||||
questionIcon?: ReactNode
|
||||
theme: Theme | null | undefined
|
||||
switchSibling?: (siblingMessageId: string) => void
|
||||
}
|
||||
|
||||
const Question: FC<QuestionProps> = ({
|
||||
item,
|
||||
questionIcon,
|
||||
theme,
|
||||
switchSibling,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
content,
|
||||
message_files,
|
||||
} = 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 (
|
||||
<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
|
||||
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) : {}}
|
||||
>
|
||||
{
|
||||
!!message_files?.length && (
|
||||
<FileList
|
||||
className='mb-2'
|
||||
files={message_files}
|
||||
showDeleteAction={false}
|
||||
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 className='mt-1 h-[18px]' />
|
||||
</div>
|
||||
|
|
|
@ -24,6 +24,7 @@ import AnswerIcon from '@/app/components/base/answer-icon'
|
|||
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FileEntity } from '../../file-uploader/types'
|
||||
|
||||
const ChatWrapper = () => {
|
||||
const {
|
||||
|
@ -140,10 +141,14 @@ const ChatWrapper = () => {
|
|||
)
|
||||
}, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
|
||||
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
|
||||
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.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])
|
||||
|
||||
const messageList = useMemo(() => {
|
||||
|
@ -179,7 +184,7 @@ const ChatWrapper = () => {
|
|||
return null
|
||||
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
|
||||
return (
|
||||
<div className='flex h-[50vh] items-center justify-center px-4 py-12'>
|
||||
<div className={cn('flex items-center justify-center px-4 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
|
||||
<div className='flex max-w-[720px] grow gap-4'>
|
||||
<AppIcon
|
||||
size='xl'
|
||||
|
@ -197,7 +202,7 @@ const ChatWrapper = () => {
|
|||
)
|
||||
}
|
||||
return (
|
||||
<div className={cn('flex h-[50vh] flex-col items-center justify-center gap-3 py-12')}>
|
||||
<div className={cn('flex h-[50vh] flex-col items-center justify-center gap-3 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
|
||||
<AppIcon
|
||||
size='xl'
|
||||
iconType={appData?.site.icon_type}
|
||||
|
|
|
@ -134,7 +134,7 @@ export const getProcessedFilesFromResponse = (files: FileResponse[]) => {
|
|||
progress: 100,
|
||||
transferMethod: fileItem.transfer_method,
|
||||
supportFileType: fileItem.type,
|
||||
uploadedId: fileItem.related_id,
|
||||
uploadedId: fileItem.upload_file_id || fileItem.related_id,
|
||||
url: fileItem.url,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import classNames from '@/utils/classnames'
|
||||
import type { FC } from 'react'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
type LogoEmbeddedChatHeaderProps = {
|
||||
className?: string
|
||||
|
@ -13,7 +14,7 @@ const LogoEmbeddedChatHeader: FC<LogoEmbeddedChatHeaderProps> = ({
|
|||
<source media="(resolution: 2x)" srcSet='/logo/logo-embedded-chat-header@2x.png' />
|
||||
<source media="(resolution: 3x)" srcSet='/logo/logo-embedded-chat-header@3x.png' />
|
||||
<img
|
||||
src='/logo/logo-embedded-chat-header.png'
|
||||
src={`${basePath}/logo/logo-embedded-chat-header.png`}
|
||||
alt='logo'
|
||||
className={classNames('block h-6 w-auto', className)}
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { basePath } from '@/utils/var'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type LogoSiteProps = {
|
||||
|
@ -11,7 +12,7 @@ const LogoSite: FC<LogoSiteProps> = ({
|
|||
}) => {
|
||||
return (
|
||||
<img
|
||||
src={'/logo/logo.png'}
|
||||
src={`${basePath}/logo/logo.png`}
|
||||
className={classNames('block w-[22.651px] h-[24.5px]', className)}
|
||||
alt='logo'
|
||||
/>
|
||||
|
|
|
@ -20,7 +20,7 @@ import { useProviderContext } from '@/context/provider-context'
|
|||
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
|
||||
|
||||
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
|
||||
type IStepOneProps = {
|
||||
datasetId?: string
|
||||
dataSourceType?: DataSourceType
|
||||
|
@ -126,9 +126,7 @@ const StepOne = ({
|
|||
return true
|
||||
if (files.some(file => !file.file.id))
|
||||
return true
|
||||
if (isShowVectorSpaceFull)
|
||||
return true
|
||||
return false
|
||||
return isShowVectorSpaceFull
|
||||
}, [files, isShowVectorSpaceFull])
|
||||
|
||||
return (
|
||||
|
@ -193,7 +191,8 @@ const StepOne = ({
|
|||
{t('datasetCreation.stepOne.dataSourceType.notion')}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
{(ENABLE_WEBSITE_FIRECRAWL || ENABLE_WEBSITE_JINAREADER || ENABLE_WEBSITE_WATERCRAWL) && (
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
'system-sm-medium',
|
||||
|
@ -201,7 +200,7 @@ const StepOne = ({
|
|||
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
|
||||
)}
|
||||
onClick={() => changeType(DataSourceType.WEB)}
|
||||
>
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.web)} />
|
||||
<span
|
||||
title={t('datasetCreation.stepOne.dataSourceType.web')}
|
||||
|
@ -209,7 +208,8 @@ const StepOne = ({
|
|||
>
|
||||
{t('datasetCreation.stepOne.dataSourceType.web')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ export enum IndexingType {
|
|||
}
|
||||
|
||||
const DEFAULT_SEGMENT_IDENTIFIER = '\\n\\n'
|
||||
const DEFAULT_MAXIMUM_CHUNK_LENGTH = 500
|
||||
const DEFAULT_MAXIMUM_CHUNK_LENGTH = 1024
|
||||
const DEFAULT_OVERLAP = 50
|
||||
const MAXIMUM_CHUNK_TOKEN_LENGTH = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-indexing-max-segmentation-tokens-length') || '4000', 10)
|
||||
|
||||
|
@ -117,11 +117,11 @@ const defaultParentChildConfig: ParentChildConfig = {
|
|||
chunkForContext: 'paragraph',
|
||||
parent: {
|
||||
delimiter: '\\n\\n',
|
||||
maxLength: 500,
|
||||
maxLength: 1024,
|
||||
},
|
||||
child: {
|
||||
delimiter: '\\n',
|
||||
maxLength: 200,
|
||||
maxLength: 512,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -623,12 +623,12 @@ const StepTwo = ({
|
|||
onChange={e => setSegmentIdentifier(e.target.value, true)}
|
||||
/>
|
||||
<MaxLengthInput
|
||||
unit='tokens'
|
||||
unit='characters'
|
||||
value={maxChunkLength}
|
||||
onChange={setMaxChunkLength}
|
||||
/>
|
||||
<OverlapInput
|
||||
unit='tokens'
|
||||
unit='characters'
|
||||
value={overlap}
|
||||
min={1}
|
||||
onChange={setOverlap}
|
||||
|
@ -756,7 +756,7 @@ const StepTwo = ({
|
|||
})}
|
||||
/>
|
||||
<MaxLengthInput
|
||||
unit='tokens'
|
||||
unit='characters'
|
||||
value={parentChildConfig.parent.maxLength}
|
||||
onChange={value => setParentChildConfig({
|
||||
...parentChildConfig,
|
||||
|
@ -803,7 +803,7 @@ const StepTwo = ({
|
|||
})}
|
||||
/>
|
||||
<MaxLengthInput
|
||||
unit='tokens'
|
||||
unit='characters'
|
||||
value={parentChildConfig.child.maxLength}
|
||||
onChange={value => setParentChildConfig({
|
||||
...parentChildConfig,
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useModalContext } from '@/context/modal-context'
|
|||
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
|
||||
import { fetchDataSources } from '@/service/datasets'
|
||||
import { type DataSourceItem, DataSourceProvider } from '@/models/common'
|
||||
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
|
||||
|
||||
type Props = {
|
||||
onPreview: (payload: CrawlResultItem) => void
|
||||
|
@ -84,7 +85,7 @@ const Website: FC<Props> = ({
|
|||
{t('datasetCreation.stepOne.website.chooseProvider')}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
{ENABLE_WEBSITE_JINAREADER && <button
|
||||
className={cn('flex items-center justify-center rounded-lg px-4 py-2',
|
||||
selectedProvider === DataSourceProvider.jinaReader
|
||||
? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary'
|
||||
|
@ -95,8 +96,8 @@ const Website: FC<Props> = ({
|
|||
>
|
||||
<span className={cn(s.jinaLogo, 'mr-2')}/>
|
||||
<span>Jina Reader</span>
|
||||
</button>
|
||||
<button
|
||||
</button>}
|
||||
{ENABLE_WEBSITE_FIRECRAWL && <button
|
||||
className={cn('rounded-lg px-4 py-2',
|
||||
selectedProvider === DataSourceProvider.fireCrawl
|
||||
? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary'
|
||||
|
@ -106,8 +107,8 @@ const Website: FC<Props> = ({
|
|||
onClick={() => setSelectedProvider(DataSourceProvider.fireCrawl)}
|
||||
>
|
||||
🔥 Firecrawl
|
||||
</button>
|
||||
<button
|
||||
</button>}
|
||||
{ENABLE_WEBSITE_WATERCRAWL && <button
|
||||
className={cn('flex items-center justify-center rounded-lg px-4 py-2',
|
||||
selectedProvider === DataSourceProvider.waterCrawl
|
||||
? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary'
|
||||
|
@ -118,7 +119,7 @@ const Website: FC<Props> = ({
|
|||
>
|
||||
<span className={cn(s.watercrawlLogo, 'mr-2')}/>
|
||||
<span>WaterCrawl</span>
|
||||
</button>
|
||||
</button>}
|
||||
</div>
|
||||
</div>
|
||||
{source && selectedProvider === DataSourceProvider.fireCrawl && (
|
||||
|
|
|
@ -6,6 +6,7 @@ import s from './index.module.css'
|
|||
import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { DataSourceProvider } from '@/models/common'
|
||||
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
|
||||
|
||||
const I18N_PREFIX = 'datasetCreation.stepOne.website'
|
||||
|
||||
|
@ -16,29 +17,30 @@ type Props = {
|
|||
|
||||
const NoData: FC<Props> = ({
|
||||
onConfig,
|
||||
provider,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const providerConfig = {
|
||||
[DataSourceProvider.jinaReader]: {
|
||||
[DataSourceProvider.jinaReader]: ENABLE_WEBSITE_JINAREADER ? {
|
||||
emoji: <span className={s.jinaLogo} />,
|
||||
title: t(`${I18N_PREFIX}.jinaReaderNotConfigured`),
|
||||
description: t(`${I18N_PREFIX}.jinaReaderNotConfiguredDescription`),
|
||||
},
|
||||
[DataSourceProvider.fireCrawl]: {
|
||||
} : null,
|
||||
[DataSourceProvider.fireCrawl]: ENABLE_WEBSITE_FIRECRAWL ? {
|
||||
emoji: '🔥',
|
||||
title: t(`${I18N_PREFIX}.fireCrawlNotConfigured`),
|
||||
description: t(`${I18N_PREFIX}.fireCrawlNotConfiguredDescription`),
|
||||
},
|
||||
[DataSourceProvider.waterCrawl]: {
|
||||
emoji: <span className={s.watercrawlLogo} />,
|
||||
} : null,
|
||||
[DataSourceProvider.waterCrawl]: ENABLE_WEBSITE_WATERCRAWL ? {
|
||||
emoji: '💧',
|
||||
title: t(`${I18N_PREFIX}.waterCrawlNotConfigured`),
|
||||
description: t(`${I18N_PREFIX}.waterCrawlNotConfiguredDescription`),
|
||||
},
|
||||
} : null,
|
||||
}
|
||||
|
||||
const currentProvider = providerConfig[provider]
|
||||
const currentProvider = Object.values(providerConfig).find(provider => provider !== null) || providerConfig[DataSourceProvider.jinaReader]
|
||||
|
||||
if (!currentProvider) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -4,7 +4,6 @@ import type { FC } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
|
||||
import { randomString } from '@/utils'
|
||||
|
||||
type ApiServerProps = {
|
||||
apiBaseUrl: string
|
||||
|
@ -16,21 +15,17 @@ const ApiServer: FC<ApiServerProps> = ({
|
|||
|
||||
return (
|
||||
<div className='flex flex-wrap items-center gap-y-2'>
|
||||
<div className='mr-2 flex h-8 items-center rounded-lg border-[0.5px] border-white bg-white/80 pl-1.5 pr-1 leading-5'>
|
||||
<div className='mr-0.5 h-5 shrink-0 rounded-md border border-gray-200 px-1.5 text-[11px] text-gray-500'>{t('appApi.apiServer')}</div>
|
||||
<div className='w-fit truncate px-1 text-[13px] font-medium text-gray-800 sm:w-[248px]'>{apiBaseUrl}</div>
|
||||
<div className='mx-1 h-[14px] w-[1px] bg-gray-200'></div>
|
||||
<CopyFeedback
|
||||
content={apiBaseUrl}
|
||||
selectorId={randomString(8)}
|
||||
className={'!h-6 !w-6 hover:bg-gray-200'}
|
||||
/>
|
||||
<div className='mr-2 flex h-8 items-center rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal pl-1.5 pr-1 leading-5'>
|
||||
<div className='mr-0.5 h-5 shrink-0 rounded-md border border-divider-subtle px-1.5 text-[11px] text-text-tertiary'>{t('appApi.apiServer')}</div>
|
||||
<div className='w-fit truncate px-1 text-[13px] font-medium text-text-secondary sm:w-[248px]'>{apiBaseUrl}</div>
|
||||
<div className='mx-1 h-[14px] w-[1px] bg-divider-regular'></div>
|
||||
<CopyFeedback content={apiBaseUrl}/>
|
||||
</div>
|
||||
<div className='mr-2 flex h-8 items-center rounded-lg border-[0.5px] border-[#D1FADF] bg-[#ECFDF3] px-3 text-xs font-semibold text-[#039855]'>
|
||||
{t('appApi.ok')}
|
||||
</div>
|
||||
<SecretKeyButton
|
||||
className='!h-8 shrink-0 bg-white'
|
||||
className='!h-8 shrink-0'
|
||||
/>
|
||||
</div>
|
||||
)
|
|
@ -17,6 +17,9 @@ import TemplateChatZh from './template/template_chat.zh.mdx'
|
|||
import TemplateChatJa from './template/template_chat.ja.mdx'
|
||||
import I18n from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type IDocProps = {
|
||||
appDetail: any
|
||||
|
@ -27,6 +30,7 @@ const Doc = ({ appDetail }: IDocProps) => {
|
|||
const { t } = useTranslation()
|
||||
const [toc, setToc] = useState<Array<{ href: string; text: string }>>([])
|
||||
const [isTocExpanded, setIsTocExpanded] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
|
||||
const variables = appDetail?.model_config?.configs?.prompt_variables || []
|
||||
const inputs = variables.reduce((res: any, variable: any) => {
|
||||
|
@ -83,12 +87,12 @@ const Doc = ({ appDetail }: IDocProps) => {
|
|||
<div className={`fixed right-8 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}>
|
||||
{isTocExpanded
|
||||
? (
|
||||
<nav className="toc w-full rounded-lg bg-gray-50 p-4 shadow-md">
|
||||
<nav className="toc w-full rounded-lg bg-components-panel-bg p-4 shadow-md">
|
||||
<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
|
||||
onClick={() => setIsTocExpanded(false)}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
className="text-text-tertiary hover:text-text-secondary"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
@ -98,7 +102,7 @@ const Doc = ({ appDetail }: IDocProps) => {
|
|||
<li key={index}>
|
||||
<a
|
||||
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)}
|
||||
>
|
||||
{item.text}
|
||||
|
@ -111,13 +115,13 @@ const Doc = ({ appDetail }: IDocProps) => {
|
|||
: (
|
||||
<button
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
<article className="prose-xl prose" >
|
||||
<article className={cn('prose-xl prose', theme === Theme.dark && 'dark:prose-invert')} >
|
||||
{(appDetail?.mode === 'chat' || appDetail?.mode === 'agent-chat') && (
|
||||
(() => {
|
||||
switch (locale) {
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import s from './secret-key/style.module.css'
|
||||
import Doc from '@/app/components/develop/doc'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import InputCopy from '@/app/components/develop/secret-key/input-copy'
|
||||
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
|
||||
import ApiServer from '@/app/components/develop/ApiServer'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
type IDevelopMainProps = {
|
||||
|
@ -13,11 +10,10 @@ type IDevelopMainProps = {
|
|||
|
||||
const DevelopMain = ({ appId }: IDevelopMainProps) => {
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!appDetail) {
|
||||
return (
|
||||
<div className='flex h-full items-center justify-center bg-white'>
|
||||
<div className='flex h-full items-center justify-center bg-background-default'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
|
@ -25,21 +21,9 @@ const DevelopMain = ({ appId }: IDevelopMainProps) => {
|
|||
|
||||
return (
|
||||
<div className='relative flex h-full flex-col overflow-hidden'>
|
||||
<div className='flex shrink-0 items-center justify-between border-b border-solid border-b-gray-100 px-6 py-2'>
|
||||
<div className='text-lg font-medium text-gray-900'></div>
|
||||
<div className='flex flex-wrap items-center gap-y-1'>
|
||||
<InputCopy className='mr-1 w-52 shrink-0 sm:w-80' value={appDetail.api_base_url}>
|
||||
<div className={`ml-2 shrink-0 rounded-[6px] border border-solid border-gray-200 px-2 py-0.5 text-[0.625rem] text-gray-500 ${s.customApi}`}>
|
||||
{t('appApi.apiServer')}
|
||||
</div>
|
||||
</InputCopy>
|
||||
<div className={`mr-2 flex h-9 items-center rounded-lg
|
||||
px-3 text-[13px] font-normal ${appDetail.enable_api ? 'bg-green-50 text-green-500' : 'bg-yellow-50 text-yellow-500'}`}>
|
||||
<div className='mr-1'>{t('appApi.status')}</div>
|
||||
<div className='font-semibold'>{appDetail.enable_api ? `${t('appApi.ok')}` : `${t('appApi.disabled')}`}</div>
|
||||
</div>
|
||||
<SecretKeyButton className='shrink-0' appId={appId} />
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center justify-between border-b border-solid border-b-divider-regular px-6 py-2'>
|
||||
<div className='text-lg font-medium text-text-primary'></div>
|
||||
<ApiServer apiBaseUrl={appDetail.api_base_url} />
|
||||
</div>
|
||||
<div className='grow overflow-auto px-4 py-4 sm:px-10'>
|
||||
<Doc appDetail={appDetail} />
|
||||
|
|
|
@ -12,7 +12,7 @@ type IChildrenProps = {
|
|||
|
||||
type IHeaderingProps = {
|
||||
url: string
|
||||
method: 'PUT' | 'DELETE' | 'GET' | 'POST'
|
||||
method: 'PUT' | 'DELETE' | 'GET' | 'POST' | 'PATCH'
|
||||
title: string
|
||||
name: string
|
||||
}
|
||||
|
@ -34,6 +34,9 @@ export const Heading = function H2({
|
|||
case 'POST':
|
||||
style = 'ring-sky-300 bg-sky-400/10 text-sky-500 dark:ring-sky-400/30 dark:bg-sky-400/10 dark:text-sky-400'
|
||||
break
|
||||
case 'PATCH':
|
||||
style = 'ring-violet-300 bg-violet-400/10 text-violet-500 dark:ring-violet-400/30 dark:bg-violet-400/10 dark:text-violet-400'
|
||||
break
|
||||
default:
|
||||
style = 'ring-emerald-300 dark:ring-emerald-400/30 bg-emerald-400/10 text-emerald-500 dark:text-emerald-400'
|
||||
break
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
'use client'
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
|
||||
import useSWR, { useSWRConfig } from 'swr'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import SecretKeyGenerateModal from './secret-key-generate'
|
||||
import s from './style.module.css'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import {
|
||||
createApikey as createAppApikey,
|
||||
delApikey as delAppApikey,
|
||||
|
@ -22,7 +23,6 @@ import {
|
|||
fetchApiKeysList as fetchDatasetApiKeysList,
|
||||
} from '@/service/datasets'
|
||||
import type { CreateApiKeyResponse } from '@/models/app'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
|
@ -54,20 +54,6 @@ const SecretKeyModal = ({
|
|||
|
||||
const [delKeyID, setDelKeyId] = useState('')
|
||||
|
||||
const [copyValue, setCopyValue] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (copyValue) {
|
||||
const timeout = setTimeout(() => {
|
||||
setCopyValue('')
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}, [copyValue])
|
||||
|
||||
const onDel = async () => {
|
||||
setShowConfirmDelete(false)
|
||||
if (!delKeyID)
|
||||
|
@ -104,7 +90,7 @@ const SecretKeyModal = ({
|
|||
{
|
||||
!!apiKeysList?.data?.length && (
|
||||
<div className='mt-4 flex grow flex-col overflow-hidden'>
|
||||
<div className='flex h-9 shrink-0 items-center border-b border-solid text-xs font-semibold text-text-tertiary'>
|
||||
<div className='flex h-9 shrink-0 items-center border-b border-divider-regular text-xs font-semibold text-text-tertiary'>
|
||||
<div className='w-64 shrink-0 px-3'>{t('appApi.apiKeyModal.secretKey')}</div>
|
||||
<div className='w-[200px] shrink-0 px-3'>{t('appApi.apiKeyModal.created')}</div>
|
||||
<div className='w-[200px] shrink-0 px-3'>{t('appApi.apiKeyModal.lastUsed')}</div>
|
||||
|
@ -112,28 +98,22 @@ const SecretKeyModal = ({
|
|||
</div>
|
||||
<div className='grow overflow-auto'>
|
||||
{apiKeysList.data.map(api => (
|
||||
<div className='flex h-9 items-center border-b border-solid text-sm font-normal text-text-secondary' key={api.id}>
|
||||
<div className='flex h-9 items-center border-b border-divider-regular text-sm font-normal text-text-secondary' key={api.id}>
|
||||
<div className='w-64 shrink-0 truncate px-3 font-mono'>{generateToken(api.token)}</div>
|
||||
<div className='w-[200px] shrink-0 truncate px-3'>{formatTime(Number(api.created_at), t('appLog.dateTimeFormat') as string)}</div>
|
||||
<div className='w-[200px] shrink-0 truncate px-3'>{api.last_used_at ? formatTime(Number(api.last_used_at), t('appLog.dateTimeFormat') as string) : t('appApi.never')}</div>
|
||||
<div className='flex grow px-3'>
|
||||
<Tooltip
|
||||
popupContent={copyValue === api.token ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
|
||||
popupClassName='mr-1'
|
||||
>
|
||||
<div className={`mr-1 flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover ${s.copyIcon} ${copyValue === api.token ? s.copied : ''}`} onClick={() => {
|
||||
// setIsCopied(true)
|
||||
copy(api.token)
|
||||
setCopyValue(api.token)
|
||||
}}></div>
|
||||
</Tooltip>
|
||||
{isCurrentWorkspaceManager
|
||||
&& <div className={`flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-lg ${s.trashIcon}`} onClick={() => {
|
||||
setDelKeyId(api.id)
|
||||
setShowConfirmDelete(true)
|
||||
}}>
|
||||
</div>
|
||||
}
|
||||
<div className='flex grow space-x-2 px-3'>
|
||||
<CopyFeedback content={api.token} />
|
||||
{isCurrentWorkspaceManager && (
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
setDelKeyId(api.id)
|
||||
setShowConfirmDelete(true)
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { Menu, MenuButton, MenuItems, Transition } from '@headlessui/react'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { basePath } from '@/utils/var'
|
||||
import PlanBadge from '@/app/components/header/plan-badge'
|
||||
import { switchWorkspace } from '@/service/common'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
|
@ -22,7 +23,7 @@ const WorkplaceSelector = () => {
|
|||
return
|
||||
await switchWorkspace({ url: '/workspaces/switch', body: { tenant_id } })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
location.assign(`${location.origin}`)
|
||||
location.assign(`${location.origin}${basePath}`)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('common.provider.saveFailed') })
|
||||
|
|
|
@ -3,6 +3,7 @@ import DataSourceNotion from './data-source-notion'
|
|||
import DataSourceWebsite from './data-source-website'
|
||||
import { fetchDataSource } from '@/service/common'
|
||||
import { DataSourceProvider } from '@/models/common'
|
||||
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
|
||||
|
||||
export default function DataSourcePage() {
|
||||
const { data } = useSWR({ url: 'data-source/integrates' }, fetchDataSource)
|
||||
|
@ -11,9 +12,9 @@ export default function DataSourcePage() {
|
|||
return (
|
||||
<div className='mb-8'>
|
||||
<DataSourceNotion workspaces={notionWorkspaces} />
|
||||
<DataSourceWebsite provider={DataSourceProvider.jinaReader} />
|
||||
<DataSourceWebsite provider={DataSourceProvider.fireCrawl} />
|
||||
<DataSourceWebsite provider={DataSourceProvider.waterCrawl} />
|
||||
{ENABLE_WEBSITE_JINAREADER && <DataSourceWebsite provider={DataSourceProvider.jinaReader} />}
|
||||
{ENABLE_WEBSITE_FIRECRAWL && <DataSourceWebsite provider={DataSourceProvider.fireCrawl} />}
|
||||
{ENABLE_WEBSITE_WATERCRAWL && <DataSourceWebsite provider={DataSourceProvider.waterCrawl} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { t } from 'i18next'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import s from './index.module.css'
|
||||
|
@ -18,7 +19,7 @@ const InvitationLink = ({
|
|||
const selector = useRef(`invite-link-${randomString(4)}`)
|
||||
|
||||
const copyHandle = useCallback(() => {
|
||||
copy(`${!value.url.startsWith('http') ? window.location.origin : ''}${value.url}`)
|
||||
copy(`${!value.url.startsWith('http') ? window.location.origin : ''}${basePath}${value.url}`)
|
||||
setIsCopied(true)
|
||||
}, [value])
|
||||
|
||||
|
@ -41,7 +42,7 @@ const InvitationLink = ({
|
|||
<Tooltip
|
||||
popupContent={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
|
||||
>
|
||||
<div className='r-0 absolute left-0 top-0 w-full cursor-pointer truncate pl-2 pr-2' onClick={copyHandle}>{value.url}</div>
|
||||
<div className='r-0 absolute left-0 top-0 w-full cursor-pointer truncate pl-2 pr-2' onClick={copyHandle}>{basePath + value.url}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="h-4 shrink-0 border bg-divider-regular" />
|
||||
|
|
|
@ -3,6 +3,7 @@ import type {
|
|||
Model,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { useLanguage } from '../hooks'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import { OpenaiBlue, OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
|
||||
|
@ -30,7 +31,7 @@ const ModelIcon: FC<ModelIconProps> = ({
|
|||
if (provider?.icon_small) {
|
||||
return (
|
||||
<div className={cn('flex h-5 w-5 items-center justify-center', isDeprecated && 'opacity-50', className)}>
|
||||
<img alt='model-icon' src={renderI18nObject(provider.icon_small, language)}/>
|
||||
<img alt='model-icon' src={basePath + renderI18nObject(provider.icon_small, language)}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { FC } from 'react'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { useLanguage } from '../hooks'
|
||||
import { Openai } from '@/app/components/base/icons/src/vender/other'
|
||||
import { AnthropicDark, AnthropicLight } from '@/app/components/base/icons/src/public/llm'
|
||||
|
@ -40,7 +41,7 @@ const ProviderIcon: FC<ProviderIconProps> = ({
|
|||
<div className={cn('inline-flex items-center gap-2', className)}>
|
||||
<img
|
||||
alt='provider-icon'
|
||||
src={renderI18nObject(provider.icon_small, language)}
|
||||
src={basePath + renderI18nObject(provider.icon_small, language)}
|
||||
className='h-6 w-6'
|
||||
/>
|
||||
<div className='system-md-semibold text-text-primary'>
|
||||
|
|
|
@ -104,7 +104,10 @@ const PluginItem: FC<Props> = ({
|
|||
{!isDifyVersionCompatible && <Tooltip popupContent={
|
||||
t('plugin.difyVersionNotCompatible', { minimalDifyVersion: declarationMeta.minimum_dify_version })
|
||||
}><RiErrorWarningLine color='red' className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" /></Tooltip>}
|
||||
<Badge className='ml-1 shrink-0' text={source === PluginSource.github ? plugin.meta!.version : plugin.version} />
|
||||
<Badge className='ml-1 shrink-0'
|
||||
text={source === PluginSource.github ? plugin.meta!.version : plugin.version}
|
||||
hasRedCornerMark={(source === PluginSource.marketplace) && !!plugin.latest_unique_identifier && plugin.latest_unique_identifier !== plugin_unique_identifier}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Description text={descriptionText} descriptionLineRows={1}></Description>
|
||||
|
|
|
@ -406,8 +406,7 @@ export type VersionProps = {
|
|||
export type StrategyParamItem = {
|
||||
name: string
|
||||
label: Record<Locale, string>
|
||||
human_description: Record<Locale, string>
|
||||
llm_description: string
|
||||
help: Record<Locale, string>
|
||||
placeholder: Record<Locale, string>
|
||||
type: string
|
||||
scope: string
|
||||
|
|
|
@ -14,6 +14,7 @@ import Type from './type'
|
|||
import Category from './category'
|
||||
import Tools from './tools'
|
||||
import cn from '@/utils/classnames'
|
||||
import { basePath } from '@/utils/var'
|
||||
import I18n from '@/context/i18n'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
@ -57,6 +58,12 @@ const AddToolModal: FC<Props> = ({
|
|||
const getAllTools = async () => {
|
||||
setListLoading(true)
|
||||
const buildInTools = await fetchAllBuiltInTools()
|
||||
if (basePath) {
|
||||
buildInTools.forEach((item) => {
|
||||
if (typeof item.icon == 'string' && !item.icon.includes(basePath))
|
||||
item.icon = `${basePath}${item.icon}`
|
||||
})
|
||||
}
|
||||
const customTools = await fetchAllCustomTools()
|
||||
const workflowTools = await fetchAllWorkflowTools()
|
||||
const mergedToolList = [
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddLine,
|
||||
|
@ -53,7 +54,7 @@ const Blocks = ({
|
|||
>
|
||||
<div className='flex h-[22px] w-full items-center justify-between pl-3 pr-1 text-xs font-medium text-gray-500'>
|
||||
{toolWithProvider.label[language]}
|
||||
<a className='hidden cursor-pointer items-center group-hover:flex' href={`/tools?category=${toolWithProvider.type}`} target='_blank'>{t('tools.addToolModal.manageInTools')}<ArrowUpRight className='ml-0.5 h-3 w-3' /></a>
|
||||
<a className='hidden cursor-pointer items-center group-hover:flex' href={`${basePath}/tools?category=${toolWithProvider.type}`} target='_blank'>{t('tools.addToolModal.manageInTools')}<ArrowUpRight className='ml-0.5 h-3 w-3' /></a>
|
||||
</div>
|
||||
{list.map((tool) => {
|
||||
const labelContent = (() => {
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import { AuthHeaderPrefix, AuthType, CollectionType } from '../types'
|
||||
import { basePath } from '@/utils/var'
|
||||
import type { Collection, CustomCollectionBackend, Tool, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '../types'
|
||||
import ToolItem from './tool-item'
|
||||
import cn from '@/utils/classnames'
|
||||
|
@ -276,7 +277,7 @@ const ProviderDetail = ({
|
|||
variant='primary'
|
||||
className={cn('my-3 w-[183px] shrink-0')}
|
||||
>
|
||||
<a className='flex items-center' href={`/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'>
|
||||
<a className='flex items-center' href={`${basePath}/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'>
|
||||
<div className='system-sm-medium'>{t('tools.openInStudio')}</div>
|
||||
<LinkExternal02 className='ml-1 h-4 w-4' />
|
||||
</a>
|
||||
|
|
|
@ -59,6 +59,7 @@ import { CollectionType } from '@/app/components/tools/types'
|
|||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
||||
import { useWorkflowConfig } from '@/service/use-workflow'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { canFindTool } from '@/utils'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
|
@ -446,6 +447,12 @@ export const useFetchToolsData = () => {
|
|||
if (type === 'builtin') {
|
||||
const buildInTools = await fetchAllBuiltInTools()
|
||||
|
||||
if (basePath) {
|
||||
buildInTools.forEach((item) => {
|
||||
if (typeof item.icon == 'string' && !item.icon.includes(basePath))
|
||||
item.icon = `${basePath}${item.icon}`
|
||||
})
|
||||
}
|
||||
workflowStore.setState({
|
||||
buildInTools: buildInTools || [],
|
||||
})
|
||||
|
|
|
@ -65,7 +65,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
|||
switch (schema.type) {
|
||||
case FormTypeEnum.textInput: {
|
||||
const def = schema as CredentialFormSchemaTextInput
|
||||
const value = props.value[schema.variable]
|
||||
const value = props.value[schema.variable] || schema.default
|
||||
const onChange = (value: string) => {
|
||||
props.onChange({ ...props.value, [schema.variable]: value })
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export function strategyParamToCredientialForm(param: StrategyParamItem): Creden
|
|||
variable: param.name,
|
||||
show_on: [],
|
||||
type: toType(param.type),
|
||||
tooltip: param.help,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,6 +54,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
|
|||
outputSchema,
|
||||
handleMemoryChange,
|
||||
} = useConfig(props.id, props.data)
|
||||
console.log('currentStrategy', currentStrategy)
|
||||
const { t } = useTranslation()
|
||||
const nodeInfo = useMemo(() => {
|
||||
if (!runResult)
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from '@/service/debug'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
|
||||
type ChatWrapperProps = {
|
||||
showConversationVariableModal: boolean
|
||||
|
@ -94,10 +95,14 @@ const ChatWrapper = (
|
|||
)
|
||||
}, [handleSend, workflowStore, conversationId, chatList, appDetail])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
|
||||
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
|
||||
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.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])
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useCallback, useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { basePath } from '@/utils/var'
|
||||
import cn from 'classnames'
|
||||
import { CheckCircleIcon } from '@heroicons/react/24/solid'
|
||||
import Input from '../components/base/input'
|
||||
|
@ -163,7 +164,7 @@ const ChangePasswordForm = () => {
|
|||
</div>
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
<Button variant='primary' className='w-full'>
|
||||
<a href="/signin">{t('login.passwordChanged')}</a>
|
||||
<a href={`${basePath}/signin`}>{t('login.passwordChanged')}</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue