mirror of
https://github.com/docling-project/docling-serve.git
synced 2025-11-29 08:33:50 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b6dd8a0d0 | ||
|
|
513ae0c119 | ||
|
|
bde040661f | ||
|
|
496f7ec26b | ||
|
|
9d6def0ec8 | ||
|
|
a4fed2d965 | ||
|
|
b0360d723b | ||
|
|
4adc0dfa79 | ||
|
|
40c7f1bcd3 | ||
|
|
d64a2a974a | ||
|
|
0d4545a65a | ||
|
|
fe98338239 | ||
|
|
b844ce737e | ||
|
|
27fdd7b85a | ||
|
|
1df62adf01 | ||
|
|
e5449472b2 | ||
|
|
81f0a8ddf8 | ||
|
|
a69cc867f5 |
56
.github/scripts/release.sh
vendored
56
.github/scripts/release.sh
vendored
@@ -3,32 +3,68 @@
|
|||||||
set -e # trigger failure on error - do not remove!
|
set -e # trigger failure on error - do not remove!
|
||||||
set -x # display command on output
|
set -x # display command on output
|
||||||
|
|
||||||
|
## debug
|
||||||
|
# TARGET_VERSION="1.2.x"
|
||||||
|
|
||||||
if [ -z "${TARGET_VERSION}" ]; then
|
if [ -z "${TARGET_VERSION}" ]; then
|
||||||
>&2 echo "No TARGET_VERSION specified"
|
>&2 echo "No TARGET_VERSION specified"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
CHGLOG_FILE="${CHGLOG_FILE:-CHANGELOG.md}"
|
CHGLOG_FILE="${CHGLOG_FILE:-CHANGELOG.md}"
|
||||||
|
|
||||||
# update package version
|
# Update package version
|
||||||
uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "${TARGET_VERSION}"
|
uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "${TARGET_VERSION}"
|
||||||
uv lock --upgrade-package docling-serve
|
uv lock --upgrade-package docling-serve
|
||||||
|
|
||||||
# collect release notes
|
# Extract all docling packages and versions from uv.lock
|
||||||
|
DOCVERSIONS=$(uvx --with toml python3 - <<'PY'
|
||||||
|
import toml
|
||||||
|
data = toml.load("uv.lock")
|
||||||
|
for pkg in data.get("package", []):
|
||||||
|
if pkg["name"].startswith("docling"):
|
||||||
|
print(f"{pkg['name']} {pkg['version']}")
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format docling versions list without trailing newline
|
||||||
|
DOCLING_VERSIONS="### Docling libraries included in this release:"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
DOCLING_VERSIONS+="
|
||||||
|
- $line"
|
||||||
|
done <<< "$DOCVERSIONS"
|
||||||
|
|
||||||
|
# Collect release notes
|
||||||
REL_NOTES=$(mktemp)
|
REL_NOTES=$(mktemp)
|
||||||
uv run --no-sync semantic-release changelog --unreleased >> "${REL_NOTES}"
|
uv run --no-sync semantic-release changelog --unreleased >> "${REL_NOTES}"
|
||||||
|
|
||||||
# update changelog
|
# Strip trailing blank lines from release notes and append docling versions
|
||||||
|
{
|
||||||
|
sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba' "${REL_NOTES}"
|
||||||
|
printf "\n"
|
||||||
|
printf "%s" "${DOCLING_VERSIONS}"
|
||||||
|
printf "\n"
|
||||||
|
} > "${REL_NOTES}.tmp" && mv "${REL_NOTES}.tmp" "${REL_NOTES}"
|
||||||
|
|
||||||
|
# Update changelog
|
||||||
TMP_CHGLOG=$(mktemp)
|
TMP_CHGLOG=$(mktemp)
|
||||||
TARGET_TAG_NAME="v${TARGET_VERSION}"
|
TARGET_TAG_NAME="v${TARGET_VERSION}"
|
||||||
RELEASE_URL="$(gh repo view --json url -q ".url")/releases/tag/${TARGET_TAG_NAME}"
|
RELEASE_URL="$(gh repo view --json url -q ".url")/releases/tag/${TARGET_TAG_NAME}"
|
||||||
printf "## [${TARGET_TAG_NAME}](${RELEASE_URL}) - $(date -Idate)\n\n" >> "${TMP_CHGLOG}"
|
## debug
|
||||||
cat "${REL_NOTES}" >> "${TMP_CHGLOG}"
|
#RELEASE_URL="myrepo/releases/tag/${TARGET_TAG_NAME}"
|
||||||
if [ -f "${CHGLOG_FILE}" ]; then
|
|
||||||
printf "\n" | cat - "${CHGLOG_FILE}" >> "${TMP_CHGLOG}"
|
# Strip leading blank lines from existing changelog to avoid multiple blank lines when appending
|
||||||
fi
|
EXISTING_CL=$(sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba' "${CHGLOG_FILE}")
|
||||||
|
|
||||||
|
{
|
||||||
|
printf "## [${TARGET_TAG_NAME}](${RELEASE_URL}) - $(date -Idate)\n\n"
|
||||||
|
cat "${REL_NOTES}"
|
||||||
|
printf "\n"
|
||||||
|
printf "%s\n" "${EXISTING_CL}"
|
||||||
|
} >> "${TMP_CHGLOG}"
|
||||||
|
|
||||||
mv "${TMP_CHGLOG}" "${CHGLOG_FILE}"
|
mv "${TMP_CHGLOG}" "${CHGLOG_FILE}"
|
||||||
|
|
||||||
# push changes
|
# Push changes
|
||||||
git config --global user.name 'github-actions[bot]'
|
git config --global user.name 'github-actions[bot]'
|
||||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
git add pyproject.toml uv.lock "${CHGLOG_FILE}"
|
git add pyproject.toml uv.lock "${CHGLOG_FILE}"
|
||||||
@@ -36,5 +72,5 @@ COMMIT_MSG="chore: bump version to ${TARGET_VERSION} [skip ci]"
|
|||||||
git commit -m "${COMMIT_MSG}"
|
git commit -m "${COMMIT_MSG}"
|
||||||
git push origin main
|
git push origin main
|
||||||
|
|
||||||
# create GitHub release (incl. Git tag)
|
# Create GitHub release (incl. Git tag)
|
||||||
gh release create "${TARGET_TAG_NAME}" -F "${REL_NOTES}"
|
gh release create "${TARGET_TAG_NAME}" -F "${REL_NOTES}"
|
||||||
|
|||||||
2
.github/workflows/actionlint.yml
vendored
2
.github/workflows/actionlint.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
actionlint:
|
actionlint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Download actionlint
|
- name: Download actionlint
|
||||||
id: get_actionlint
|
id: get_actionlint
|
||||||
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
|
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
|
||||||
|
|||||||
4
.github/workflows/cd.yml
vendored
4
.github/workflows/cd.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
TARGET_TAG_V: ${{ steps.version_check.outputs.TRGT_VERSION }}
|
TARGET_TAG_V: ${{ steps.version_check.outputs.TRGT_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # for fetching tags, required for semantic-release
|
fetch-depth: 0 # for fetching tags, required for semantic-release
|
||||||
- name: Install uv and set the python version
|
- name: Install uv and set the python version
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
app-id: ${{ vars.CI_APP_ID }}
|
app-id: ${{ vars.CI_APP_ID }}
|
||||||
private-key: ${{ secrets.CI_PRIVATE_KEY }}
|
private-key: ${{ secrets.CI_PRIVATE_KEY }}
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
fetch-depth: 0 # for fetching tags, required for semantic-release
|
fetch-depth: 0 # for fetching tags, required for semantic-release
|
||||||
|
|||||||
2
.github/workflows/job-build.yml
vendored
2
.github/workflows/job-build.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.12']
|
python-version: ['3.12']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Install uv and set the python version
|
- name: Install uv and set the python version
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
18
.github/workflows/job-checks.yml
vendored
18
.github/workflows/job-checks.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.12']
|
python-version: ['3.12']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Install uv and set the python version
|
- name: Install uv and set the python version
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@v6
|
||||||
with:
|
with:
|
||||||
@@ -58,11 +58,11 @@ jobs:
|
|||||||
- name: Create the server
|
- name: Create the server
|
||||||
run: .venv/bin/python -c 'from docling_serve.app import create_app; create_app()'
|
run: .venv/bin/python -c 'from docling_serve.app import create_app; create_app()'
|
||||||
|
|
||||||
markdown-lint:
|
# markdown-lint:
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v4
|
# - uses: actions/checkout@v5
|
||||||
- name: markdownlint-cli2-action
|
# - name: markdownlint-cli2-action
|
||||||
uses: DavidAnson/markdownlint-cli2-action@v16
|
# uses: DavidAnson/markdownlint-cli2-action@v16
|
||||||
with:
|
# with:
|
||||||
globs: "**/*.md"
|
# globs: "**/*.md"
|
||||||
|
|||||||
109
.github/workflows/job-image.yml
vendored
109
.github/workflows/job-image.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
|||||||
df -h
|
df -h
|
||||||
|
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Log in to the GHCR container image registry
|
- name: Log in to the GHCR container image registry
|
||||||
if: ${{ inputs.publish }}
|
if: ${{ inputs.publish }}
|
||||||
@@ -88,19 +88,114 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ env.GHCR_REGISTRY }}/${{ inputs.ghcr_image_name }}
|
images: ${{ env.GHCR_REGISTRY }}/${{ inputs.ghcr_image_name }}
|
||||||
|
|
||||||
|
# # Local test
|
||||||
|
# - name: Set metadata outputs for local testing ## comment out Free up space, Log in to cr, Cache Docker, Extract metadata, and quay blocks and run act
|
||||||
|
# id: ghcr_meta
|
||||||
|
# run: |
|
||||||
|
# echo "tags=ghcr.io/docling-project/docling-serve:pr-123" >> $GITHUB_OUTPUT
|
||||||
|
# echo "labels=org.opencontainers.image.source=https://github.com/docling-project/docling-serve" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build and push image to ghcr.io
|
- name: Build and push image to ghcr.io
|
||||||
id: ghcr_push
|
id: ghcr_push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ inputs.publish }}
|
push: ${{ inputs.publish }} # set 'false' for local test
|
||||||
tags: ${{ steps.ghcr_meta.outputs.tags }}
|
tags: ${{ steps.ghcr_meta.outputs.tags }}
|
||||||
labels: ${{ steps.ghcr_meta.outputs.labels }}
|
labels: ${{ steps.ghcr_meta.outputs.labels }}
|
||||||
platforms: ${{ inputs.platforms}}
|
platforms: ${{ inputs.platforms }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
file: Containerfile
|
file: Containerfile
|
||||||
build-args: ${{ inputs.build_args }}
|
build-args: ${{ inputs.build_args }}
|
||||||
|
##
|
||||||
|
## This stage runs after the build, so it leverages all build cache
|
||||||
|
##
|
||||||
|
- name: Export built image for testing
|
||||||
|
id: ghcr_export_built_image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: ${{ env.GHCR_REGISTRY }}/${{ inputs.ghcr_image_name }}:${{ github.sha }}-test
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.title=docling-serve
|
||||||
|
org.opencontainers.image.test=true
|
||||||
|
platforms: linux/amd64 # when 'load' is true, we can't use a list ${{ inputs.platforms }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
file: Containerfile
|
||||||
|
build-args: ${{ inputs.build_args }}
|
||||||
|
|
||||||
|
- name: Test image
|
||||||
|
if: steps.ghcr_export_built_image.outcome == 'success'
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
IMAGE_TAG="${{ env.GHCR_REGISTRY }}/${{ inputs.ghcr_image_name }}:${{ github.sha }}-test"
|
||||||
|
echo "Testing local image: $IMAGE_TAG"
|
||||||
|
|
||||||
|
# Remove existing container if any
|
||||||
|
docker rm -f docling-serve-test-container 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Starting container..."
|
||||||
|
docker run -d -p 5001:5001 --name docling-serve-test-container "$IMAGE_TAG"
|
||||||
|
|
||||||
|
echo "Waiting 15s for container to boot..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
echo "Checking service health..."
|
||||||
|
for i in {1..20}; do
|
||||||
|
HEALTH_RESPONSE=$(curl -s http://localhost:5001/health || true)
|
||||||
|
echo "Health check response [$i]: $HEALTH_RESPONSE"
|
||||||
|
|
||||||
|
if echo "$HEALTH_RESPONSE" | grep -q '"status":"ok"'; then
|
||||||
|
echo "Service is healthy!"
|
||||||
|
|
||||||
|
# Install pytest and dependencies
|
||||||
|
echo "Installing pytest and dependencies..."
|
||||||
|
pip install uv
|
||||||
|
uv venv --allow-existing
|
||||||
|
source .venv/bin/activate
|
||||||
|
uv sync --all-extras --no-extra flash-attn
|
||||||
|
|
||||||
|
# Run pytest tests
|
||||||
|
echo "Running tests..."
|
||||||
|
# Test import
|
||||||
|
python -c 'from docling_serve.app import create_app; create_app()'
|
||||||
|
|
||||||
|
# Run pytest and check result directly
|
||||||
|
if ! pytest -sv -k "test_convert_url" tests/test_1-url-async.py \
|
||||||
|
--disable-warnings; then
|
||||||
|
echo "Tests failed!"
|
||||||
|
docker logs docling-serve-test-container
|
||||||
|
docker rm -f docling-serve-test-container
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Tests passed successfully!"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "Waiting for service... [$i/20]"
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Final health check if service didn't pass earlier
|
||||||
|
if ! echo "$HEALTH_RESPONSE" | grep -q '"status":"ok"'; then
|
||||||
|
echo "Service did not become healthy in time."
|
||||||
|
docker logs docling-serve-test-container
|
||||||
|
docker rm -f docling-serve-test-container
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
echo "Cleaning up test container..."
|
||||||
|
docker rm -f docling-serve-test-container
|
||||||
|
echo "Cleaning up test image..."
|
||||||
|
docker rmi "$IMAGE_TAG"
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
if: ${{ inputs.publish }}
|
if: ${{ inputs.publish }}
|
||||||
@@ -120,7 +215,7 @@ jobs:
|
|||||||
- name: Build and push image to quay.io
|
- name: Build and push image to quay.io
|
||||||
if: ${{ inputs.publish }}
|
if: ${{ inputs.publish }}
|
||||||
# id: push-serve-cpu-quay
|
# id: push-serve-cpu-quay
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ inputs.publish }}
|
push: ${{ inputs.publish }}
|
||||||
@@ -132,10 +227,6 @@ jobs:
|
|||||||
file: Containerfile
|
file: Containerfile
|
||||||
build-args: ${{ inputs.build_args }}
|
build-args: ${{ inputs.build_args }}
|
||||||
|
|
||||||
# - name: Inspect the image details
|
|
||||||
# run: |
|
|
||||||
# echo "${{ steps.ghcr_push.outputs.metadata }}"
|
|
||||||
|
|
||||||
- name: Remove Local Docker Images
|
- name: Remove Local Docker Images
|
||||||
run: |
|
run: |
|
||||||
docker image prune -af
|
docker image prune -af
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -446,3 +446,6 @@ pip-selfcheck.json
|
|||||||
.markdown-lint
|
.markdown-lint
|
||||||
|
|
||||||
cookies.txt
|
cookies.txt
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
/examples/splitted_pdf/*
|
||||||
@@ -7,12 +7,12 @@ repos:
|
|||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
name: "Ruff formatter"
|
name: "Ruff formatter"
|
||||||
args: [--config=pyproject.toml]
|
args: [--config=pyproject.toml]
|
||||||
files: '^(docling_serve|tests).*\.(py|ipynb)$'
|
files: '^(docling_serve|tests|examples).*\.(py|ipynb)$'
|
||||||
# Run the Ruff linter.
|
# Run the Ruff linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
name: "Ruff linter"
|
name: "Ruff linter"
|
||||||
args: [--exit-non-zero-on-fix, --fix, --config=pyproject.toml]
|
args: [--exit-non-zero-on-fix, --fix, --config=pyproject.toml]
|
||||||
files: '^(docling_serve|tests).*\.(py|ipynb)$'
|
files: '^(docling_serve|tests|examples).*\.(py|ipynb)$'
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: system
|
- id: system
|
||||||
|
|||||||
75
CHANGELOG.md
75
CHANGELOG.md
@@ -1,3 +1,78 @@
|
|||||||
|
## [v1.5.1](https://github.com/docling-project/docling-serve/releases/tag/v1.5.1) - 2025-09-17
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Remove old dependencies, fixes in docling-parse and more minor dependencies upgrade ([#362](https://github.com/docling-project/docling-serve/issues/362)) ([`513ae0c`](https://github.com/docling-project/docling-serve/commit/513ae0c119b66d3b17cf9a5d371a0f7971f43be7))
|
||||||
|
* Updates rapidocr deps ([#361](https://github.com/docling-project/docling-serve/issues/361)) ([`bde0406`](https://github.com/docling-project/docling-serve/commit/bde040661fb65c67699326cd6281c0e6232e26f2))
|
||||||
|
|
||||||
|
### Docling libraries included in this release:
|
||||||
|
- docling 2.52.0
|
||||||
|
- docling-core 2.48.1
|
||||||
|
- docling-ibm-models 3.9.1
|
||||||
|
- docling-jobkit 1.5.0
|
||||||
|
- docling-mcp 1.2.0
|
||||||
|
- docling-parse 4.5.0
|
||||||
|
- docling-serve 1.5.1
|
||||||
|
|
||||||
|
## [v1.5.0](https://github.com/docling-project/docling-serve/releases/tag/v1.5.0) - 2025-09-09
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
|
||||||
|
* Add chunking endpoints ([#353](https://github.com/docling-project/docling-serve/issues/353)) ([`9d6def0`](https://github.com/docling-project/docling-serve/commit/9d6def0ec8b1804ad31aa71defa17658d73d29a1))
|
||||||
|
|
||||||
|
### Docling libraries included in this release:
|
||||||
|
- docling 2.46.0
|
||||||
|
- docling 2.51.0
|
||||||
|
- docling-core 2.47.0
|
||||||
|
- docling-ibm-models 3.9.1
|
||||||
|
- docling-jobkit 1.5.0
|
||||||
|
- docling-mcp 1.2.0
|
||||||
|
- docling-parse 4.4.0
|
||||||
|
- docling-serve 1.5.0
|
||||||
|
|
||||||
|
## [v1.4.1](https://github.com/docling-project/docling-serve/releases/tag/v1.4.1) - 2025-09-08
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Trigger fix after ci fixes ([#355](https://github.com/docling-project/docling-serve/issues/355)) ([`b0360d7`](https://github.com/docling-project/docling-serve/commit/b0360d723bff202dcf44a25a3173ec1995945fc2))
|
||||||
|
|
||||||
|
### Docling libraries included in this release:
|
||||||
|
- docling 2.46.0
|
||||||
|
- docling 2.51.0
|
||||||
|
- docling-core 2.47.0
|
||||||
|
- docling-ibm-models 3.9.1
|
||||||
|
- docling-jobkit 1.4.1
|
||||||
|
- docling-mcp 1.2.0
|
||||||
|
- docling-parse 4.4.0
|
||||||
|
- docling-serve 1.4.1
|
||||||
|
|
||||||
|
## [v1.4.0](https://github.com/docling-project/docling-serve/releases/tag/v1.4.0) - 2025-09-05
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
|
||||||
|
* **docling:** Perfomance improvements in parsing, new layout model, fixes in html processing ([#352](https://github.com/docling-project/docling-serve/issues/352)) ([`d64a2a9`](https://github.com/docling-project/docling-serve/commit/d64a2a974a276c7ae3b105c448fd79f77a653d20))
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Upgrade to latest docling version with fixes ([#335](https://github.com/docling-project/docling-serve/issues/335)) ([`e544947`](https://github.com/docling-project/docling-serve/commit/e5449472b2a3e71796f41c8a58c251d8229305c1))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* Add split processing example ([#303](https://github.com/docling-project/docling-serve/issues/303)) ([`0d4545a`](https://github.com/docling-project/docling-serve/commit/0d4545a65a5a941fc1fdefda57e39cfb1ea106ab))
|
||||||
|
* Document DOCLING_NUM_THREADS environment variable ([#341](https://github.com/docling-project/docling-serve/issues/341)) ([`27fdd7b`](https://github.com/docling-project/docling-serve/commit/27fdd7b85ab18b3eece428366f46dc5cf0995e38))
|
||||||
|
* Fix parameters typo ([#333](https://github.com/docling-project/docling-serve/issues/333)) ([`81f0a8d`](https://github.com/docling-project/docling-serve/commit/81f0a8ddf80a532042d550ae4568f891458b45e7))
|
||||||
|
* Describe how to use Docling MCP ([#332](https://github.com/docling-project/docling-serve/issues/332)) ([`a69cc86`](https://github.com/docling-project/docling-serve/commit/a69cc867f5a3fb76648803ca866d65cc3a75c6b8))
|
||||||
|
|
||||||
|
### Docling libraries included in this release:
|
||||||
|
- docling 2.46.0
|
||||||
|
- docling 2.51.0
|
||||||
|
- docling-core 2.47.0
|
||||||
|
- docling-ibm-models 3.9.1
|
||||||
|
- docling-jobkit 1.4.1
|
||||||
|
- docling-mcp 1.2.0
|
||||||
|
- docling-parse 4.4.0
|
||||||
|
- docling-serve 1.4.0
|
||||||
|
|
||||||
## [v1.3.1](https://github.com/docling-project/docling-serve/releases/tag/v1.3.1) - 2025-08-21
|
## [v1.3.1](https://github.com/docling-project/docling-serve/releases/tag/v1.3.1) - 2025-08-21
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|||||||
@@ -35,9 +35,15 @@ from docling_jobkit.datamodel.callback import (
|
|||||||
ProgressCallbackRequest,
|
ProgressCallbackRequest,
|
||||||
ProgressCallbackResponse,
|
ProgressCallbackResponse,
|
||||||
)
|
)
|
||||||
|
from docling_jobkit.datamodel.chunking import (
|
||||||
|
BaseChunkerOptions,
|
||||||
|
ChunkingExportOptions,
|
||||||
|
HierarchicalChunkerOptions,
|
||||||
|
HybridChunkerOptions,
|
||||||
|
)
|
||||||
from docling_jobkit.datamodel.http_inputs import FileSource, HttpSource
|
from docling_jobkit.datamodel.http_inputs import FileSource, HttpSource
|
||||||
from docling_jobkit.datamodel.s3_coords import S3Coordinates
|
from docling_jobkit.datamodel.s3_coords import S3Coordinates
|
||||||
from docling_jobkit.datamodel.task import Task, TaskSource
|
from docling_jobkit.datamodel.task import Task, TaskSource, TaskType
|
||||||
from docling_jobkit.datamodel.task_targets import (
|
from docling_jobkit.datamodel.task_targets import (
|
||||||
InBodyTarget,
|
InBodyTarget,
|
||||||
TaskTarget,
|
TaskTarget,
|
||||||
@@ -54,11 +60,14 @@ from docling_serve.datamodel.convert import ConvertDocumentsRequestOptions
|
|||||||
from docling_serve.datamodel.requests import (
|
from docling_serve.datamodel.requests import (
|
||||||
ConvertDocumentsRequest,
|
ConvertDocumentsRequest,
|
||||||
FileSourceRequest,
|
FileSourceRequest,
|
||||||
|
GenericChunkDocumentsRequest,
|
||||||
HttpSourceRequest,
|
HttpSourceRequest,
|
||||||
S3SourceRequest,
|
S3SourceRequest,
|
||||||
TargetName,
|
TargetName,
|
||||||
|
make_request_model,
|
||||||
)
|
)
|
||||||
from docling_serve.datamodel.responses import (
|
from docling_serve.datamodel.responses import (
|
||||||
|
ChunkDocumentResponse,
|
||||||
ClearResponse,
|
ClearResponse,
|
||||||
ConvertDocumentResponse,
|
ConvertDocumentResponse,
|
||||||
HealthCheckResponse,
|
HealthCheckResponse,
|
||||||
@@ -249,10 +258,11 @@ def create_app(): # noqa: C901
|
|||||||
########################
|
########################
|
||||||
|
|
||||||
async def _enque_source(
|
async def _enque_source(
|
||||||
orchestrator: BaseOrchestrator, conversion_request: ConvertDocumentsRequest
|
orchestrator: BaseOrchestrator,
|
||||||
|
request: ConvertDocumentsRequest | GenericChunkDocumentsRequest,
|
||||||
) -> Task:
|
) -> Task:
|
||||||
sources: list[TaskSource] = []
|
sources: list[TaskSource] = []
|
||||||
for s in conversion_request.sources:
|
for s in request.sources:
|
||||||
if isinstance(s, FileSourceRequest):
|
if isinstance(s, FileSourceRequest):
|
||||||
sources.append(FileSource.model_validate(s))
|
sources.append(FileSource.model_validate(s))
|
||||||
elif isinstance(s, HttpSourceRequest):
|
elif isinstance(s, HttpSourceRequest):
|
||||||
@@ -260,17 +270,40 @@ def create_app(): # noqa: C901
|
|||||||
elif isinstance(s, S3SourceRequest):
|
elif isinstance(s, S3SourceRequest):
|
||||||
sources.append(S3Coordinates.model_validate(s))
|
sources.append(S3Coordinates.model_validate(s))
|
||||||
|
|
||||||
|
convert_options: ConvertDocumentsRequestOptions
|
||||||
|
chunking_options: BaseChunkerOptions | None = None
|
||||||
|
chunking_export_options = ChunkingExportOptions()
|
||||||
|
task_type: TaskType
|
||||||
|
if isinstance(request, ConvertDocumentsRequest):
|
||||||
|
task_type = TaskType.CONVERT
|
||||||
|
convert_options = request.options
|
||||||
|
elif isinstance(request, GenericChunkDocumentsRequest):
|
||||||
|
task_type = TaskType.CHUNK
|
||||||
|
convert_options = request.convert_options
|
||||||
|
chunking_options = request.chunking_options
|
||||||
|
chunking_export_options.include_converted_doc = (
|
||||||
|
request.include_converted_doc
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Uknown request type.")
|
||||||
|
|
||||||
task = await orchestrator.enqueue(
|
task = await orchestrator.enqueue(
|
||||||
|
task_type=task_type,
|
||||||
sources=sources,
|
sources=sources,
|
||||||
options=conversion_request.options,
|
convert_options=convert_options,
|
||||||
target=conversion_request.target,
|
chunking_options=chunking_options,
|
||||||
|
chunking_export_options=chunking_export_options,
|
||||||
|
target=request.target,
|
||||||
)
|
)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
async def _enque_file(
|
async def _enque_file(
|
||||||
orchestrator: BaseOrchestrator,
|
orchestrator: BaseOrchestrator,
|
||||||
files: list[UploadFile],
|
files: list[UploadFile],
|
||||||
options: ConvertDocumentsRequestOptions,
|
task_type: TaskType,
|
||||||
|
convert_options: ConvertDocumentsRequestOptions,
|
||||||
|
chunking_options: BaseChunkerOptions | None,
|
||||||
|
chunking_export_options: ChunkingExportOptions | None,
|
||||||
target: TaskTarget,
|
target: TaskTarget,
|
||||||
) -> Task:
|
) -> Task:
|
||||||
_log.info(f"Received {len(files)} files for processing.")
|
_log.info(f"Received {len(files)} files for processing.")
|
||||||
@@ -284,7 +317,12 @@ def create_app(): # noqa: C901
|
|||||||
file_sources.append(DocumentStream(name=name, stream=buf))
|
file_sources.append(DocumentStream(name=name, stream=buf))
|
||||||
|
|
||||||
task = await orchestrator.enqueue(
|
task = await orchestrator.enqueue(
|
||||||
sources=file_sources, options=options, target=target
|
task_type=task_type,
|
||||||
|
sources=file_sources,
|
||||||
|
convert_options=convert_options,
|
||||||
|
chunking_options=chunking_options,
|
||||||
|
chunking_export_options=chunking_export_options,
|
||||||
|
target=target,
|
||||||
)
|
)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
@@ -381,7 +419,7 @@ def create_app(): # noqa: C901
|
|||||||
response = RedirectResponse(url=logo_url)
|
response = RedirectResponse(url=logo_url)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health", tags=["health"])
|
||||||
def health() -> HealthCheckResponse:
|
def health() -> HealthCheckResponse:
|
||||||
return HealthCheckResponse()
|
return HealthCheckResponse()
|
||||||
|
|
||||||
@@ -393,6 +431,7 @@ def create_app(): # noqa: C901
|
|||||||
# Convert a document from URL(s)
|
# Convert a document from URL(s)
|
||||||
@app.post(
|
@app.post(
|
||||||
"/v1/convert/source",
|
"/v1/convert/source",
|
||||||
|
tags=["convert"],
|
||||||
response_model=ConvertDocumentResponse | PresignedUrlConvertDocumentResponse,
|
response_model=ConvertDocumentResponse | PresignedUrlConvertDocumentResponse,
|
||||||
responses={
|
responses={
|
||||||
200: {
|
200: {
|
||||||
@@ -408,7 +447,7 @@ def create_app(): # noqa: C901
|
|||||||
conversion_request: ConvertDocumentsRequest,
|
conversion_request: ConvertDocumentsRequest,
|
||||||
):
|
):
|
||||||
task = await _enque_source(
|
task = await _enque_source(
|
||||||
orchestrator=orchestrator, conversion_request=conversion_request
|
orchestrator=orchestrator, request=conversion_request
|
||||||
)
|
)
|
||||||
completed = await _wait_task_complete(
|
completed = await _wait_task_complete(
|
||||||
orchestrator=orchestrator, task_id=task.task_id
|
orchestrator=orchestrator, task_id=task.task_id
|
||||||
@@ -438,6 +477,7 @@ def create_app(): # noqa: C901
|
|||||||
# Convert a document from file(s)
|
# Convert a document from file(s)
|
||||||
@app.post(
|
@app.post(
|
||||||
"/v1/convert/file",
|
"/v1/convert/file",
|
||||||
|
tags=["convert"],
|
||||||
response_model=ConvertDocumentResponse | PresignedUrlConvertDocumentResponse,
|
response_model=ConvertDocumentResponse | PresignedUrlConvertDocumentResponse,
|
||||||
responses={
|
responses={
|
||||||
200: {
|
200: {
|
||||||
@@ -457,7 +497,13 @@ def create_app(): # noqa: C901
|
|||||||
):
|
):
|
||||||
target = InBodyTarget() if target_type == TargetName.INBODY else ZipTarget()
|
target = InBodyTarget() if target_type == TargetName.INBODY else ZipTarget()
|
||||||
task = await _enque_file(
|
task = await _enque_file(
|
||||||
orchestrator=orchestrator, files=files, options=options, target=target
|
task_type=TaskType.CONVERT,
|
||||||
|
orchestrator=orchestrator,
|
||||||
|
files=files,
|
||||||
|
convert_options=options,
|
||||||
|
chunking_options=None,
|
||||||
|
chunking_export_options=None,
|
||||||
|
target=target,
|
||||||
)
|
)
|
||||||
completed = await _wait_task_complete(
|
completed = await _wait_task_complete(
|
||||||
orchestrator=orchestrator, task_id=task.task_id
|
orchestrator=orchestrator, task_id=task.task_id
|
||||||
@@ -487,6 +533,7 @@ def create_app(): # noqa: C901
|
|||||||
# Convert a document from URL(s) using the async api
|
# Convert a document from URL(s) using the async api
|
||||||
@app.post(
|
@app.post(
|
||||||
"/v1/convert/source/async",
|
"/v1/convert/source/async",
|
||||||
|
tags=["convert"],
|
||||||
response_model=TaskStatusResponse,
|
response_model=TaskStatusResponse,
|
||||||
)
|
)
|
||||||
async def process_url_async(
|
async def process_url_async(
|
||||||
@@ -495,13 +542,14 @@ def create_app(): # noqa: C901
|
|||||||
conversion_request: ConvertDocumentsRequest,
|
conversion_request: ConvertDocumentsRequest,
|
||||||
):
|
):
|
||||||
task = await _enque_source(
|
task = await _enque_source(
|
||||||
orchestrator=orchestrator, conversion_request=conversion_request
|
orchestrator=orchestrator, request=conversion_request
|
||||||
)
|
)
|
||||||
task_queue_position = await orchestrator.get_queue_position(
|
task_queue_position = await orchestrator.get_queue_position(
|
||||||
task_id=task.task_id
|
task_id=task.task_id
|
||||||
)
|
)
|
||||||
return TaskStatusResponse(
|
return TaskStatusResponse(
|
||||||
task_id=task.task_id,
|
task_id=task.task_id,
|
||||||
|
task_type=task.task_type,
|
||||||
task_status=task.task_status,
|
task_status=task.task_status,
|
||||||
task_position=task_queue_position,
|
task_position=task_queue_position,
|
||||||
task_meta=task.processing_meta,
|
task_meta=task.processing_meta,
|
||||||
@@ -510,6 +558,7 @@ def create_app(): # noqa: C901
|
|||||||
# Convert a document from file(s) using the async api
|
# Convert a document from file(s) using the async api
|
||||||
@app.post(
|
@app.post(
|
||||||
"/v1/convert/file/async",
|
"/v1/convert/file/async",
|
||||||
|
tags=["convert"],
|
||||||
response_model=TaskStatusResponse,
|
response_model=TaskStatusResponse,
|
||||||
)
|
)
|
||||||
async def process_file_async(
|
async def process_file_async(
|
||||||
@@ -524,21 +573,249 @@ def create_app(): # noqa: C901
|
|||||||
):
|
):
|
||||||
target = InBodyTarget() if target_type == TargetName.INBODY else ZipTarget()
|
target = InBodyTarget() if target_type == TargetName.INBODY else ZipTarget()
|
||||||
task = await _enque_file(
|
task = await _enque_file(
|
||||||
orchestrator=orchestrator, files=files, options=options, target=target
|
task_type=TaskType.CONVERT,
|
||||||
|
orchestrator=orchestrator,
|
||||||
|
files=files,
|
||||||
|
convert_options=options,
|
||||||
|
chunking_options=None,
|
||||||
|
chunking_export_options=None,
|
||||||
|
target=target,
|
||||||
)
|
)
|
||||||
task_queue_position = await orchestrator.get_queue_position(
|
task_queue_position = await orchestrator.get_queue_position(
|
||||||
task_id=task.task_id
|
task_id=task.task_id
|
||||||
)
|
)
|
||||||
return TaskStatusResponse(
|
return TaskStatusResponse(
|
||||||
task_id=task.task_id,
|
task_id=task.task_id,
|
||||||
|
task_type=task.task_type,
|
||||||
task_status=task.task_status,
|
task_status=task.task_status,
|
||||||
task_position=task_queue_position,
|
task_position=task_queue_position,
|
||||||
task_meta=task.processing_meta,
|
task_meta=task.processing_meta,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Chunking endpoints
|
||||||
|
for display_name, path_name, opt_cls in (
|
||||||
|
("HybridChunker", "hybrid", HybridChunkerOptions),
|
||||||
|
("HierarchicalChunker", "hierarchical", HierarchicalChunkerOptions),
|
||||||
|
):
|
||||||
|
req_cls = make_request_model(opt_cls)
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
f"/v1/chunk/{path_name}/source/async",
|
||||||
|
name=f"Chunk sources with {display_name} as async task",
|
||||||
|
tags=["chunk"],
|
||||||
|
response_model=TaskStatusResponse,
|
||||||
|
)
|
||||||
|
async def chunk_source_async(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
auth: Annotated[AuthenticationResult, Depends(require_auth)],
|
||||||
|
orchestrator: Annotated[BaseOrchestrator, Depends(get_async_orchestrator)],
|
||||||
|
request: req_cls,
|
||||||
|
):
|
||||||
|
task = await _enque_source(orchestrator=orchestrator, request=request)
|
||||||
|
task_queue_position = await orchestrator.get_queue_position(
|
||||||
|
task_id=task.task_id
|
||||||
|
)
|
||||||
|
return TaskStatusResponse(
|
||||||
|
task_id=task.task_id,
|
||||||
|
task_type=task.task_type,
|
||||||
|
task_status=task.task_status,
|
||||||
|
task_position=task_queue_position,
|
||||||
|
task_meta=task.processing_meta,
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
f"/v1/chunk/{path_name}/file/async",
|
||||||
|
name=f"Chunk files with {display_name} as async task",
|
||||||
|
tags=["chunk"],
|
||||||
|
response_model=TaskStatusResponse,
|
||||||
|
)
|
||||||
|
async def chunk_file_async(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
auth: Annotated[AuthenticationResult, Depends(require_auth)],
|
||||||
|
orchestrator: Annotated[BaseOrchestrator, Depends(get_async_orchestrator)],
|
||||||
|
files: list[UploadFile],
|
||||||
|
convert_options: Annotated[
|
||||||
|
ConvertDocumentsRequestOptions,
|
||||||
|
FormDepends(
|
||||||
|
ConvertDocumentsRequestOptions,
|
||||||
|
prefix="convert_",
|
||||||
|
excluded_fields=[
|
||||||
|
"to_formats",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
chunking_options: Annotated[
|
||||||
|
opt_cls,
|
||||||
|
FormDepends(
|
||||||
|
HybridChunkerOptions,
|
||||||
|
prefix="chunking_",
|
||||||
|
excluded_fields=["chunker"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
include_converted_doc: Annotated[
|
||||||
|
bool,
|
||||||
|
Form(
|
||||||
|
description="If true, the output will include both the chunks and the converted document."
|
||||||
|
),
|
||||||
|
] = False,
|
||||||
|
target_type: Annotated[
|
||||||
|
TargetName,
|
||||||
|
Form(description="Specification for the type of output target."),
|
||||||
|
] = TargetName.INBODY,
|
||||||
|
):
|
||||||
|
target = InBodyTarget() if target_type == TargetName.INBODY else ZipTarget()
|
||||||
|
task = await _enque_file(
|
||||||
|
task_type=TaskType.CHUNK,
|
||||||
|
orchestrator=orchestrator,
|
||||||
|
files=files,
|
||||||
|
convert_options=convert_options,
|
||||||
|
chunking_options=chunking_options,
|
||||||
|
chunking_export_options=ChunkingExportOptions(
|
||||||
|
include_converted_doc=include_converted_doc
|
||||||
|
),
|
||||||
|
target=target,
|
||||||
|
)
|
||||||
|
task_queue_position = await orchestrator.get_queue_position(
|
||||||
|
task_id=task.task_id
|
||||||
|
)
|
||||||
|
return TaskStatusResponse(
|
||||||
|
task_id=task.task_id,
|
||||||
|
task_type=task.task_type,
|
||||||
|
task_status=task.task_status,
|
||||||
|
task_position=task_queue_position,
|
||||||
|
task_meta=task.processing_meta,
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
f"/v1/chunk/{path_name}/source",
|
||||||
|
name=f"Chunk sources with {display_name}",
|
||||||
|
tags=["chunk"],
|
||||||
|
response_model=ChunkDocumentResponse,
|
||||||
|
responses={
|
||||||
|
200: {
|
||||||
|
"content": {"application/zip": {}},
|
||||||
|
# "description": "Return the JSON item or an image.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def chunk_source(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
auth: Annotated[AuthenticationResult, Depends(require_auth)],
|
||||||
|
orchestrator: Annotated[BaseOrchestrator, Depends(get_async_orchestrator)],
|
||||||
|
request: req_cls,
|
||||||
|
):
|
||||||
|
task = await _enque_source(orchestrator=orchestrator, request=request)
|
||||||
|
completed = await _wait_task_complete(
|
||||||
|
orchestrator=orchestrator, task_id=task.task_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not completed:
|
||||||
|
# TODO: abort task!
|
||||||
|
return HTTPException(
|
||||||
|
status_code=504,
|
||||||
|
detail=f"Conversion is taking too long. The maximum wait time is configure as DOCLING_SERVE_MAX_SYNC_WAIT={docling_serve_settings.max_sync_wait}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
task_result = await orchestrator.task_result(task_id=task.task_id)
|
||||||
|
if task_result is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Task result not found. Please wait for a completion status.",
|
||||||
|
)
|
||||||
|
response = await prepare_response(
|
||||||
|
task_id=task.task_id,
|
||||||
|
task_result=task_result,
|
||||||
|
orchestrator=orchestrator,
|
||||||
|
background_tasks=background_tasks,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
f"/v1/chunk/{path_name}/file",
|
||||||
|
name=f"Chunk files with {display_name}",
|
||||||
|
tags=["chunk"],
|
||||||
|
response_model=ChunkDocumentResponse,
|
||||||
|
responses={
|
||||||
|
200: {
|
||||||
|
"content": {"application/zip": {}},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def chunk_file(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
auth: Annotated[AuthenticationResult, Depends(require_auth)],
|
||||||
|
orchestrator: Annotated[BaseOrchestrator, Depends(get_async_orchestrator)],
|
||||||
|
files: list[UploadFile],
|
||||||
|
convert_options: Annotated[
|
||||||
|
ConvertDocumentsRequestOptions,
|
||||||
|
FormDepends(
|
||||||
|
ConvertDocumentsRequestOptions,
|
||||||
|
prefix="convert_",
|
||||||
|
excluded_fields=[
|
||||||
|
"to_formats",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
chunking_options: Annotated[
|
||||||
|
opt_cls,
|
||||||
|
FormDepends(
|
||||||
|
HybridChunkerOptions,
|
||||||
|
prefix="chunking_",
|
||||||
|
excluded_fields=["chunker"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
include_converted_doc: Annotated[
|
||||||
|
bool,
|
||||||
|
Form(
|
||||||
|
description="If true, the output will include both the chunks and the converted document."
|
||||||
|
),
|
||||||
|
] = False,
|
||||||
|
target_type: Annotated[
|
||||||
|
TargetName,
|
||||||
|
Form(description="Specification for the type of output target."),
|
||||||
|
] = TargetName.INBODY,
|
||||||
|
):
|
||||||
|
target = InBodyTarget() if target_type == TargetName.INBODY else ZipTarget()
|
||||||
|
task = await _enque_file(
|
||||||
|
task_type=TaskType.CHUNK,
|
||||||
|
orchestrator=orchestrator,
|
||||||
|
files=files,
|
||||||
|
convert_options=convert_options,
|
||||||
|
chunking_options=chunking_options,
|
||||||
|
chunking_export_options=ChunkingExportOptions(
|
||||||
|
include_converted_doc=include_converted_doc
|
||||||
|
),
|
||||||
|
target=target,
|
||||||
|
)
|
||||||
|
completed = await _wait_task_complete(
|
||||||
|
orchestrator=orchestrator, task_id=task.task_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not completed:
|
||||||
|
# TODO: abort task!
|
||||||
|
return HTTPException(
|
||||||
|
status_code=504,
|
||||||
|
detail=f"Conversion is taking too long. The maximum wait time is configure as DOCLING_SERVE_MAX_SYNC_WAIT={docling_serve_settings.max_sync_wait}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
task_result = await orchestrator.task_result(task_id=task.task_id)
|
||||||
|
if task_result is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Task result not found. Please wait for a completion status.",
|
||||||
|
)
|
||||||
|
response = await prepare_response(
|
||||||
|
task_id=task.task_id,
|
||||||
|
task_result=task_result,
|
||||||
|
orchestrator=orchestrator,
|
||||||
|
background_tasks=background_tasks,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
# Task status poll
|
# Task status poll
|
||||||
@app.get(
|
@app.get(
|
||||||
"/v1/status/poll/{task_id}",
|
"/v1/status/poll/{task_id}",
|
||||||
|
tags=["tasks"],
|
||||||
response_model=TaskStatusResponse,
|
response_model=TaskStatusResponse,
|
||||||
)
|
)
|
||||||
async def task_status_poll(
|
async def task_status_poll(
|
||||||
@@ -557,6 +834,7 @@ def create_app(): # noqa: C901
|
|||||||
raise HTTPException(status_code=404, detail="Task not found.")
|
raise HTTPException(status_code=404, detail="Task not found.")
|
||||||
return TaskStatusResponse(
|
return TaskStatusResponse(
|
||||||
task_id=task.task_id,
|
task_id=task.task_id,
|
||||||
|
task_type=task.task_type,
|
||||||
task_status=task.task_status,
|
task_status=task.task_status,
|
||||||
task_position=task_queue_position,
|
task_position=task_queue_position,
|
||||||
task_meta=task.processing_meta,
|
task_meta=task.processing_meta,
|
||||||
@@ -600,6 +878,7 @@ def create_app(): # noqa: C901
|
|||||||
task_queue_position = await orchestrator.get_queue_position(task_id=task_id)
|
task_queue_position = await orchestrator.get_queue_position(task_id=task_id)
|
||||||
task_response = TaskStatusResponse(
|
task_response = TaskStatusResponse(
|
||||||
task_id=task.task_id,
|
task_id=task.task_id,
|
||||||
|
task_type=task.task_type,
|
||||||
task_status=task.task_status,
|
task_status=task.task_status,
|
||||||
task_position=task_queue_position,
|
task_position=task_queue_position,
|
||||||
task_meta=task.processing_meta,
|
task_meta=task.processing_meta,
|
||||||
@@ -615,6 +894,7 @@ def create_app(): # noqa: C901
|
|||||||
)
|
)
|
||||||
task_response = TaskStatusResponse(
|
task_response = TaskStatusResponse(
|
||||||
task_id=task.task_id,
|
task_id=task.task_id,
|
||||||
|
task_type=task.task_type,
|
||||||
task_status=task.task_status,
|
task_status=task.task_status,
|
||||||
task_position=task_queue_position,
|
task_position=task_queue_position,
|
||||||
task_meta=task.processing_meta,
|
task_meta=task.processing_meta,
|
||||||
@@ -637,7 +917,10 @@ def create_app(): # noqa: C901
|
|||||||
# Task result
|
# Task result
|
||||||
@app.get(
|
@app.get(
|
||||||
"/v1/result/{task_id}",
|
"/v1/result/{task_id}",
|
||||||
response_model=ConvertDocumentResponse | PresignedUrlConvertDocumentResponse,
|
tags=["tasks"],
|
||||||
|
response_model=ConvertDocumentResponse
|
||||||
|
| PresignedUrlConvertDocumentResponse
|
||||||
|
| ChunkDocumentResponse,
|
||||||
responses={
|
responses={
|
||||||
200: {
|
200: {
|
||||||
"content": {"application/zip": {}},
|
"content": {"application/zip": {}},
|
||||||
@@ -670,6 +953,8 @@ def create_app(): # noqa: C901
|
|||||||
# Update task progress
|
# Update task progress
|
||||||
@app.post(
|
@app.post(
|
||||||
"/v1/callback/task/progress",
|
"/v1/callback/task/progress",
|
||||||
|
tags=["internal"],
|
||||||
|
include_in_schema=False,
|
||||||
response_model=ProgressCallbackResponse,
|
response_model=ProgressCallbackResponse,
|
||||||
)
|
)
|
||||||
async def callback_task_progress(
|
async def callback_task_progress(
|
||||||
@@ -692,6 +977,7 @@ def create_app(): # noqa: C901
|
|||||||
# Offload models
|
# Offload models
|
||||||
@app.get(
|
@app.get(
|
||||||
"/v1/clear/converters",
|
"/v1/clear/converters",
|
||||||
|
tags=["clear"],
|
||||||
response_model=ClearResponse,
|
response_model=ClearResponse,
|
||||||
)
|
)
|
||||||
async def clear_converters(
|
async def clear_converters(
|
||||||
@@ -704,6 +990,7 @@ def create_app(): # noqa: C901
|
|||||||
# Clean results
|
# Clean results
|
||||||
@app.get(
|
@app.get(
|
||||||
"/v1/clear/results",
|
"/v1/clear/results",
|
||||||
|
tags=["clear"],
|
||||||
response_model=ClearResponse,
|
response_model=ClearResponse,
|
||||||
)
|
)
|
||||||
async def clear_results(
|
async def clear_results(
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import enum
|
import enum
|
||||||
from typing import Annotated, Literal
|
from functools import cache
|
||||||
|
from typing import Annotated, Generic, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field, model_validator
|
||||||
from pydantic_core import PydanticCustomError
|
from pydantic_core import PydanticCustomError
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self, TypeVar
|
||||||
|
|
||||||
|
from docling_jobkit.datamodel.chunking import (
|
||||||
|
BaseChunkerOptions,
|
||||||
|
)
|
||||||
from docling_jobkit.datamodel.http_inputs import FileSource, HttpSource
|
from docling_jobkit.datamodel.http_inputs import FileSource, HttpSource
|
||||||
from docling_jobkit.datamodel.s3_coords import S3Coordinates
|
from docling_jobkit.datamodel.s3_coords import S3Coordinates
|
||||||
from docling_jobkit.datamodel.task_targets import (
|
from docling_jobkit.datamodel.task_targets import (
|
||||||
@@ -70,3 +74,52 @@ class ConvertDocumentsRequest(BaseModel):
|
|||||||
"error target", 'target kind "s3" requires source kind "s3"'
|
"error target", 'target kind "s3" requires source kind "s3"'
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
## Source chunking requests
|
||||||
|
|
||||||
|
|
||||||
|
class BaseChunkDocumentsRequest(BaseModel):
|
||||||
|
convert_options: Annotated[
|
||||||
|
ConvertDocumentsRequestOptions, Field(description="Conversion options.")
|
||||||
|
] = ConvertDocumentsRequestOptions()
|
||||||
|
sources: Annotated[
|
||||||
|
list[SourceRequestItem],
|
||||||
|
Field(description="List of input document sources to process."),
|
||||||
|
]
|
||||||
|
include_converted_doc: Annotated[
|
||||||
|
bool,
|
||||||
|
Field(
|
||||||
|
description="If true, the output will include both the chunks and the converted document."
|
||||||
|
),
|
||||||
|
] = False
|
||||||
|
target: Annotated[
|
||||||
|
TaskTarget, Field(description="Specification for the type of output target.")
|
||||||
|
] = InBodyTarget()
|
||||||
|
|
||||||
|
|
||||||
|
ChunkingOptT = TypeVar("ChunkingOptT", bound=BaseChunkerOptions)
|
||||||
|
|
||||||
|
|
||||||
|
class GenericChunkDocumentsRequest(BaseChunkDocumentsRequest, Generic[ChunkingOptT]):
|
||||||
|
chunking_options: ChunkingOptT
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def make_request_model(
|
||||||
|
opt_type: type[ChunkingOptT],
|
||||||
|
) -> type[GenericChunkDocumentsRequest[ChunkingOptT]]:
|
||||||
|
"""
|
||||||
|
Dynamically create (and cache) a subclass of GenericChunkDocumentsRequest[opt_type]
|
||||||
|
with chunking_options having a default factory.
|
||||||
|
"""
|
||||||
|
return type(
|
||||||
|
f"{opt_type.__name__}DocumentsRequest",
|
||||||
|
(GenericChunkDocumentsRequest[opt_type],), # type: ignore[valid-type]
|
||||||
|
{
|
||||||
|
"__annotations__": {"chunking_options": opt_type},
|
||||||
|
"chunking_options": Field(
|
||||||
|
default_factory=opt_type, description="Options specific to the chunker."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from docling.datamodel.document import ConversionStatus, ErrorItem
|
from docling.datamodel.document import ConversionStatus, ErrorItem
|
||||||
from docling.utils.profiling import ProfilingItem
|
from docling.utils.profiling import ProfilingItem
|
||||||
from docling_jobkit.datamodel.result import ExportDocumentResponse
|
from docling_jobkit.datamodel.result import (
|
||||||
from docling_jobkit.datamodel.task_meta import TaskProcessingMeta
|
ChunkedDocumentResultItem,
|
||||||
|
ExportDocumentResponse,
|
||||||
|
ExportResult,
|
||||||
|
)
|
||||||
|
from docling_jobkit.datamodel.task_meta import TaskProcessingMeta, TaskType
|
||||||
|
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
@@ -37,8 +41,15 @@ class ConvertDocumentErrorResponse(BaseModel):
|
|||||||
status: ConversionStatus
|
status: ConversionStatus
|
||||||
|
|
||||||
|
|
||||||
|
class ChunkDocumentResponse(BaseModel):
|
||||||
|
chunks: list[ChunkedDocumentResultItem]
|
||||||
|
documents: list[ExportResult]
|
||||||
|
processing_time: float
|
||||||
|
|
||||||
|
|
||||||
class TaskStatusResponse(BaseModel):
|
class TaskStatusResponse(BaseModel):
|
||||||
task_id: str
|
task_id: str
|
||||||
|
task_type: TaskType
|
||||||
task_status: str
|
task_status: str
|
||||||
task_position: Optional[int] = None
|
task_position: Optional[int] = None
|
||||||
task_meta: Optional[TaskProcessingMeta] = None
|
task_meta: Optional[TaskProcessingMeta] = None
|
||||||
|
|||||||
@@ -29,10 +29,15 @@ def is_pydantic_model(type_):
|
|||||||
|
|
||||||
# Adapted from
|
# Adapted from
|
||||||
# https://github.com/fastapi/fastapi/discussions/8971#discussioncomment-7892972
|
# https://github.com/fastapi/fastapi/discussions/8971#discussioncomment-7892972
|
||||||
def FormDepends(cls: type[BaseModel]):
|
def FormDepends(
|
||||||
|
cls: type[BaseModel], prefix: str = "", excluded_fields: list[str] = []
|
||||||
|
):
|
||||||
new_parameters = []
|
new_parameters = []
|
||||||
|
|
||||||
for field_name, model_field in cls.model_fields.items():
|
for field_name, model_field in cls.model_fields.items():
|
||||||
|
if field_name in excluded_fields:
|
||||||
|
continue
|
||||||
|
|
||||||
annotation = model_field.annotation
|
annotation = model_field.annotation
|
||||||
description = model_field.description
|
description = model_field.description
|
||||||
default = (
|
default = (
|
||||||
@@ -63,7 +68,7 @@ def FormDepends(cls: type[BaseModel]):
|
|||||||
|
|
||||||
new_parameters.append(
|
new_parameters.append(
|
||||||
inspect.Parameter(
|
inspect.Parameter(
|
||||||
name=field_name,
|
name=f"{prefix}{field_name}",
|
||||||
kind=inspect.Parameter.POSITIONAL_ONLY,
|
kind=inspect.Parameter.POSITIONAL_ONLY,
|
||||||
default=default,
|
default=default,
|
||||||
annotation=annotation,
|
annotation=annotation,
|
||||||
@@ -71,19 +76,23 @@ def FormDepends(cls: type[BaseModel]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def as_form_func(**data):
|
async def as_form_func(**data):
|
||||||
|
newdata = {}
|
||||||
for field_name, model_field in cls.model_fields.items():
|
for field_name, model_field in cls.model_fields.items():
|
||||||
value = data.get(field_name)
|
if field_name in excluded_fields:
|
||||||
|
continue
|
||||||
|
value = data.get(f"{prefix}{field_name}")
|
||||||
|
newdata[field_name] = value
|
||||||
annotation = model_field.annotation
|
annotation = model_field.annotation
|
||||||
|
|
||||||
# Parse nested models from JSON string
|
# Parse nested models from JSON string
|
||||||
if value is not None and is_pydantic_model(annotation):
|
if value is not None and is_pydantic_model(annotation):
|
||||||
try:
|
try:
|
||||||
validator = TypeAdapter(annotation)
|
validator = TypeAdapter(annotation)
|
||||||
data[field_name] = validator.validate_json(value)
|
newdata[field_name] = validator.validate_json(value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Invalid JSON for field '{field_name}': {e}")
|
raise ValueError(f"Invalid JSON for field '{field_name}': {e}")
|
||||||
|
|
||||||
return cls(**data)
|
return cls(**newdata)
|
||||||
|
|
||||||
sig = inspect.signature(as_form_func)
|
sig = inspect.signature(as_form_func)
|
||||||
sig = sig.replace(parameters=new_parameters)
|
sig = sig.replace(parameters=new_parameters)
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import logging
|
|||||||
from fastapi import BackgroundTasks, Response
|
from fastapi import BackgroundTasks, Response
|
||||||
|
|
||||||
from docling_jobkit.datamodel.result import (
|
from docling_jobkit.datamodel.result import (
|
||||||
ConvertDocumentResult,
|
ChunkedDocumentResult,
|
||||||
|
DoclingTaskResult,
|
||||||
ExportResult,
|
ExportResult,
|
||||||
RemoteTargetResult,
|
RemoteTargetResult,
|
||||||
ZipArchiveResult,
|
ZipArchiveResult,
|
||||||
@@ -14,6 +15,7 @@ from docling_jobkit.orchestrators.base_orchestrator import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from docling_serve.datamodel.responses import (
|
from docling_serve.datamodel.responses import (
|
||||||
|
ChunkDocumentResponse,
|
||||||
ConvertDocumentResponse,
|
ConvertDocumentResponse,
|
||||||
PresignedUrlConvertDocumentResponse,
|
PresignedUrlConvertDocumentResponse,
|
||||||
)
|
)
|
||||||
@@ -24,11 +26,16 @@ _log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
async def prepare_response(
|
async def prepare_response(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
task_result: ConvertDocumentResult,
|
task_result: DoclingTaskResult,
|
||||||
orchestrator: BaseOrchestrator,
|
orchestrator: BaseOrchestrator,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
):
|
):
|
||||||
response: Response | ConvertDocumentResponse | PresignedUrlConvertDocumentResponse
|
response: (
|
||||||
|
Response
|
||||||
|
| ConvertDocumentResponse
|
||||||
|
| PresignedUrlConvertDocumentResponse
|
||||||
|
| ChunkDocumentResponse
|
||||||
|
)
|
||||||
if isinstance(task_result.result, ExportResult):
|
if isinstance(task_result.result, ExportResult):
|
||||||
response = ConvertDocumentResponse(
|
response = ConvertDocumentResponse(
|
||||||
document=task_result.result.content,
|
document=task_result.result.content,
|
||||||
@@ -52,6 +59,12 @@ async def prepare_response(
|
|||||||
num_succeeded=task_result.num_succeeded,
|
num_succeeded=task_result.num_succeeded,
|
||||||
num_failed=task_result.num_failed,
|
num_failed=task_result.num_failed,
|
||||||
)
|
)
|
||||||
|
elif isinstance(task_result.result, ChunkedDocumentResult):
|
||||||
|
response = ChunkDocumentResponse(
|
||||||
|
chunks=task_result.result.chunks,
|
||||||
|
documents=task_result.result.documents,
|
||||||
|
processing_time=task_result.processing_time,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown result type")
|
raise ValueError("Unknown result type")
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class WebsocketNotifier(BaseNotifier):
|
|||||||
task_queue_position = await self.orchestrator.get_queue_position(task_id)
|
task_queue_position = await self.orchestrator.get_queue_position(task_id)
|
||||||
msg = TaskStatusResponse(
|
msg = TaskStatusResponse(
|
||||||
task_id=task.task_id,
|
task_id=task.task_id,
|
||||||
|
task_type=task.task_type,
|
||||||
task_status=task.task_status,
|
task_status=task.task_status,
|
||||||
task_position=task_queue_position,
|
task_position=task_queue_position,
|
||||||
task_meta=task.processing_meta,
|
task_meta=task.processing_meta,
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ This documentation pages explore the webserver configurations, runtime options,
|
|||||||
- [Handling models](./models.md)
|
- [Handling models](./models.md)
|
||||||
- [Usage](./usage.md)
|
- [Usage](./usage.md)
|
||||||
- [Deployment](./deployment.md)
|
- [Deployment](./deployment.md)
|
||||||
|
- [MCP](./mcp.md)
|
||||||
- [Development](./development.md)
|
- [Development](./development.md)
|
||||||
- [`v1` migration](./v1_migration.md)
|
- [`v1` migration](./v1_migration.md)
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ THe following table describes the options to configure the Docling Serve app.
|
|||||||
| | `DOCLING_SERVE_SINGLE_USE_RESULTS` | `true` | If true, results can be accessed only once. If false, the results accumulate in the scratch directory. |
|
| | `DOCLING_SERVE_SINGLE_USE_RESULTS` | `true` | If true, results can be accessed only once. If false, the results accumulate in the scratch directory. |
|
||||||
| | `DOCLING_SERVE_RESULT_REMOVAL_DELAY` | `300` | When `DOCLING_SERVE_SINGLE_USE_RESULTS` is active, this is the delay before results are removed from the task registry. |
|
| | `DOCLING_SERVE_RESULT_REMOVAL_DELAY` | `300` | When `DOCLING_SERVE_SINGLE_USE_RESULTS` is active, this is the delay before results are removed from the task registry. |
|
||||||
| | `DOCLING_SERVE_MAX_DOCUMENT_TIMEOUT` | `604800` (7 days) | The maximum time for processing a document. |
|
| | `DOCLING_SERVE_MAX_DOCUMENT_TIMEOUT` | `604800` (7 days) | The maximum time for processing a document. |
|
||||||
|
| | `DOCLING_NUM_THREADS` | `4` | Number of concurrent threads for processing a document. |
|
||||||
| | `DOCLING_SERVE_MAX_NUM_PAGES` | | The maximum number of pages for a document to be processed. |
|
| | `DOCLING_SERVE_MAX_NUM_PAGES` | | The maximum number of pages for a document to be processed. |
|
||||||
| | `DOCLING_SERVE_MAX_FILE_SIZE` | | The maximum file size for a document to be processed. |
|
| | `DOCLING_SERVE_MAX_FILE_SIZE` | | The maximum file size for a document to be processed. |
|
||||||
| | `DOCLING_SERVE_MAX_SYNC_WAIT` | `120` | Max number of seconds a synchronous endpoint is waiting for the task completion. |
|
| | `DOCLING_SERVE_MAX_SYNC_WAIT` | `120` | Max number of seconds a synchronous endpoint is waiting for the task completion. |
|
||||||
@@ -77,7 +78,7 @@ The following table describes the options to configure the Docling Serve RQ engi
|
|||||||
|-----|---------|-------------|
|
|-----|---------|-------------|
|
||||||
| `DOCLING_SERVE_ENG_RQ_REDIS_URL` | (required) | The connection Redis url, e.g. `redis://localhost:6373/` |
|
| `DOCLING_SERVE_ENG_RQ_REDIS_URL` | (required) | The connection Redis url, e.g. `redis://localhost:6373/` |
|
||||||
| `DOCLING_SERVE_ENG_RQ_RESULTS_PREFIX` | `docling:results` | The prefix used for storing the results in Redis. |
|
| `DOCLING_SERVE_ENG_RQ_RESULTS_PREFIX` | `docling:results` | The prefix used for storing the results in Redis. |
|
||||||
| `DOCLING_SERVE_ENG_RQ_RESULTS_PREFIX` | `docling:updates` | The channel key name used for storing communicating updates between the workers and the orchestrator. |
|
| `DOCLING_SERVE_ENG_RQ_SUB_CHANNEL` | `docling:updates` | The channel key name used for storing communicating updates between the workers and the orchestrator. |
|
||||||
|
|
||||||
#### KFP engine
|
#### KFP engine
|
||||||
|
|
||||||
|
|||||||
22
docs/examples.md
Normal file
22
docs/examples.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Examples
|
||||||
|
|
||||||
|
## Split processing
|
||||||
|
|
||||||
|
The example of provided of split processing demonstrates how to split a PDF into chunks of pages and send them for conversion. At the end, it concatenates all split pages into a single conversion `JSON`.
|
||||||
|
|
||||||
|
At beginning of file there's variables to be used (and modified) such as:
|
||||||
|
| Variable | Description |
|
||||||
|
| ---------|-------------|
|
||||||
|
| `path_to_pdf`| Path to PDF file to be split |
|
||||||
|
| `pages_per_file`| The number of pages per chunk to split PDF |
|
||||||
|
| `base_url`| Base url of the `docling-serve` host |
|
||||||
|
| `out_dir`| The output folder of each conversion `JSON` of split PDF and the final concatenated `JSON` |
|
||||||
|
|
||||||
|
The example follows the following logic:
|
||||||
|
- Get the number of pages of the `PDF`
|
||||||
|
- Based on the number of chunks of pages, send each chunk to conversion using `page_range` parameter
|
||||||
|
- Wait all conversions to finish
|
||||||
|
- Get all conversion results
|
||||||
|
- Save each conversion `JSON` result into a `JSON` file
|
||||||
|
- Concatenate all `JSONs` into a single `JSON` using `docling` concatenate method
|
||||||
|
- Save concatenated `JSON` into a `JSON` file
|
||||||
39
docs/mcp.md
Normal file
39
docs/mcp.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Docling MCP in Docling Serve
|
||||||
|
|
||||||
|
The `docling-serve` container image includes all MCP (Model Communication Protocol) features starting from version v1.1.0. To leverage these features, you simply need to use a different entrypoint—no custom image builds or additional installations are required. The image provides the `docling-mcp-server` executable, which enables MCP functionality out of the box as of version v1.1.0 ([changelog](https://github.com/docling-project/docling-serve/blob/624f65d41b734e8b39ff267bc8bf6e766c376d6d/CHANGELOG.md)).
|
||||||
|
|
||||||
|
Read more on [Docling MCP](https://github.com/docling-project/docling-mcp) in its dedicated repository.
|
||||||
|
|
||||||
|
## Launching the MCP Service
|
||||||
|
|
||||||
|
By default, the container runs `docling-serve run` and exposes port 5001. To start the MCP service, override the entrypoint and specify your desired port mapping. For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
podman run -p 8000:8000 quay.io/docling-project/docling-serve -- docling-mcp-server --transport streamable-http --port 8000 --host 0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
This command starts the MCP server on port 8000, accessible at `http://localhost:8000/mcp`. Adjust the port and host as needed. Key arguments for `docling-mcp-server` include `--transport streamable-http` (HTTP transport for client connections), `--port <PORT>`, and `--host <HOST>` (use `0.0.0.0` to accept connections from any interface).
|
||||||
|
|
||||||
|
## Configuring MCP Clients
|
||||||
|
|
||||||
|
Most MCP-compatible clients, such as LM Studio and Claude Desktop, allow you to specify custom MCP server endpoints. The standard configuration uses a JSON block to define available MCP servers. For example, to connect to the Docling MCP server running on port 8000:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"docling": {
|
||||||
|
"url": "http://localhost:8000/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Insert this configuration in your client's settings where MCP servers are defined. Update the URL if you use a different port.
|
||||||
|
|
||||||
|
### LM Studio and Claude Desktop
|
||||||
|
|
||||||
|
Both LM Studio and Claude Desktop support MCP endpoints via configuration files or UI settings. Paste the above JSON block into the appropriate configuration section. For Claude Desktop, add the MCP server in the "Custom Model" or "MCP Server" section. For LM Studio, refer to its documentation for the location of the MCP server configuration.
|
||||||
|
|
||||||
|
### Other MCP Clients
|
||||||
|
|
||||||
|
Other clients, such as Continue Coding Assistant, also support custom MCP endpoints. Use the same configuration pattern: provide the MCP server URL ending with `/mcp` and ensure the port matches your container setup. See the [Docling MCP docs](https://github.com/docling-project/docling-mcp/tree/main/docs/integrations) for more details.
|
||||||
124
examples/split_processing.py
Normal file
124
examples/split_processing.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pypdf import PdfReader
|
||||||
|
|
||||||
|
from docling_core.types.doc.document import DoclingDocument
|
||||||
|
|
||||||
|
# Variables to use
|
||||||
|
path_to_pdf = Path("./tests/2206.01062v1.pdf")
|
||||||
|
pages_per_file = 4
|
||||||
|
base_url = "http://localhost:5001/v1"
|
||||||
|
out_dir = Path("examples/splitted_pdf/")
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertedSplittedPdf(BaseModel):
|
||||||
|
task_id: str
|
||||||
|
conversion_finished: bool = False
|
||||||
|
result: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_result(task_id: str):
|
||||||
|
response = httpx.get(
|
||||||
|
f"{base_url}/result/{task_id}",
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def check_task_status(task_id: str):
|
||||||
|
response = httpx.get(f"{base_url}/status/poll/{task_id}", timeout=15)
|
||||||
|
task = response.json()
|
||||||
|
task_status = task["task_status"]
|
||||||
|
|
||||||
|
task_finished = False
|
||||||
|
if task_status == "success":
|
||||||
|
task_finished = True
|
||||||
|
|
||||||
|
if task_status in ("failure", "revoked"):
|
||||||
|
raise RuntimeError("A conversion failed")
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
return task_finished
|
||||||
|
|
||||||
|
|
||||||
|
def post_file(file_path: Path, start_page: int, end_page: int):
|
||||||
|
payload = {
|
||||||
|
"to_formats": ["json"],
|
||||||
|
"image_export_mode": "placeholder",
|
||||||
|
"ocr": False,
|
||||||
|
"abort_on_error": False,
|
||||||
|
"page_range": [start_page, end_page],
|
||||||
|
}
|
||||||
|
|
||||||
|
files = {
|
||||||
|
"files": (file_path.name, file_path.open("rb"), "application/pdf"),
|
||||||
|
}
|
||||||
|
response = httpx.post(
|
||||||
|
f"{base_url}/convert/file/async",
|
||||||
|
files=files,
|
||||||
|
data=payload,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
task = response.json()
|
||||||
|
|
||||||
|
return task["task_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
filename = path_to_pdf
|
||||||
|
|
||||||
|
splitted_pdfs: list[ConvertedSplittedPdf] = []
|
||||||
|
|
||||||
|
with open(filename, "rb") as input_pdf_file:
|
||||||
|
pdf_reader = PdfReader(input_pdf_file)
|
||||||
|
total_pages = len(pdf_reader.pages)
|
||||||
|
|
||||||
|
for start_page in range(0, total_pages, pages_per_file):
|
||||||
|
task_id = post_file(
|
||||||
|
filename, start_page + 1, min(start_page + pages_per_file, total_pages)
|
||||||
|
)
|
||||||
|
splitted_pdfs.append(ConvertedSplittedPdf(task_id=task_id))
|
||||||
|
|
||||||
|
all_files_converted = False
|
||||||
|
while not all_files_converted:
|
||||||
|
found_conversion_running = False
|
||||||
|
for splitted_pdf in splitted_pdfs:
|
||||||
|
if not splitted_pdf.conversion_finished:
|
||||||
|
found_conversion_running = True
|
||||||
|
print("checking conversion status...")
|
||||||
|
splitted_pdf.conversion_finished = check_task_status(
|
||||||
|
splitted_pdf.task_id
|
||||||
|
)
|
||||||
|
if not found_conversion_running:
|
||||||
|
all_files_converted = True
|
||||||
|
|
||||||
|
for splitted_pdf in splitted_pdfs:
|
||||||
|
splitted_pdf.result = get_task_result(splitted_pdf.task_id)
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for i, splitted_pdf in enumerate(splitted_pdfs):
|
||||||
|
json_content = json.dumps(
|
||||||
|
splitted_pdf.result.get("document").get("json_content"), indent=2
|
||||||
|
)
|
||||||
|
doc = DoclingDocument.model_validate_json(json_content)
|
||||||
|
filename = f"{out_dir}/splited_json_{i}.json"
|
||||||
|
doc.save_as_json(filename=filename)
|
||||||
|
files.append(filename)
|
||||||
|
|
||||||
|
docs = [DoclingDocument.load_from_json(filename=f) for f in files]
|
||||||
|
concate_doc = DoclingDocument.concatenate(docs=docs)
|
||||||
|
|
||||||
|
exp_json_file = Path(f"{out_dir}/concatenated.json")
|
||||||
|
concate_doc.save_as_json(exp_json_file)
|
||||||
|
|
||||||
|
print("Finished")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "docling-serve"
|
name = "docling-serve"
|
||||||
version = "1.3.1" # DO NOT EDIT, updated automatically
|
version = "1.5.1" # DO NOT EDIT, updated automatically
|
||||||
description = "Running Docling as a service"
|
description = "Running Docling as a service"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
authors = [
|
authors = [
|
||||||
@@ -34,8 +34,8 @@ classifiers = [
|
|||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"docling~=2.38",
|
"docling~=2.38",
|
||||||
"docling-core>=2.44.1",
|
"docling-core>=2.45.0",
|
||||||
"docling-jobkit[kfp,rq,vlm]>=1.4.0,<2.0.0",
|
"docling-jobkit[kfp,rq,vlm]>=1.5.0,<2.0.0",
|
||||||
"fastapi[standard]~=0.115",
|
"fastapi[standard]~=0.115",
|
||||||
"httpx~=0.28",
|
"httpx~=0.28",
|
||||||
"pydantic~=2.10",
|
"pydantic~=2.10",
|
||||||
@@ -50,15 +50,15 @@ dependencies = [
|
|||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
ui = [
|
ui = [
|
||||||
"gradio~=5.9",
|
"gradio~=5.23.2",
|
||||||
"pydantic<2.11.0", # fix compatibility between gradio and new pydantic 2.11
|
|
||||||
]
|
]
|
||||||
tesserocr = [
|
tesserocr = [
|
||||||
"tesserocr~=2.7"
|
"tesserocr~=2.7"
|
||||||
]
|
]
|
||||||
rapidocr = [
|
rapidocr = [
|
||||||
"rapidocr-onnxruntime~=1.4; python_version<'3.13'",
|
"rapidocr (>=3.3,<4.0.0) ; python_version < '3.14'",
|
||||||
"onnxruntime~=1.7",
|
"onnxruntime (>=1.7.0,<2.0.0)",
|
||||||
|
"modelscope>=1.29.0",
|
||||||
]
|
]
|
||||||
flash-attn = [
|
flash-attn = [
|
||||||
"flash-attn~=2.8.2; sys_platform == 'linux' and platform_machine == 'x86_64'"
|
"flash-attn~=2.8.2; sys_platform == 'linux' and platform_machine == 'x86_64'"
|
||||||
@@ -69,6 +69,7 @@ dev = [
|
|||||||
"asgi-lifespan~=2.0",
|
"asgi-lifespan~=2.0",
|
||||||
"mypy~=1.11",
|
"mypy~=1.11",
|
||||||
"pre-commit-uv~=4.1",
|
"pre-commit-uv~=4.1",
|
||||||
|
"pypdf>=6.0.0",
|
||||||
"pytest~=8.3",
|
"pytest~=8.3",
|
||||||
"pytest-asyncio~=0.24",
|
"pytest-asyncio~=0.24",
|
||||||
"pytest-check~=2.4",
|
"pytest-check~=2.4",
|
||||||
@@ -86,25 +87,25 @@ cpu = [
|
|||||||
"torchvision>=0.22.1",
|
"torchvision>=0.22.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
cu124 = [
|
# cu124 = [
|
||||||
"torch>=2.6.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version < '3.13'",
|
# "torch>=2.6.0",
|
||||||
"torchvision>=0.21.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version < '3.13'",
|
# "torchvision>=0.21.0",
|
||||||
]
|
# ]
|
||||||
|
|
||||||
cu126 = [
|
cu126 = [
|
||||||
"torch>=2.7.1 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version < '3.13'",
|
"torch>=2.7.1",
|
||||||
"torchvision>=0.22.1 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version < '3.13'",
|
"torchvision>=0.22.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
cu128 = [
|
cu128 = [
|
||||||
"torch>=2.7.1 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version < '3.13'",
|
"torch>=2.7.1",
|
||||||
"torchvision>=0.22.1 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version < '3.13'",
|
"torchvision>=0.22.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
rocm = [
|
rocm = [
|
||||||
"torch>=2.7.1 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version < '3.13'",
|
"torch>=2.7.1",
|
||||||
"torchvision>=0.22.1 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version < '3.13'",
|
"torchvision>=0.22.1",
|
||||||
"pytorch-triton-rocm>=3.3.1 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version < '3.13'",
|
"pytorch-triton-rocm>=3.3.1 ; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
@@ -114,7 +115,7 @@ conflicts = [
|
|||||||
[
|
[
|
||||||
{ group = "pypi" },
|
{ group = "pypi" },
|
||||||
{ group = "cpu" },
|
{ group = "cpu" },
|
||||||
{ group = "cu124" },
|
# { group = "cu124" },
|
||||||
{ group = "cu126" },
|
{ group = "cu126" },
|
||||||
{ group = "cu128" },
|
{ group = "cu128" },
|
||||||
{ group = "rocm" },
|
{ group = "rocm" },
|
||||||
@@ -129,7 +130,7 @@ override-dependencies = [
|
|||||||
torch = [
|
torch = [
|
||||||
{ index = "pytorch-pypi", group = "pypi" },
|
{ index = "pytorch-pypi", group = "pypi" },
|
||||||
{ index = "pytorch-cpu", group = "cpu" },
|
{ index = "pytorch-cpu", group = "cpu" },
|
||||||
{ index = "pytorch-cu124", group = "cu124", marker = "sys_platform == 'linux'" },
|
# { index = "pytorch-cu124", group = "cu124", marker = "sys_platform == 'linux'" },
|
||||||
{ index = "pytorch-cu126", group = "cu126", marker = "sys_platform == 'linux'" },
|
{ index = "pytorch-cu126", group = "cu126", marker = "sys_platform == 'linux'" },
|
||||||
{ index = "pytorch-cu128", group = "cu128", marker = "sys_platform == 'linux'" },
|
{ index = "pytorch-cu128", group = "cu128", marker = "sys_platform == 'linux'" },
|
||||||
{ index = "pytorch-rocm", group = "rocm", marker = "sys_platform == 'linux'" },
|
{ index = "pytorch-rocm", group = "rocm", marker = "sys_platform == 'linux'" },
|
||||||
@@ -138,7 +139,7 @@ torch = [
|
|||||||
torchvision = [
|
torchvision = [
|
||||||
{ index = "pytorch-pypi", group = "pypi" },
|
{ index = "pytorch-pypi", group = "pypi" },
|
||||||
{ index = "pytorch-cpu", group = "cpu" },
|
{ index = "pytorch-cpu", group = "cpu" },
|
||||||
{ index = "pytorch-cu124", group = "cu124", marker = "sys_platform == 'linux'" },
|
# { index = "pytorch-cu124", group = "cu124", marker = "sys_platform == 'linux'" },
|
||||||
{ index = "pytorch-cu126", group = "cu126", marker = "sys_platform == 'linux'" },
|
{ index = "pytorch-cu126", group = "cu126", marker = "sys_platform == 'linux'" },
|
||||||
{ index = "pytorch-cu128", group = "cu128", marker = "sys_platform == 'linux'" },
|
{ index = "pytorch-cu128", group = "cu128", marker = "sys_platform == 'linux'" },
|
||||||
{ index = "pytorch-rocm", group = "rocm", marker = "sys_platform == 'linux'" },
|
{ index = "pytorch-rocm", group = "rocm", marker = "sys_platform == 'linux'" },
|
||||||
@@ -161,10 +162,10 @@ name = "pytorch-cpu"
|
|||||||
url = "https://download.pytorch.org/whl/cpu"
|
url = "https://download.pytorch.org/whl/cpu"
|
||||||
explicit = true
|
explicit = true
|
||||||
|
|
||||||
[[tool.uv.index]]
|
# [[tool.uv.index]]
|
||||||
name = "pytorch-cu124"
|
# name = "pytorch-cu124"
|
||||||
url = "https://download.pytorch.org/whl/cu124"
|
# url = "https://download.pytorch.org/whl/cu124"
|
||||||
explicit = true
|
# explicit = true
|
||||||
|
|
||||||
[[tool.uv.index]]
|
[[tool.uv.index]]
|
||||||
name = "pytorch-cu126"
|
name = "pytorch-cu126"
|
||||||
@@ -278,6 +279,7 @@ module = [
|
|||||||
"kfp.*",
|
"kfp.*",
|
||||||
"kfp_server_api.*",
|
"kfp_server_api.*",
|
||||||
"mlx_vlm.*",
|
"mlx_vlm.*",
|
||||||
|
"mlx.*",
|
||||||
"scalar_fastapi.*",
|
"scalar_fastapi.*",
|
||||||
]
|
]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|||||||
@@ -62,3 +62,60 @@ async def test_convert_url(async_client):
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
assert task["task_status"] == "success"
|
assert task["task_status"] == "success"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("include_converted_doc", [False, True])
|
||||||
|
async def test_chunk_url(async_client, include_converted_doc: bool):
|
||||||
|
"""Test chunk URL"""
|
||||||
|
|
||||||
|
example_docs = [
|
||||||
|
"https://arxiv.org/pdf/2311.18481",
|
||||||
|
]
|
||||||
|
|
||||||
|
base_url = "http://localhost:5001/v1"
|
||||||
|
payload = {
|
||||||
|
"sources": [{"kind": "http", "url": random.choice(example_docs)}],
|
||||||
|
"include_converted_doc": include_converted_doc,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await async_client.post(
|
||||||
|
f"{base_url}/chunk/hybrid/source/async", json=payload
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, "Response should be 200 OK"
|
||||||
|
|
||||||
|
task = response.json()
|
||||||
|
|
||||||
|
print(json.dumps(task, indent=2))
|
||||||
|
|
||||||
|
while task["task_status"] not in ("success", "failure"):
|
||||||
|
response = await async_client.get(f"{base_url}/status/poll/{task['task_id']}")
|
||||||
|
assert response.status_code == 200, "Response should be 200 OK"
|
||||||
|
task = response.json()
|
||||||
|
print(f"{task['task_status']=}")
|
||||||
|
print(f"{task['task_position']=}")
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
assert task["task_status"] == "success"
|
||||||
|
|
||||||
|
result_resp = await async_client.get(f"{base_url}/result/{task['task_id']}")
|
||||||
|
assert result_resp.status_code == 200, "Response should be 200 OK"
|
||||||
|
result = result_resp.json()
|
||||||
|
print("Got result.")
|
||||||
|
|
||||||
|
assert "chunks" in result
|
||||||
|
assert len(result["chunks"]) > 0
|
||||||
|
|
||||||
|
assert "documents" in result
|
||||||
|
assert len(result["documents"]) > 0
|
||||||
|
assert result["documents"][0]["status"] == "success"
|
||||||
|
|
||||||
|
if include_converted_doc:
|
||||||
|
assert result["documents"][0]["content"]["json_content"] is not None
|
||||||
|
assert (
|
||||||
|
result["documents"][0]["content"]["json_content"]["schema_name"]
|
||||||
|
== "DoclingDocument"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert result["documents"][0]["content"]["json_content"] is None
|
||||||
|
|||||||
Reference in New Issue
Block a user