Merge branch 'main' into feat/structured-output

This commit is contained in:
Novice 2025-04-16 18:12:00 +08:00
commit 5aa541fe37
115 changed files with 7301 additions and 11331 deletions

View File

@ -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

View File

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

View File

@ -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 }}

34
.github/actions/setup-uv/action.yml vendored Normal file
View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -27,7 +27,6 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}

View File

@ -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

View File

@ -23,7 +23,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Check changed files

1
.gitignore vendored
View File

@ -46,6 +46,7 @@ htmlcov/
.cache
nosetests.xml
coverage.xml
coverage.json
*.cover
*.py,cover
.hypothesis/

View File

@ -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

View File

@ -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
```

View File

@ -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()

View File

@ -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 = {

View File

@ -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:

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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()

View File

@ -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]

View File

@ -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}'"),
)

View File

@ -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)

View File

@ -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")

View File

@ -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:

View File

@ -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,
}

View File

@ -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,

View File

@ -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:

View File

@ -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"):

View File

@ -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 = {

View File

@ -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)
]

10605
api/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +0,0 @@
[virtualenvs]
in-project = true
create = true
prefer-active-python = true

View File

@ -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",
]

View File

@ -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

View File

@ -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(

View File

@ -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"
)

6286
api/uv.lock Normal file

File diff suppressed because it is too large Load Diff

7
dev/mypy-check Executable file
View File

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

View File

@ -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

View File

@ -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 .

View File

@ -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

10
dev/sync-uv Executable file
View File

@ -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

View File

@ -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

22
dev/update-uv Executable file
View File

@ -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

View File

@ -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

View File

@ -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:-}

View File

@ -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:-}

View File

@ -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:-}

View File

@ -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

View File

@ -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}

View File

@ -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')
}

View File

@ -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'

View File

@ -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>

View File

@ -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>

View File

@ -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')
}

View File

@ -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(() => {

View File

@ -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)

View File

@ -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>

View File

@ -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)

View File

@ -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) => {

View File

@ -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'

View File

@ -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)

View File

@ -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(() => {

View File

@ -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}`,
})
})

View File

@ -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} />

View File

@ -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>
)

View File

@ -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>
)
)
}

View File

@ -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}
/>
)
})

View File

@ -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>

View File

@ -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}

View File

@ -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,
}
})

View File

@ -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)}
/>

View File

@ -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'
/>

View File

@ -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>
)
}

View File

@ -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,

View File

@ -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 && (

View File

@ -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 (
<>

View File

@ -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>
)

View File

@ -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) {

View File

@ -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} />

View File

@ -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

View File

@ -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>
))}

View File

@ -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') })

View File

@ -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>
)
}

View File

@ -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" />

View File

@ -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>
)
}

View File

@ -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'>

View File

@ -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>

View File

@ -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

View File

@ -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 = [

View File

@ -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 = (() => {

View File

@ -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>

View File

@ -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 || [],
})

View File

@ -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 })
}

View File

@ -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)

View File

@ -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, () => {

View File

@ -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