commit 15dadb54324dcc5d7b80d437a4e57fefae0e078d Author: wehub-resource-sync Date: Mon Jul 13 13:38:09 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..83cb419 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +{ + "name": "LEANN Dev", + "build": { + "context": "..", + "dockerfile": "../docker/Dockerfile.dev", + "args": { + "PYTHON_VERSION": "3.12" + } + }, + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "remoteUser": "root", + "overrideCommand": true, + "postCreateCommand": "uv sync --group lint --group test", + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff", + "ms-azuretools.vscode-docker", + "ms-toolsai.jupyter", + "tamasfe.even-better-toml", + "eamodio.gitlens", + "EditorConfig.EditorConfig", + "DavidAnson.vscode-markdownlint" + ], + "settings": { + "python.defaultInterpreterPath": "/workspaces/${localWorkspaceFolderBasename}/.venv/bin/python", + "python.terminal.activateEnvironment": true, + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": [ + "tests" + ], + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports.ruff": "explicit", + "source.fixAll.ruff": "explicit" + } + } + } + } +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..b18bb62 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,50 @@ +name: Bug Report +description: Report a bug in LEANN +labels: ["bug"] + +body: + - type: textarea + id: description + attributes: + label: What happened? + description: A clear description of the bug + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: How to reproduce + placeholder: | + 1. Install with... + 2. Run command... + 3. See error + validations: + required: true + + - type: textarea + id: error + attributes: + label: Error message + description: Paste any error messages + render: shell + + - type: input + id: version + attributes: + label: LEANN Version + placeholder: "0.1.0" + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + options: + - macOS + - Linux + - Windows + - Docker + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ce0f898 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Documentation + url: https://github.com/LEANN-RAG/LEANN-RAG/tree/main/docs + about: Read the docs first + - name: Discussions + url: https://github.com/LEANN-RAG/LEANN-RAG/discussions + about: Ask questions and share ideas diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..0b01b1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,27 @@ +name: Feature Request +description: Suggest a new feature for LEANN +labels: ["enhancement"] + +body: + - type: textarea + id: problem + attributes: + label: What problem does this solve? + description: Describe the problem or need + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + description: How would you like this to work? + validations: + required: true + + - type: textarea + id: example + attributes: + label: Example usage + description: Show how the API might look + render: python diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..e482f32 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## What does this PR do? + + + +## Related Issues + +Fixes # + +## Checklist + +- [ ] Tests pass (`uv run pytest`) +- [ ] Code formatted (`ruff format` and `ruff check`) +- [ ] Pre-commit hooks pass (`pre-commit run --all-files`) diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..9b60b2b --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,12 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + build: + uses: ./.github/workflows/build-reusable.yml diff --git a/.github/workflows/build-reusable.yml b/.github/workflows/build-reusable.yml new file mode 100644 index 0000000..7f5a917 --- /dev/null +++ b/.github/workflows/build-reusable.yml @@ -0,0 +1,741 @@ +name: Reusable Build + +on: + workflow_call: + inputs: + ref: + description: 'Git ref to build' + required: false + type: string + default: '' + +env: + # Large matrix builds download many Python wheels in parallel. Give transient + # PyPI/CDN stalls enough room to recover instead of failing a whole PR run. + UV_HTTP_RETRIES: "8" + UV_HTTP_TIMEOUT: "120" + UV_HTTP_CONNECT_TIMEOUT: "30" + UV_CONCURRENT_DOWNLOADS: "8" + UV_NO_PROGRESS: "1" + +jobs: + lint: + name: Lint and Format Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + submodules: recursive + + - name: Install uv and Python + uses: astral-sh/setup-uv@v6 + with: + python-version: '3.11' + enable-cache: true + cache-dependency-glob: uv.lock + + - name: Run pre-commit with only lint group (no project deps) + run: | + uv run --frozen --only-group lint pre-commit run --all-files --show-diff-on-failure + + type-check: + name: Type Check with ty + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + submodules: recursive + + - name: Install uv and Python + uses: astral-sh/setup-uv@v6 + with: + python-version: '3.11' + enable-cache: true + cache-dependency-glob: uv.lock + + - name: Install ty + run: uv tool install ty==0.0.17 + + - name: Run ty type checker + run: | + # Run ty on core packages, apps, and tests + ty check packages/leann-core/src apps tests + + build: + needs: [lint, type-check] + name: Build ${{ matrix.os }} Python ${{ matrix.python }} + defaults: + run: + shell: bash + strategy: + fail-fast: false + matrix: + include: + # Note: Python 3.9 dropped - uses PEP 604 union syntax (str | None) + # which requires Python 3.10+ + - os: ubuntu-22.04 + python: '3.10' + - os: ubuntu-22.04 + python: '3.11' + - os: ubuntu-22.04 + python: '3.12' + - os: ubuntu-22.04 + python: '3.13' + # ARM64 Linux builds + - os: ubuntu-22.04-arm + python: '3.10' + - os: ubuntu-22.04-arm + python: '3.11' + - os: ubuntu-22.04-arm + python: '3.12' + - os: ubuntu-22.04-arm + python: '3.13' + - os: macos-14 + python: '3.10' + - os: macos-14 + python: '3.11' + - os: macos-14 + python: '3.12' + - os: macos-14 + python: '3.13' + - os: macos-15 + python: '3.10' + - os: macos-15 + python: '3.11' + - os: macos-15 + python: '3.12' + - os: macos-15 + python: '3.13' + # Intel Mac builds (x86_64) - replaces deprecated macos-13 + # Note: Python 3.13 excluded - PyTorch has no wheels for macOS x86_64 + Python 3.13 + # (PyTorch <=2.4.1 lacks cp313, PyTorch >=2.5.0 dropped Intel Mac support) + - os: macos-15-intel + python: '3.10' + - os: macos-15-intel + python: '3.11' + - os: macos-15-intel + python: '3.12' + # macOS 26 (beta) - arm64 + - os: macos-26 + python: '3.10' + - os: macos-26 + python: '3.11' + - os: macos-26 + python: '3.12' + - os: macos-26 + python: '3.13' + # Windows validation (native HNSW + DiskANN build/install path) + - os: windows-2022 + python: '3.11' + - os: windows-2022 + python: '3.12' + - os: windows-2022 + python: '3.13' + - os: windows-2022 + python: '3.14' + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.ref }} + submodules: recursive + + - name: Install uv and Python + uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python }} + enable-cache: true + cache-dependency-glob: uv.lock + + - name: Install system dependencies (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y cmake libomp-dev libboost-all-dev protobuf-compiler libzmq3-dev \ + pkg-config libabsl-dev libaio-dev libprotobuf-dev \ + patchelf + + # Debug: Show system information + echo "πŸ” System Information:" + echo "Architecture: $(uname -m)" + echo "OS: $(uname -a)" + echo "CPU info: $(lscpu | head -5)" + + # Install math library based on architecture + ARCH=$(uname -m) + echo "πŸ” Setting up math library for architecture: $ARCH" + + if [[ "$ARCH" == "x86_64" ]]; then + # Install Intel MKL for DiskANN on x86_64 + echo "πŸ“¦ Installing Intel MKL for x86_64..." + wget -q https://registrationcenter-download.intel.com/akdlm/IRC_NAS/79153e0f-74d7-45af-b8c2-258941adf58a/intel-onemkl-2025.0.0.940.sh + sudo sh intel-onemkl-2025.0.0.940.sh -a --components intel.oneapi.lin.mkl.devel --action install --eula accept -s + source /opt/intel/oneapi/setvars.sh + echo "MKLROOT=/opt/intel/oneapi/mkl/latest" >> $GITHUB_ENV + echo "LD_LIBRARY_PATH=/opt/intel/oneapi/compiler/latest/linux/compiler/lib/intel64_lin" >> $GITHUB_ENV + echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/intel/oneapi/mkl/latest/lib/intel64" >> $GITHUB_ENV + echo "βœ… Intel MKL installed for x86_64" + + # Debug: Check MKL installation + echo "πŸ” MKL Installation Check:" + ls -la /opt/intel/oneapi/mkl/latest/ || echo "MKL directory not found" + ls -la /opt/intel/oneapi/mkl/latest/lib/ || echo "MKL lib directory not found" + + elif [[ "$ARCH" == "aarch64" ]]; then + # Use OpenBLAS for ARM64 (MKL installer not compatible with ARM64) + echo "πŸ“¦ Installing OpenBLAS for ARM64..." + sudo apt-get install -y libopenblas-dev liblapack-dev liblapacke-dev + echo "βœ… OpenBLAS installed for ARM64" + + # Debug: Check OpenBLAS installation + echo "πŸ” OpenBLAS Installation Check:" + dpkg -l | grep openblas || echo "OpenBLAS package not found" + ls -la /usr/lib/aarch64-linux-gnu/openblas/ || echo "OpenBLAS directory not found" + fi + + # Debug: Show final library paths + echo "πŸ” Final LD_LIBRARY_PATH: $LD_LIBRARY_PATH" + + - name: Install system dependencies (macOS) + if: runner.os == 'macOS' + run: | + # Don't install LLVM, use system clang for better compatibility + # CMake is required for scikit-build (HNSW + DiskANN); install via brew so + # we do not depend on fetching the PyPI cmake package during uv build. + brew install cmake libomp boost protobuf zeromq + + - name: Install system dependencies (Windows) + if: runner.os == 'Windows' + run: | + retry() { + local attempts=$1 + shift + local n=1 + while true; do + "$@" && break + if [[ $n -ge $attempts ]]; then + echo "Command failed after $n attempts: $*" + return 1 + fi + echo "Command failed (attempt $n/$attempts). Retrying in 10s: $*" + sleep 10 + n=$((n + 1)) + done + } + + retry 5 choco install swig -y --no-progress + retry 5 choco install nuget.commandline -y --no-progress + # pkgconfiglite via Chocolatey is flaky (exits 0 even on failure); + # verify the binary exists and fall back to direct download. + retry 3 choco install pkgconfiglite -y --no-progress || true + PKG_CONFIG_DIR="C:/ProgramData/chocolatey/bin" + if [[ ! -f "${PKG_CONFIG_DIR}/pkg-config.exe" ]]; then + echo "pkg-config.exe not found after choco, downloading directly..." + PKG_CONFIG_DIR="${RUNNER_TEMP}/pkg-config" + mkdir -p "${PKG_CONFIG_DIR}" + curl -fsSL -o "${RUNNER_TEMP}/pkgconfiglite.zip" \ + "https://sourceforge.net/projects/pkgconfiglite/files/0.28-1/pkg-config-lite-0.28-1_bin-win32.zip/download" + unzip -q "${RUNNER_TEMP}/pkgconfiglite.zip" -d "${RUNNER_TEMP}/pkgconfiglite" + cp "${RUNNER_TEMP}/pkgconfiglite/pkg-config-lite-0.28-1/bin/"* "${PKG_CONFIG_DIR}/" + fi + echo "${PKG_CONFIG_DIR}" >> "$GITHUB_PATH" + echo "PKG_CONFIG_EXECUTABLE=${PKG_CONFIG_DIR}/pkg-config.exe" >> "$GITHUB_ENV" + if [[ -z "${VCPKG_INSTALLATION_ROOT:-}" ]]; then + echo "VCPKG_INSTALLATION_ROOT is not set on this runner" + exit 1 + fi + retry 5 "${VCPKG_INSTALLATION_ROOT}/vcpkg" install zeromq:x64-windows + retry 5 "${VCPKG_INSTALLATION_ROOT}/vcpkg" install openblas:x64-windows + retry 5 "${VCPKG_INSTALLATION_ROOT}/vcpkg" install lapack:x64-windows + retry 5 "${VCPKG_INSTALLATION_ROOT}/vcpkg" install boost-program-options:x64-windows + retry 5 "${VCPKG_INSTALLATION_ROOT}/vcpkg" install protobuf:x64-windows + + # DiskANN links against Intel OpenMP (libiomp5md) via NuGet during its + # CMake build. The NuGet packages end up in a temp build dir that is + # cleaned up by `uv build`, so delvewheel can't find the DLL later. + # Download it here to a persistent, known location. + NUGET_PKG_DIR="${RUNNER_TEMP}/nuget_pkgs" + retry 5 nuget install intelopenmp.redist.win -Version 2022.0.3.3747 \ + -ExcludeVersion -OutputDirectory "${NUGET_PKG_DIR}" + echo "INTEL_OMP_BIN_DIR=${NUGET_PKG_DIR}/intelopenmp.redist.win/runtimes/win-x64/native" >> "$GITHUB_ENV" + + echo "PKG_CONFIG_PATH=${VCPKG_INSTALLATION_ROOT}/installed/x64-windows/lib/pkgconfig" >> "$GITHUB_ENV" + echo "CMAKE_PREFIX_PATH=${VCPKG_INSTALLATION_ROOT}/installed/x64-windows" >> "$GITHUB_ENV" + echo "OPENBLAS_LIB=${VCPKG_INSTALLATION_ROOT}/installed/x64-windows/lib/openblas.lib" >> "$GITHUB_ENV" + echo "${VCPKG_INSTALLATION_ROOT}/installed/x64-windows/bin" >> "$GITHUB_PATH" + echo "${VCPKG_INSTALLATION_ROOT}/installed/x64-windows/debug/bin" >> "$GITHUB_PATH" + echo "${VCPKG_INSTALLATION_ROOT}/installed/x64-windows/tools/protobuf" >> "$GITHUB_PATH" + pkg-config --version || true + where pkg-config || true + pkg-config --modversion libzmq || true + + - name: Install build dependencies + run: | + retry() { + local attempts=$1 + shift + local n=1 + while true; do + "$@" && break + if [[ $n -ge $attempts ]]; then + echo "Command failed after $n attempts: $*" + return 1 + fi + echo "Command failed (attempt $n/$attempts). Retrying in 5s: $*" + sleep 5 + n=$((n + 1)) + done + } + + retry 5 uv python install ${{ matrix.python }} + uv venv --python ${{ matrix.python }} .uv-build + if [[ "$RUNNER_OS" == "Windows" ]]; then + BUILD_PY=".uv-build\\Scripts\\python.exe" + else + BUILD_PY=".uv-build/bin/python" + fi + retry 5 uv pip install --python "$BUILD_PY" scikit-build-core numpy swig Cython pybind11 + if [[ "$RUNNER_OS" == "Linux" ]]; then + retry 5 uv pip install --python "$BUILD_PY" auditwheel + elif [[ "$RUNNER_OS" == "macOS" ]]; then + retry 5 uv pip install --python "$BUILD_PY" delocate + else + retry 5 uv pip install --python "$BUILD_PY" delvewheel + fi + + if [[ "$RUNNER_OS" == "Windows" ]]; then + echo "$(pwd)\\.uv-build\\Scripts" >> $GITHUB_PATH + else + echo "$(pwd)/.uv-build/bin" >> $GITHUB_PATH + fi + + - name: Set macOS environment variables + if: runner.os == 'macOS' + run: | + # Use brew --prefix to automatically detect Homebrew installation path + HOMEBREW_PREFIX=$(brew --prefix) + echo "HOMEBREW_PREFIX=${HOMEBREW_PREFIX}" >> $GITHUB_ENV + echo "OpenMP_ROOT=${HOMEBREW_PREFIX}/opt/libomp" >> $GITHUB_ENV + + # Set CMAKE_PREFIX_PATH to let CMake find all packages automatically + echo "CMAKE_PREFIX_PATH=${HOMEBREW_PREFIX}" >> $GITHUB_ENV + + # Set compiler flags for OpenMP (required for both backends) + echo "LDFLAGS=-L${HOMEBREW_PREFIX}/opt/libomp/lib" >> $GITHUB_ENV + echo "CPPFLAGS=-I${HOMEBREW_PREFIX}/opt/libomp/include" >> $GITHUB_ENV + + - name: Build packages + run: | + retry() { + local attempts=$1 + shift + local n=1 + while true; do + "$@" && break + if [[ $n -ge $attempts ]]; then + echo "Command failed after $n attempts: $*" + return 1 + fi + echo "Command failed (attempt $n/$attempts). Retrying in 10s: $*" + sleep 10 + n=$((n + 1)) + done + } + + # Build core (platform independent) + cd packages/leann-core + retry 3 uv build + cd ../.. + + # Build HNSW backend + cd packages/leann-backend-hnsw + if [[ "${{ matrix.os }}" == macos-* ]]; then + # Use system clang for better compatibility + export CC=clang + export CXX=clang++ + # Set deployment target based on runner + # macos-15-intel runs macOS 15, so target 15.0 (system libraries require it) + if [[ "${{ matrix.os }}" == "macos-15-intel" ]]; then + export MACOSX_DEPLOYMENT_TARGET=15.0 + elif [[ "${{ matrix.os }}" == macos-14* ]]; then + export MACOSX_DEPLOYMENT_TARGET=14.0 + elif [[ "${{ matrix.os }}" == macos-15* ]]; then + export MACOSX_DEPLOYMENT_TARGET=15.0 + elif [[ "${{ matrix.os }}" == macos-26* ]]; then + export MACOSX_DEPLOYMENT_TARGET=26.0 + fi + retry 3 uv build --wheel --python ${{ matrix.python }} --find-links ${GITHUB_WORKSPACE}/packages/leann-core/dist + else + retry 3 uv build --wheel --python ${{ matrix.python }} --find-links ${GITHUB_WORKSPACE}/packages/leann-core/dist + fi + cd ../.. + + # Build DiskANN backend + cd packages/leann-backend-diskann + if [[ "${{ matrix.os }}" == macos-* ]]; then + # Use system clang for better compatibility + export CC=clang + export CXX=clang++ + # Set deployment target based on runner + # macos-15-intel runs macOS 15, so target 15.0 (system libraries require it) + if [[ "${{ matrix.os }}" == "macos-15-intel" ]]; then + export MACOSX_DEPLOYMENT_TARGET=15.0 + elif [[ "${{ matrix.os }}" == macos-14* ]]; then + export MACOSX_DEPLOYMENT_TARGET=14.0 + elif [[ "${{ matrix.os }}" == macos-15* ]]; then + export MACOSX_DEPLOYMENT_TARGET=15.0 + elif [[ "${{ matrix.os }}" == macos-26* ]]; then + export MACOSX_DEPLOYMENT_TARGET=26.0 + fi + retry 3 uv build --wheel --python ${{ matrix.python }} --find-links ${GITHUB_WORKSPACE}/packages/leann-core/dist + else + retry 3 uv build --wheel --python ${{ matrix.python }} --find-links ${GITHUB_WORKSPACE}/packages/leann-core/dist + fi + cd ../.. + + # Build IVF backend (pure Python; depends on leann-core + faiss-cpu at install time) + cd packages/leann-backend-ivf + retry 3 uv build --wheel --python ${{ matrix.python }} --find-links ${GITHUB_WORKSPACE}/packages/leann-core/dist + cd ../.. + + # Build meta package (platform independent) + cd packages/leann + retry 3 uv build + cd ../.. + + - name: Repair wheels (Linux) + if: runner.os == 'Linux' + run: | + # Repair HNSW wheel + cd packages/leann-backend-hnsw + if [ -d dist ]; then + auditwheel repair dist/*.whl -w dist_repaired + rm -rf dist + mv dist_repaired dist + fi + cd ../.. + + # Repair DiskANN wheel + cd packages/leann-backend-diskann + if [ -d dist ]; then + auditwheel repair dist/*.whl -w dist_repaired + rm -rf dist + mv dist_repaired dist + fi + cd ../.. + + - name: Repair wheels (macOS) + if: runner.os == 'macOS' + run: | + # Determine deployment target based on runner OS + # macos-15-intel runs macOS 15, so target 15.0 (system libraries require it) + if [[ "${{ matrix.os }}" == "macos-15-intel" ]]; then + HNSW_TARGET="15.0" + DISKANN_TARGET="15.0" + elif [[ "${{ matrix.os }}" == macos-14* ]]; then + HNSW_TARGET="14.0" + DISKANN_TARGET="14.0" + elif [[ "${{ matrix.os }}" == macos-15* ]]; then + HNSW_TARGET="15.0" + DISKANN_TARGET="15.0" + elif [[ "${{ matrix.os }}" == macos-26* ]]; then + HNSW_TARGET="26.0" + DISKANN_TARGET="26.0" + fi + + # Repair HNSW wheel + cd packages/leann-backend-hnsw + if [ -d dist ]; then + export MACOSX_DEPLOYMENT_TARGET=$HNSW_TARGET + delocate-wheel -w dist_repaired -v --require-target-macos-version $HNSW_TARGET dist/*.whl + rm -rf dist + mv dist_repaired dist + fi + cd ../.. + + # Repair DiskANN wheel + cd packages/leann-backend-diskann + if [ -d dist ]; then + export MACOSX_DEPLOYMENT_TARGET=$DISKANN_TARGET + delocate-wheel -w dist_repaired -v --require-target-macos-version $DISKANN_TARGET dist/*.whl + rm -rf dist + mv dist_repaired dist + fi + cd ../.. + + - name: Repair wheels (Windows) + if: runner.os == 'Windows' + run: | + # Repair HNSW wheel + cd packages/leann-backend-hnsw + if [ -d dist ]; then + delvewheel repair dist/*.whl -w dist_repaired --add-path "${VCPKG_INSTALLATION_ROOT}/installed/x64-windows/bin" + rm -rf dist + mv dist_repaired dist + fi + cd ../.. + + # Repair DiskANN wheel. + # DiskANN's CMake install bundles diskann.dll and libiomp5md.dll inside + # the wheel, but delvewheel doesn't search inside the wheel for deps. + # Extract the wheel so delvewheel can discover them via --add-path. + cd packages/leann-backend-diskann + if [ -d dist ]; then + # Extract the wheel and build --add-path with native Windows paths. + # mktemp returns Git Bash paths (/tmp/...) that delvewheel.exe can't + # resolve, so use Python for the entire extract-and-repair flow. + python -c " + import zipfile, sys, glob, tempfile, subprocess, os, shutil + whl = glob.glob('dist/*.whl')[0] + tmpdir = tempfile.mkdtemp(prefix='diskann_whl_') + zipfile.ZipFile(whl).extractall(tmpdir) + + add_paths = [os.environ.get('VCPKG_INSTALLATION_ROOT','') + '/installed/x64-windows/bin', + os.environ.get('INTEL_OMP_BIN_DIR','')] + for entry in os.listdir(tmpdir): + full = os.path.join(tmpdir, entry) + if os.path.isdir(full): + add_paths.append(full) + add_path_str = ';'.join(p for p in add_paths if p) + + print(f'add-path: {add_path_str}') + rc = subprocess.call([sys.executable, '-m', 'delvewheel', 'repair', whl, + '-w', 'dist_repaired', '--add-path', add_path_str]) + shutil.rmtree(tmpdir, ignore_errors=True) + sys.exit(rc) + " + rm -rf dist + mv dist_repaired dist + fi + cd ../.. + + - name: List built packages + run: | + echo "πŸ“¦ Built packages:" + find packages/*/dist -name "*.whl" -o -name "*.tar.gz" | sort + + + - name: Install built packages for testing + run: | + retry() { + local attempts=$1 + shift + local n=1 + while true; do + "$@" && break + if [[ $n -ge $attempts ]]; then + echo "Command failed after $n attempts: $*" + return 1 + fi + echo "Command failed (attempt $n/$attempts). Retrying in 5s: $*" + sleep 5 + n=$((n + 1)) + done + } + + # Create uv-managed virtual environment with the requested interpreter + retry 5 uv python install ${{ matrix.python }} + uv venv --python ${{ matrix.python }} + source .venv/bin/activate || source .venv/Scripts/activate + + if [[ "$RUNNER_OS" == "Windows" ]]; then + UV_PY=".venv\\Scripts\\python.exe" + else + UV_PY=".venv/bin/python" + fi + + # Install test dependency group only (avoids reinstalling project package) + retry 5 uv pip install --python "$UV_PY" --group test + + # Install core wheel built in this job + CORE_WHL=$(find packages/leann-core/dist -maxdepth 1 -name "*.whl" -print -quit) + if [[ -n "$CORE_WHL" ]]; then + retry 5 uv pip install --python "$UV_PY" "$CORE_WHL" + else + retry 5 uv pip install --python "$UV_PY" packages/leann-core/dist/*.tar.gz + fi + + PY_TAG=$($UV_PY -c "import sys; print(f'cp{sys.version_info[0]}{sys.version_info[1]}')") + + if [[ "$RUNNER_OS" == "macOS" ]]; then + # macos-15-intel runs macOS 15, so target 15.0 (system libraries require it) + if [[ "${{ matrix.os }}" == "macos-15-intel" ]]; then + export MACOSX_DEPLOYMENT_TARGET=15.0 + elif [[ "${{ matrix.os }}" == macos-14* ]]; then + export MACOSX_DEPLOYMENT_TARGET=14.0 + elif [[ "${{ matrix.os }}" == macos-15* ]]; then + export MACOSX_DEPLOYMENT_TARGET=15.0 + elif [[ "${{ matrix.os }}" == macos-26* ]]; then + export MACOSX_DEPLOYMENT_TARGET=26.0 + fi + fi + + HNSW_WHL=$(find packages/leann-backend-hnsw/dist -maxdepth 1 -name "*-${PY_TAG}-*.whl" -print -quit) + if [[ -z "$HNSW_WHL" ]]; then + HNSW_WHL=$(find packages/leann-backend-hnsw/dist -maxdepth 1 -name "*-py3-*.whl" -print -quit) + fi + if [[ -n "$HNSW_WHL" ]]; then + retry 5 uv pip install --python "$UV_PY" "$HNSW_WHL" + else + retry 5 uv pip install --python "$UV_PY" ./packages/leann-backend-hnsw + fi + + DISKANN_WHL=$(find packages/leann-backend-diskann/dist -maxdepth 1 -name "*-${PY_TAG}-*.whl" -print -quit) + if [[ -z "$DISKANN_WHL" ]]; then + DISKANN_WHL=$(find packages/leann-backend-diskann/dist -maxdepth 1 -name "*-py3-*.whl" -print -quit) + fi + if [[ -n "$DISKANN_WHL" ]]; then + retry 5 uv pip install --python "$UV_PY" "$DISKANN_WHL" + else + retry 5 uv pip install --python "$UV_PY" ./packages/leann-backend-diskann + fi + + IVF_WHL=$(find packages/leann-backend-ivf/dist -maxdepth 1 -name "*.whl" -print -quit) + if [[ -n "$IVF_WHL" ]]; then + retry 5 uv pip install --python "$UV_PY" "$IVF_WHL" + else + retry 5 uv pip install --python "$UV_PY" ./packages/leann-backend-ivf + fi + + LEANN_WHL=$(find packages/leann/dist -maxdepth 1 -name "*.whl" -print -quit) + if [[ -n "$LEANN_WHL" ]]; then + retry 5 uv pip install --python "$UV_PY" "$LEANN_WHL" + else + retry 5 uv pip install --python "$UV_PY" packages/leann/dist/*.tar.gz + fi + + - name: Run tests with pytest + env: + CI: true + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + HF_HUB_DISABLE_SYMLINKS: 1 + TOKENIZERS_PARALLELISM: false + PYTORCH_ENABLE_MPS_FALLBACK: 0 + OMP_NUM_THREADS: 1 + MKL_NUM_THREADS: 1 + run: | + source .venv/bin/activate || source .venv/Scripts/activate + pytest tests/ -v --tb=short + + - name: Run sanity checks (optional) + run: | + # Activate virtual environment + source .venv/bin/activate || source .venv/Scripts/activate + + # Run distance function tests if available + if [ -f test/sanity_checks/test_distance_functions.py ]; then + echo "Running distance function sanity checks..." + python test/sanity_checks/test_distance_functions.py || echo "⚠️ Distance function test failed, continuing..." + fi + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: packages-${{ matrix.os }}-py${{ matrix.python }} + path: packages/*/dist/ + + + arch-smoke: + name: Arch Linux smoke test (install & import) + needs: build + runs-on: ubuntu-latest + container: + image: archlinux:latest + + steps: + - name: Prepare system + run: | + # Initialize pacman keyring to avoid "no secret key available" error + pacman-key --init + pacman -Syu --noconfirm + # Install build essentials (uv will manage Python version) + pacman -S --noconfirm gcc git zlib openssl + + - name: Download ALL wheel artifacts from this run + uses: actions/download-artifact@v5 + with: + # Don't specify name, download all artifacts + path: ./wheels + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Create virtual environment and install wheels + run: | + retry() { + local attempts=$1 + shift + local n=1 + while true; do + "$@" && break + if [[ $n -ge $attempts ]]; then + echo "Command failed after $n attempts: $*" + return 1 + fi + echo "Command failed (attempt $n/$attempts). Retrying in 10s: $*" + sleep 10 + n=$((n + 1)) + done + } + + # Use Python 3.13 explicitly (Arch has Python 3.14 which PyO3/tokenizers doesn't support yet) + retry 5 uv python install 3.13 + uv venv --python 3.13 + source .venv/bin/activate || source .venv/Scripts/activate + # Flatten artifact subdirectories into a single wheelhouse. + # actions/download-artifact stores each artifact in its own folder and + # pip/uv --find-links does not recurse into nested directories. + mkdir -p wheelhouse + find wheels -name "*.whl" -exec cp {} wheelhouse/ \; + + # Prefer wheels produced in this workflow run for our internal packages, + # but still allow dependencies to be installed from the normal index. + retry 5 uv pip install --find-links wheelhouse leann-core + retry 5 uv pip install --find-links wheelhouse leann-backend-hnsw + retry 5 uv pip install --find-links wheelhouse leann-backend-diskann + retry 5 uv pip install --find-links wheelhouse leann-backend-ivf + retry 5 uv pip install --find-links wheelhouse leann + + - name: Import & tiny runtime check + env: + OMP_NUM_THREADS: 1 + MKL_NUM_THREADS: 1 + run: | + source .venv/bin/activate || source .venv/Scripts/activate + python - <<'PY' + import numpy as np + import leann + import leann_backend_hnsw as h + import leann_backend_diskann as d + import leann_backend_ivf as ivf + from leann import LeannBuilder, LeannSearcher + + b = LeannBuilder( + backend_name="hnsw", + dimensions=2, + is_compact=False, + is_recompute=False, + ) + b.add_text("hello arch") + b.build_index_from_arrays( + "arch_demo.leann", + ["0"], + np.asarray([[1.0, 0.0]], dtype=np.float32), + ) + + with LeannSearcher( + "arch_demo.leann", + recompute_embeddings=False, + enable_warmup=False, + ) as s: + s.backend_impl.compute_query_embedding = lambda *args, **kwargs: np.asarray( + [[1.0, 0.0]], dtype=np.float32 + ) + result = s.search("hello", top_k=1) + + assert result and result[0].text == "hello arch" + print("arch smoke ok") + PY diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml new file mode 100644 index 0000000..8879bb5 --- /dev/null +++ b/.github/workflows/link-check.yml @@ -0,0 +1,27 @@ +name: Link Check + +on: + push: + branches: [ main, master ] + pull_request: + schedule: + - cron: "0 3 * * 1" + +jobs: + link-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: lycheeverse/lychee-action@v2 + with: + args: >- + --no-progress --insecure + --user-agent 'curl/7.68.0' + --max-retries 3 + --retry-wait-time 5 + --exclude '.*star-history\.com.*' + --accept 200,201,202,203,204,205,206,207,208,226,300,301,302,303,304,305,306,307,308,503 + README.md docs/ apps/ examples/ benchmarks/ + fail: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-manual.yml b/.github/workflows/release-manual.yml new file mode 100644 index 0000000..8054edb --- /dev/null +++ b/.github/workflows/release-manual.yml @@ -0,0 +1,171 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., 0.1.2)' + required: true + type: string + +jobs: + verify-ci: + name: Verify main CI + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + + steps: + - name: Require latest main CI success + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + MAIN_SHA=$(gh api repos/${GITHUB_REPOSITORY}/commits/main --jq '.sha') + RUN_ID=$(gh api repos/${GITHUB_REPOSITORY}/actions/workflows/build-and-publish.yml/runs \ + --method GET \ + --field branch=main \ + --field head_sha="${MAIN_SHA}" \ + --field per_page=1 \ + --jq '.workflow_runs[0].id') + + if [ -z "${RUN_ID}" ] || [ "${RUN_ID}" = "null" ]; then + echo "❌ No CI run found for main @ ${MAIN_SHA}" + exit 1 + fi + + STATUS=$(gh api repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID} --jq '.status') + CONCLUSION=$(gh api repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID} --jq '.conclusion') + URL=$(gh api repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID} --jq '.html_url') + + echo "CI run: ${URL}" + if [ "${STATUS}" != "completed" ] || [ "${CONCLUSION}" != "success" ]; then + echo "❌ CI not successful for main @ ${MAIN_SHA}" + echo "Status: ${STATUS}" + echo "Conclusion: ${CONCLUSION}" + exit 1 + fi + + echo "βœ… CI succeeded for main @ ${MAIN_SHA}" + + update-version: + name: Update Version + needs: verify-ci + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + commit-sha: ${{ steps.push.outputs.commit-sha }} + + steps: + - uses: actions/checkout@v4 + + - name: Validate version + run: | + # Remove 'v' prefix if present for validation + VERSION_CLEAN="${{ inputs.version }}" + VERSION_CLEAN="${VERSION_CLEAN#v}" + if ! [[ "$VERSION_CLEAN" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "❌ Invalid version format. Expected format: X.Y.Z or vX.Y.Z" + exit 1 + fi + echo "βœ… Version format valid: ${{ inputs.version }}" + + - name: Update versions and push + id: push + run: | + # Check current version + CURRENT_VERSION=$(grep "^version" packages/leann-core/pyproject.toml | cut -d'"' -f2) + echo "Current version: $CURRENT_VERSION" + echo "Target version: ${{ inputs.version }}" + + if [ "$CURRENT_VERSION" = "${{ inputs.version }}" ]; then + echo "⚠️ Version is already ${{ inputs.version }}, skipping update" + COMMIT_SHA=$(git rev-parse HEAD) + else + ./scripts/bump_version.sh ${{ inputs.version }} + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git add packages/*/pyproject.toml + git commit -m "chore: release v${{ inputs.version }}" + git push origin main + COMMIT_SHA=$(git rev-parse HEAD) + echo "βœ… Pushed version update: $COMMIT_SHA" + fi + + echo "commit-sha=$COMMIT_SHA" >> $GITHUB_OUTPUT + + build-packages: + name: Build packages + needs: update-version + uses: ./.github/workflows/build-reusable.yml + with: + ref: 'main' + + publish: + name: Publish and Release + needs: [update-version, build-packages] + if: always() && needs.update-version.result == 'success' && needs.build-packages.result == 'success' + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + ref: 'main' + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist-artifacts + + - name: Collect packages + run: | + mkdir -p dist + find dist-artifacts -name "*.whl" -exec cp {} dist/ \; + find dist-artifacts -name "*.tar.gz" -exec cp {} dist/ \; + + echo "πŸ“¦ Packages to publish:" + ls -la dist/ + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + if [ -z "$TWINE_PASSWORD" ]; then + echo "❌ PYPI_API_TOKEN not configured!" + exit 1 + fi + + pip install twine + twine upload dist/* --skip-existing --verbose + + echo "βœ… Published to PyPI!" + + - name: Create release + run: | + # Check if tag already exists + if git rev-parse "v${{ inputs.version }}" >/dev/null 2>&1; then + echo "⚠️ Tag v${{ inputs.version }} already exists, skipping tag creation" + else + git tag "v${{ inputs.version }}" + git push origin "v${{ inputs.version }}" + echo "βœ… Created and pushed tag v${{ inputs.version }}" + fi + + # Check if release already exists + if gh release view "v${{ inputs.version }}" >/dev/null 2>&1; then + echo "⚠️ Release v${{ inputs.version }} already exists, skipping release creation" + else + gh release create "v${{ inputs.version }}" \ + --title "Release v${{ inputs.version }}" \ + --notes "πŸš€ Released to PyPI: https://pypi.org/project/leann/${{ inputs.version }}/" \ + --latest + echo "βœ… Created GitHub release v${{ inputs.version }}" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..5bcae98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,126 @@ +raw_data/ +scaling_out/ +scaling_out_old/ +sanity_check/ +demo/indices/ +# .vscode/ +*.log +*pycache* +outputs/ +*.pkl +*.pdf +*.idx +*.map +.history/ +lm_eval.egg-info/ +demo/experiment_results/**/*.json +*.jsonl +*.eml +*.emlx +*.json +*.png +!.vscode/*.json +!.devcontainer/*.json +!skills/**/claw.json +*.sh +*.txt +!CMakeLists.txt +!llms.txt +latency_breakdown*.json +experiment_results/eval_results/diskann/*.json +aws/ +.venv/ +.cursor/rules/ +*.egg-info/ +skip_reorder_comparison/ +analysis_results/ +build/ +.cache/ +nprobe_logs/ +micro/results +micro/contriever-INT8 +data/* +!data/2501.14312v1 (1).pdf +!data/2506.08276v1.pdf +!data/PrideandPrejudice.txt +!data/huawei_pangu.md +!data/ground_truth/ +!data/indices/ +!data/queries/ +!data/.gitattributes +*.qdstrm +benchmark_results/ +results/ +frac_*.png +final_in_*.png +embedding_comparison_results/ +*.ind +*.gz +*.fvecs +*.ivecs +*.index +*.bin +*.old + +read_graph +analyze_diskann_graph +degree_distribution.png +micro/degree_distribution.png + +policy_results_* +results_*/ +experiment_results/ +.DS_Store + +# The above are inherited from old Power RAG repo + +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +.env + +test_indices*/ +test_*.py +!tests/** +# Re-ignore common generated artifacts globally (especially after allowlist rules). +**/.DS_Store +**/__pycache__/ +**/*.cpython-*.pyc +**/*.cpython-*.pyo +packages/leann-backend-diskann/third_party/DiskANN/_deps/ + +*.meta.json +*.passages.json +*.npy +*.db +batchtest.py +tests/__pytest_cache__/ +tests/__pycache__/ +benchmarks/data/ + +## multi vector +apps/multimodal/vision-based-pdf-multi-vector/multi-vector-colpali-native-weaviate.py + +# Ignore all PDFs (keep data exceptions above) and do not track demo PDFs +# If you need to commit a specific demo PDF, remove this negation locally. +# The following line used to force-add a large demo PDF; remove it to satisfy pre-commit: +# !apps/multimodal/vision-based-pdf-multi-vector/pdfs/2004.12832v2.pdf +!apps/multimodal/vision-based-pdf-multi-vector/fig/* + +# AUR build directory (Arch Linux) +paru-bin/ +merkle-tree-test/ +test-code/ +localtestmcp/ +*.csv +*.pickle + +# Personal dev notes (not tracked) +docs/dev/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..359164c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,19 @@ +[submodule "packages/leann-backend-diskann/third_party/DiskANN"] + path = packages/leann-backend-diskann/third_party/DiskANN + url = https://github.com/yichuan-w/DiskANN.git +[submodule "packages/leann-backend-hnsw/third_party/faiss"] + path = packages/leann-backend-hnsw/third_party/faiss + url = https://github.com/yichuan-w/faiss.git +[submodule "packages/leann-backend-hnsw/third_party/msgpack-c"] + path = packages/leann-backend-hnsw/third_party/msgpack-c + url = https://github.com/msgpack/msgpack-c.git + branch = cpp_master +[submodule "packages/leann-backend-hnsw/third_party/cppzmq"] + path = packages/leann-backend-hnsw/third_party/cppzmq + url = https://github.com/zeromq/cppzmq.git +[submodule "packages/leann-backend-hnsw/third_party/libzmq"] + path = packages/leann-backend-hnsw/third_party/libzmq + url = https://github.com/zeromq/libzmq.git +[submodule "packages/astchunk-leann"] + path = packages/astchunk-leann + url = https://github.com/yichuan-w/astchunk-leann.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b3e7040 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict + - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.7 # Fixed version to match pyproject.toml + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..e6a7fad --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "charliermarsh.ruff", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1fedac2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + "python.defaultInterpreterPath": ".venv/bin/python", + "python.terminal.activateEnvironment": true, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.fixAll": "explicit" + }, + "editor.insertSpaces": true, + "editor.tabSize": 4 + }, + "ruff.enable": true, + "files.watcherExclude": { + "**/.venv/**": true, + "**/__pycache__/**": true, + "**/*.egg-info/**": true, + "**/build/**": true, + "**/dist/**": true + }, + "accessibility.signals.terminalBell": { + "sound": "on", + "announcement": "auto" + }, + "cmake.sourceDirectory": "/Users/yichuan/Desktop/code/LEANN/leann/packages/leann-backend-hnsw" +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c6f5487 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,214 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +LEANN is a lightweight vector database and RAG (Retrieval-Augmented Generation) system that achieves 97% storage reduction compared to traditional vector databases through graph-based selective recomputation. It enables semantic search across various data sources (emails, browser history, chat history, code, documents) on a single laptop without cloud dependencies. + +## Build & Development Commands + +### Quick install (pip) + +```bash +pip install leann +``` + +### Development setup (from source) + +```bash +# Install uv first (required package manager) +curl -LsSf https://astral.sh/uv/install.sh | sh + +git submodule update --init --recursive + +# macOS +brew install libomp boost protobuf zeromq pkgconf +uv sync + +# Ubuntu/Debian +sudo apt-get install libomp-dev libboost-all-dev protobuf-compiler \ + libabsl-dev libmkl-full-dev libaio-dev libzmq3-dev +uv sync + +# Windows (requires VS 2022 Build Tools with C++ workload, vcpkg, chocolatey) +choco install cmake swig pkgconfiglite nuget.commandline -y +vcpkg install zeromq:x64-windows openblas:x64-windows lapack:x64-windows boost-program-options:x64-windows protobuf:x64-windows +# Set CMAKE_PREFIX_PATH, PKG_CONFIG_PATH, OPENBLAS_LIB to vcpkg paths (see README) +uv sync --extra diskann + +# Install lint tools +uv sync --group lint + +# Install test tools +uv sync --group test +``` + +## Code Quality + +```bash +# Format code +ruff format + +# Lint with auto-fix +ruff check --fix + +# Pre-commit hooks (install once) +pre-commit install + +# Run pre-commit manually +uv run pre-commit run --all-files +``` + +## Architecture + +### Core API Layer (`packages/leann-core/src/leann/`) + +- `api.py`: Main APIs - `LeannBuilder`, `LeannSearcher`, `LeannChat` +- `react_agent.py`: `ReActAgent` for multi-turn reasoning +- `cli.py`: CLI implementation (`leann build`, `leann search`, `leann ask`) +- `chat.py`: LLM provider integrations (OpenAI, Ollama, HuggingFace, Anthropic) +- `embedding_compute.py`: Embedding computation (sentence-transformers, MLX, OpenAI) +- `metadata_filter.py`: Search result filtering by metadata + +### Backend Layer (`packages/`) + +- `leann-backend-hnsw/`: Default backend using FAISS HNSW for fast in-memory search +- `leann-backend-ivf/`: IVF backend (FAISS IndexIVFFlat + DirectMap.Hashtable) supporting in-place add/remove without rebuild +- `leann-backend-diskann/`: DiskANN backend for larger-than-memory datasets +- `leann-mcp/`: MCP server for Claude Code integration + +Backends are auto-discovered via `leann-backend-*` naming convention and registered in `registry.py`. + +### RAG Applications (`apps/`) + +Example applications demonstrating RAG on various data sources: +- `document_rag.py`: PDF/TXT/MD documents +- `email_rag.py`: Apple Mail +- `browser_rag.py`: Chrome browser history +- `wechat_rag.py`, `imessage_rag.py`: Chat history +- `code_rag.py`: Codebase search with AST-aware chunking +- `slack_rag.py`, `twitter_rag.py`: MCP-based live data + +## Key Design Patterns + +### Incremental Update (IVF backend) + +The IVF backend supports in-place updates and deletes without rebuilding the entire index: +- `add_vectors(index_path, embeddings, passage_ids)`: Append new vectors to an existing index. +- `remove_ids(index_path, passage_ids)`: Remove vectors by passage ID using FAISS DirectMap.Hashtable. +- `LeannBuilder.update_index()`: High-level API that orchestrates remove-then-add for changed files, compacts `passages.jsonl`, and updates the offset map. + +`leann build` is idempotent β€” re-running it on an existing index automatically performs an incremental update instead of a full rebuild. It detects new, modified, and removed files and applies the minimal set of changes: +- **IVF**: Supports add, remove, and modify incrementally (remove old chunks then re-insert). +- **HNSW** (non-compact): Supports add-only incremental updates; modified/removed files trigger a full rebuild. +- Use `--force` / `-f` to force a full rebuild regardless. + +### Index Structure + +A LEANN index consists of: +- `.meta.json`: Metadata (backend, embedding model, dimensions) +- `.passages.jsonl`: Raw text chunks with metadata +- `.passages.idx`: Offset map for fast passage lookup +- `.index`: Backend-specific vector index + +### Embedding Recomputation + +The core storage optimization: instead of storing embeddings, LEANN stores a pruned graph and recomputes embeddings on-demand during search via ZMQ server communication. + +## CLI Usage + +```bash +# Build index +leann build my-docs --docs ./documents/ + +# Search +leann search my-docs "query" + +# Interactive chat +leann ask my-docs --interactive + +# List indexes +leann list + +# Remove index +leann remove my-docs +``` + +## Common Development Tasks + +Running example RAG applications: +```bash +# Document RAG (easiest to test) +python -m apps.document_rag --query "What is LEANN?" + +# Code RAG +python -m apps.code_rag --repo-dir ./src --query "How does search work?" +``` + +## Python Version + +Requires Python 3.10+ (uses PEP 604 union syntax `X | Y`). + + + + +# Agent Coding Guidelines + +## General +- Voice input may contain typos β€” interpret intent, not literal text. +- When you encounter a problem, fix it immediately and keep going until there are no more problems. +- Do not ask about ordering or sequencing β€” figure it out. If something is unclear, note it and skip it; only escalate when all paths are blocked. +- Obvious bugs: fix silently without reporting. +- No fallbacks or compatibility shims. One correct implementation per feature β€” no redundancy. + +## Roadmap +- Public roadmap: `docs/roadmap.md` β€” tracks P0/P1 priorities, completed milestones, and timeline. +- Long-term vision: `docs/ultimate_goal.md` β€” the north star for where LEANN is headed. +- Keep in sync with [GitHub issue #237](https://github.com/yichuan-w/LEANN/issues/237). +- Welcome everyone to add more, and the craziest feature you want to put here! If people want some feature, all put there. + +## Changelog (for contributors) +- Maintain `docs/CHANGELOG.md` β€” append-only log of major changes (new features, breaking changes, important fixes). +- Format: `## YYYY-MM-DD: ` followed by bullet points. +- Update the changelog when merging significant PRs or completing notable work. +- See `docs/CONTRIBUTING.md` for full contributor workflow (conventional commits, PR process, CI). + +## Personal Dev Notes (gitignored) +- `docs/dev/` is gitignored for personal development notes (TODO, progress, experiments). +- Use `docs/dev/TODO.md` for in-progress tasks, `docs/dev/PROGRESS.md` for completed work. +- These are private scratch space β€” but must follow the Self-Contained Principle below. + +## Documentation β€” Self-Contained Principle + +All dev docs (`PROGRESS.md`, `STATES.md`, `EXPERIMENTS.md`, `TODO.md`) must be fully understandable from the document alone, with no reliance on conversation context or implied knowledge. + +Requirements: +1. **Every technique/approach must be explained on first use.** Not "switched to IVF backend" β€” write "switched to IVF backend (FAISS IndexIVFFlat + DirectMap.Hashtable, supports in-place add/remove without full index rebuild)." +2. **Never assume the reader knows any abbreviation.** On first use: full name + one-sentence explanation. E.g., "HNSW (Hierarchical Navigable Small World β€” a graph-based ANN index used as LEANN's default backend)." +3. **Benchmark results must include full context.** Not "recall improved to 0.95" β€” write "recall@10 improved from 0.91 to 0.95 after switching from flat chunking (512 tokens, no overlap) to AST-aware chunking (function-level splits with 64-token overlap)." +4. **Numbers must have reference points.** Not "build time: 12s" β€” write "build time: 12s (vs. 45s before incremental update support, on 10k-document corpus)." +5. **Include the causal chain β€” not just conclusions.** Not "duplicate chunks appeared after incremental build" β€” write "Duplicate chunks appeared after incremental build because `passages.jsonl` was appended without first removing stale entries for modified files. The IVF index had correct vectors (remove-then-add), but the passage store was append-only, causing the same text to appear at multiple offsets." +6. **`docs/dev/STATES.md` top section maintains a glossary** of all key terms (backends, index files, chunking strategies, embedding models). Other docs reference it at the top. + +Bad examples (forbidden): +- "Fixed the chunking bug" β†’ Which bug? What was the symptom? What was the root cause? +- "Improved search quality" β†’ By what metric? From what baseline? What change caused it? +- "Used nprobe=32" β†’ What is nprobe? Why 32? What was it before and what effect did the change have? + +## Doc Maintenance +- Maintain `docs/dev/PROGRESS.md` β€” completed work only (with key script/log/config paths). No plans. +- Maintain `docs/dev/TODO.md` β€” incomplete/in-progress/next-steps only (aim for one-command reproducibility). When done: remove from TODO, write result to PROGRESS, update STATES/EXPERIMENTS if needed. +- Both files: **append-only, chronological order** (oldest first). Use `tail -n 80 docs/dev/PROGRESS.md` to read recent entries; increase range or grep by date/keyword if needed. +- Keep TODO clean β€” either do items or remove them. Ask the user when unsure how to handle a TODO item. +- Maintain `docs/dev/STATES.md` β€” tracks all currently useful state (index configs, backend choices, known limitations); does NOT grow indefinitely (delete stale entries). +- Maintain `docs/dev/EXPERIMENTS.md` β€” benchmarks, A/B comparisons, parameter sweeps (recall@k, latency, storage size). Experimental content goes here, not in STATES.md. + +## Commits +Commit when: (1) a complete feature is finished and tested, or (2) a destructive change is unavoidable. +```bash +git add +git commit -m β€œfeat: ...” # follow conventional commits +``` +- When correcting errors: fix directly with no trace of the error. +- If you write a correct new version of a file, delete the wrong version. No duplicate implementations. diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..4ce95e5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 LEANN Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100755 index 0000000..468027d --- /dev/null +++ b/README.md @@ -0,0 +1,1353 @@ +

+ LEANN Logo +

+ +

+ + yichuan-w/LEANN | Trendshift + +

+ +

+ Python Versions + CI Status + Platform + MIT License + MCP Integration + + Join Slack + + +

+ +
+ + Take Survey + +

+ We track zero telemetry. This survey is the ONLY way to tell us if you want
+ GPU Acceleration or More Integrations next.
+ πŸ‘‰ Click here to cast your vote (2 mins) +

+
+ +
+

πŸ’¬ Join our Slack community!

+

+ We'd love for you to be part of the LEANN community!
+ πŸ‘‰ Join LEANN Slack
+ If the invite link has expired or you have trouble joining, please open an issue and we'll help you get in! +

+
+ +

+ The smallest vector index in the world. RAG Everything with LEANN! +

+ +LEANN is an innovative vector database that democratizes personal AI. Transform your laptop into a powerful RAG system that can index and search through millions of documents while using **97% less storage** than traditional solutions **without accuracy loss**. + + +LEANN achieves this through *graph-based selective recomputation* with *high-degree preserving pruning*, computing embeddings on-demand instead of storing them all. [Illustration Fig β†’](#️-architecture--how-it-works) | [Paper β†’](https://arxiv.org/abs/2506.08276) + +**Ready to RAG Everything?** Transform your laptop into a personal AI assistant that can semantic search your **[file system](#-personal-data-manager-process-any-documents-pdf-txt-md)**, **[emails](#-your-personal-email-secretary-rag-on-apple-mail)**, **[browser history](#-time-machine-for-the-web-rag-your-entire-browser-history)**, **[chat history](#-wechat-detective-unlock-your-golden-memories)** ([WeChat](#-wechat-detective-unlock-your-golden-memories), [iMessage](#-imessage-history-your-personal-conversation-archive)), **[agent memory](#-chatgpt-chat-history-your-personal-ai-conversation-archive)** ([ChatGPT](#-chatgpt-chat-history-your-personal-ai-conversation-archive), [Claude](#-claude-chat-history-your-personal-ai-conversation-archive)), **[live data](#mcp-integration-rag-on-live-data-from-any-platform)** ([Slack](#slack-messages-search-your-team-conversations), [Twitter](#-twitter-bookmarks-your-personal-tweet-library)), **[codebase](#-claude-code-integration-transform-your-development-workflow)**\*, or external knowledge bases (i.e., 60M documents) - all on your laptop, with zero cloud costs and complete privacy. + + +\* Claude Code only supports basic `grep`-style keyword search. **LEANN** is a drop-in **semantic search MCP service fully compatible with Claude Code**, unlocking intelligent retrieval without changing your workflow. πŸ”₯ Check out [the easy setup β†’](packages/leann-mcp/README.md) + + + +## Why LEANN? + +

+ LEANN vs Traditional Vector DB Storage Comparison +

+ +> **The numbers speak for themselves:** Index 60 million text chunks in just 6GB instead of 201GB. From emails to browser history, everything fits on your laptop. [See detailed benchmarks for different applications below ↓](#-storage-comparison) + + +πŸ”’ **Privacy:** Your data never leaves your laptop. No OpenAI, no cloud, no "terms of service". + +πŸͺΆ **Lightweight:** Graph-based recomputation eliminates heavy embedding storage, while smart graph pruning and CSR format minimize graph storage overhead. Always less storage, less memory usage! + +πŸ“¦ **Portable:** Transfer your entire knowledge base between devices (even with others) with minimal cost - your personal AI memory travels with you. + +πŸ“ˆ **Scalability:** Handle messy personal data that would crash traditional vector DBs, easily managing your growing personalized data and agent generated memory! + +✨ **No Accuracy Loss:** Maintain the same search quality as heavyweight solutions while using 97% less storage. + +## Installation + +### πŸ“¦ Prerequisites: Install uv + +[Install uv](https://docs.astral.sh/uv/getting-started/installation/#installation-methods) first if you don't have it. Typically, you can install it with: + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +### πŸš€ Quick Install + +Clone the repository to access all examples and try amazing applications, + +```bash +git clone https://github.com/yichuan-w/LEANN.git leann +cd leann +``` + +and install LEANN from [PyPI](https://pypi.org/project/leann/) to run them immediately: + +```bash +uv venv +source .venv/bin/activate +uv pip install leann + +# CPU-only (Linux): use the `cpu` extra (e.g. `leann[cpu]`) +``` + + + +
+ +πŸ”§ Build from Source (Recommended for development) + + + + +```bash +git clone https://github.com/yichuan-w/LEANN.git leann +cd leann +git submodule update --init --recursive +``` + +**macOS:** + +Note: DiskANN requires MacOS 13.3 or later. + +```bash +brew install libomp boost protobuf zeromq pkgconf +uv sync --extra diskann +``` + +**Linux (Ubuntu/Debian):** + +Note: On Ubuntu 20.04, you may need to build a newer Abseil and pin Protobuf (e.g., v3.20.x) for building DiskANN. See [Issue #30](https://github.com/yichuan-w/LEANN/issues/30) for a step-by-step note. + +You can manually install [Intel oneAPI MKL](https://www.intel.com/content/www/us/en/developer/tools/oneapi/onemkl.html) instead of `libmkl-full-dev` for DiskANN. You can also use `libopenblas-dev` for building HNSW only, by removing `--extra diskann` in the command below. + +```bash +sudo apt-get update && sudo apt-get install -y \ + libomp-dev libboost-all-dev protobuf-compiler libzmq3-dev \ + pkg-config libabsl-dev libaio-dev libprotobuf-dev \ + libmkl-full-dev + +uv sync --extra diskann +``` + +**Linux (Arch Linux):** + +```bash +sudo pacman -Syu && sudo pacman -S --needed base-devel cmake pkgconf git gcc \ + boost boost-libs protobuf abseil-cpp libaio zeromq + +# For MKL in DiskANN +sudo pacman -S --needed base-devel git +git clone https://aur.archlinux.org/paru-bin.git +cd paru-bin && makepkg -si +paru -S intel-oneapi-mkl intel-oneapi-compiler +source /opt/intel/oneapi/setvars.sh + +uv sync --extra diskann +``` + +**Linux (RHEL / CentOS Stream / Oracle / Rocky / AlmaLinux):** + +See [Issue #50](https://github.com/yichuan-w/LEANN/issues/50) for more details. + +```bash +sudo dnf groupinstall -y "Development Tools" +sudo dnf install -y libomp-devel boost-devel protobuf-compiler protobuf-devel \ + abseil-cpp-devel libaio-devel zeromq-devel pkgconf-pkg-config + +# For MKL in DiskANN +sudo dnf install -y intel-oneapi-mkl intel-oneapi-mkl-devel \ + intel-oneapi-openmp || sudo dnf install -y intel-oneapi-compiler +source /opt/intel/oneapi/setvars.sh + +uv sync --extra diskann +``` + +**Windows:** + +Requires [Visual Studio 2022 Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) with the **C++ desktop development** workload, and [vcpkg](https://github.com/microsoft/vcpkg). + +```powershell +# Install toolchain (if not already present) +choco install cmake swig pkgconfiglite nuget.commandline -y + +# Install C++ dependencies via vcpkg +vcpkg install zeromq:x64-windows openblas:x64-windows lapack:x64-windows ` + boost-program-options:x64-windows protobuf:x64-windows + +# Set environment variables (adjust VCPKG_ROOT to your vcpkg path) +$env:CMAKE_PREFIX_PATH = "$env:VCPKG_ROOT\installed\x64-windows" +$env:PKG_CONFIG_PATH = "$env:VCPKG_ROOT\installed\x64-windows\lib\pkgconfig" +$env:PKG_CONFIG_EXECUTABLE = "C:\ProgramData\chocolatey\bin\pkg-config.exe" +$env:OPENBLAS_LIB = "$env:VCPKG_ROOT\installed\x64-windows\lib\openblas.lib" +$env:PATH += ";$env:VCPKG_ROOT\installed\x64-windows\bin" +$env:PATH += ";$env:VCPKG_ROOT\installed\x64-windows\tools\protobuf" + +uv sync --extra diskann +``` + +
+ + +## Quick Start + +Our declarative API makes RAG as easy as writing a config file. + +Check out [demo.ipynb](demo.ipynb) or [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/yichuan-w/LEANN/blob/main/demo.ipynb) + +```python +from leann import LeannBuilder, LeannSearcher, LeannChat +from pathlib import Path +INDEX_PATH = str(Path("./").resolve() / "demo.leann") + +# Build an index +builder = LeannBuilder(backend_name="hnsw") +builder.add_text("LEANN saves 97% storage compared to traditional vector databases.") +builder.add_text("Tung Tung Tung Sahur calledβ€”they need their banana‑crocodile hybrid back") +builder.build_index(INDEX_PATH) + +# Search +searcher = LeannSearcher(INDEX_PATH) +results = searcher.search("fantastical AI-generated creatures", top_k=1) + +# Chat with your data +chat = LeannChat(INDEX_PATH, llm_config={"type": "hf", "model": "Qwen/Qwen3-0.6B"}) +response = chat.ask("How much storage does LEANN save?", top_k=1) +``` + +## RAG on Everything! + +LEANN supports RAG on various data sources including documents (`.pdf`, `.txt`, `.md`), Apple Mail, Google Search History, WeChat, ChatGPT conversations, Claude conversations, iMessage conversations, and **live data from any platform through MCP (Model Context Protocol) servers** - including Slack, Twitter, and more. + + + +### Generation Model Setup + +#### LLM Backend + +LEANN supports many LLM providers for text generation (HuggingFace, Ollama, Anthropic, and Any OpenAI compatible API). + + +
+πŸ”‘ OpenAI API Setup (Default) + +Set your OpenAI API key as an environment variable: + +```bash +export OPENAI_API_KEY="your-api-key-here" +``` + +Make sure to use `--llm openai` flag when using the CLI. +You can also specify the model name with `--llm-model ` flag. + +
+ +
+πŸ› οΈ Supported LLM & Embedding Providers (via OpenAI Compatibility) + +Thanks to the widespread adoption of the OpenAI API format, LEANN is compatible out-of-the-box with a vast array of LLM and embedding providers. Simply set the `OPENAI_BASE_URL` and `OPENAI_API_KEY` environment variables to connect to your preferred service. + +```sh +export OPENAI_API_KEY="xxx" +export OPENAI_BASE_URL="http://localhost:1234/v1" # base url of the provider +``` + +To use OpenAI compatible endpoint with the CLI interface: + +If you are using it for text generation, make sure to use `--llm openai` flag and specify the model name with `--llm-model ` flag. + +If you are using it for embedding, set the `--embedding-mode openai` flag and specify the model name with `--embedding-model `. + +----- + + +Below is a list of base URLs for common providers to get you started. + + +### πŸ–₯️ Local Inference Engines (Recommended for full privacy) + +| Provider | Sample Base URL | +| ---------------- | --------------------------- | +| **Ollama** | `http://localhost:11434/v1` | +| **LM Studio** | `http://localhost:1234/v1` | +| **vLLM** | `http://localhost:8000/v1` | +| **llama.cpp** | `http://localhost:8080/v1` | +| **SGLang** | `http://localhost:30000/v1` | +| **LiteLLM** | `http://localhost:4000` | + +----- + +### ☁️ Cloud Providers + +> **🚨 A Note on Privacy:** Before choosing a cloud provider, carefully review their privacy and data retention policies. Depending on their terms, your data may be used for their own purposes, including but not limited to human reviews and model training, which can lead to serious consequences if not handled properly. + + +| Provider | Base URL | +| ---------------- | ---------------------------------------------------------- | +| **OpenAI** | `https://api.openai.com/v1` | +| **OpenRouter** | `https://openrouter.ai/api/v1` | +| **Gemini** | `https://generativelanguage.googleapis.com/v1beta/openai/` | +| **x.AI (Grok)** | `https://api.x.ai/v1` | +| **Groq AI** | `https://api.groq.com/openai/v1` | +| **DeepSeek** | `https://api.deepseek.com/v1` | +| **SiliconFlow** | `https://api.siliconflow.cn/v1` | +| **Zhipu (BigModel)** | `https://open.bigmodel.cn/api/paas/v4/` | +| **Mistral AI** | `https://api.mistral.ai/v1` | +| **Anthropic** | `https://api.anthropic.com/v1` | +| **Jina AI** (Embeddings) | `https://api.jina.ai/v1` | + +> **πŸ’‘ Tip: Separate Embedding Provider** +> +> To use a different provider for embeddings (e.g., Jina AI) while using another for LLM, use `--embedding-api-base` and `--embedding-api-key`: +> ```bash +> leann build my-index --docs ./docs \ +> --embedding-mode openai \ +> --embedding-model jina-embeddings-v3 \ +> --embedding-api-base https://api.jina.ai/v1 \ +> --embedding-api-key $JINA_API_KEY +> ``` + +If your provider isn't on this list, don't worry! Check their documentation for an OpenAI-compatible endpointβ€”chances are, it's OpenAI Compatible too! + +
+ +
+πŸ”§ Ollama Setup (Recommended for full privacy) + +**macOS:** + +First, [download Ollama for macOS](https://ollama.com/download/mac). + +```bash +# Pull a lightweight model (recommended for consumer hardware) +ollama pull llama3.2:1b +``` + +**Linux:** + +```bash +# Install Ollama +curl -fsSL https://ollama.ai/install.sh | sh + +# Start Ollama service manually +ollama serve & + +# Pull a lightweight model (recommended for consumer hardware) +ollama pull llama3.2:1b +``` + +
+ + +## ⭐ Flexible Configuration + +LEANN provides flexible parameters for embedding models, search strategies, and data processing to fit your specific needs. + +πŸ“š **Need configuration best practices?** Check our [Configuration Guide](docs/configuration-guide.md) for detailed optimization tips, model selection advice, and solutions to common issues like slow embeddings or poor search quality. + +
+πŸ“‹ Click to expand: Common Parameters (Available in All Examples) + +All RAG examples share these common parameters. **Interactive mode** is available in all examples - simply run without `--query` to start a continuous Q&A session where you can ask multiple questions. Type 'quit' to exit. + +```bash +# Environment Variables (GPU Device Selection) +LEANN_EMBEDDING_DEVICE # GPU for embedding model (e.g., cuda:0, cuda:1, cpu) +LEANN_LLM_DEVICE # GPU for HFChat LLM (e.g., cuda:1, or "cuda" for multi-GPU auto) + +# Core Parameters (General preprocessing for all examples) +--index-dir DIR # Directory to store the index (default: current directory) +--query "YOUR QUESTION" # Single query mode. Omit for interactive chat (type 'quit' to exit), and now you can play with your index interactively +--max-items N # Limit data preprocessing (default: -1, process all data) +--force-rebuild # Force rebuild index even if it exists + +# Embedding Parameters +--embedding-model MODEL # e.g., facebook/contriever, text-embedding-3-small, mlx-community/Qwen3-Embedding-0.6B-8bit or nomic-embed-text +--embedding-mode MODE # sentence-transformers, openai, mlx, or ollama + +# LLM Parameters (Text generation models) +--llm TYPE # LLM backend: openai, ollama, hf, or anthropic (default: openai) +--llm-model MODEL # Model name (default: gpt-4o) e.g., gpt-4o-mini, llama3.2:1b, Qwen/Qwen2.5-1.5B-Instruct +--thinking-budget LEVEL # Thinking budget for reasoning models: low/medium/high (supported by o3, o3-mini, GPT-Oss:20b, and other reasoning models) + +# Search Parameters +--top-k N # Number of results to retrieve (default: 20) +--search-complexity N # Search complexity for graph traversal (default: 32) + +# Chunking Parameters +--chunk-size N # Size of text chunks (default varies by source: 256 for most, 192 for WeChat) +--chunk-overlap N # Overlap between chunks (default varies: 25-128 depending on source) + +# Index Building Parameters +--backend-name NAME # Backend to use: hnsw or diskann (default: hnsw) +--graph-degree N # Graph degree for index construction (default: 32) +--build-complexity N # Build complexity for index construction (default: 64) +--compact / --no-compact # Use compact storage (default: true). Must be `no-compact` for `no-recompute` build. +--recompute / --no-recompute # Enable/disable embedding recomputation (default: enabled). Should not do a `no-recompute` search in a `recompute` build. +``` + +
+ +### πŸ“„ Personal Data Manager: Process Any Documents (`.pdf`, `.txt`, `.md`)! + +Ask questions directly about your personal PDFs, documents, and any directory containing your files! + +

+ LEANN Document Search Demo +

+ +The example below asks a question about summarizing our paper (uses default data in `data/`, which is a directory with diverse data sources: two papers, Pride and Prejudice, and a Technical report about LLM in Huawei in Chinese), and this is the **easiest example** to run here: + +```bash +source .venv/bin/activate # Don't forget to activate the virtual environment +python -m apps.document_rag --query "What are the main techniques LEANN explores?" +``` + +
+πŸ“‹ Click to expand: Document-Specific Arguments + +#### Parameters +```bash +--data-dir DIR # Directory containing documents to process (default: data) +--file-types .ext .ext # Filter by specific file types (optional - all LlamaIndex supported types if omitted) +``` + +#### Example Commands +```bash +# Process all documents with larger chunks for academic papers +python -m apps.document_rag --data-dir "~/Documents/Papers" --chunk-size 1024 + +# Filter only markdown and Python files with smaller chunks +python -m apps.document_rag --data-dir "./docs" --chunk-size 256 --file-types .md .py + +# Enable AST-aware chunking for code files +python -m apps.document_rag --enable-code-chunking --data-dir "./my_project" + +# Or use the specialized code RAG for better code understanding +python -m apps.code_rag --repo-dir "./my_codebase" --query "How does authentication work?" +``` + +
+ +### 🎨 ColQwen: Multimodal PDF Retrieval with Vision-Language Models + +Search through PDFs using both text and visual understanding with ColQwen2/ColPali models. Perfect for research papers, technical documents, and any PDFs with complex layouts, figures, or diagrams. + +> **🍎 Mac Users**: ColQwen is optimized for Apple Silicon with MPS acceleration for faster inference! + +```bash +# Build index from PDFs +python -m apps.colqwen_rag build --pdfs ./my_papers/ --index research_papers + +# Search with text queries +python -m apps.colqwen_rag search research_papers "How does attention mechanism work?" + +# Interactive Q&A +python -m apps.colqwen_rag ask research_papers --interactive +``` + +
+πŸ“‹ Click to expand: ColQwen Setup & Usage + +#### Prerequisites +```bash +# Install dependencies +uv pip install colpali_engine pdf2image pillow matplotlib qwen_vl_utils einops seaborn +brew install poppler # macOS only, for PDF processing +``` + +#### Build Index +```bash +python -m apps.colqwen_rag build \ + --pdfs ./pdf_directory/ \ + --index my_index \ + --model colqwen2 # or colpali +``` + +#### Search +```bash +python -m apps.colqwen_rag search my_index "your question here" --top-k 5 +``` + +#### Models +- **ColQwen2** (`colqwen2`): Latest vision-language model with improved performance +- **ColPali** (`colpali`): Proven multimodal retriever + +For detailed usage, see the [ColQwen Guide](docs/COLQWEN_GUIDE.md). + +
+ +### πŸ“§ Your Personal Email Secretary: RAG on Apple Mail! + +> **Note:** The examples below currently support macOS only. Windows support coming soon. + + +

+ LEANN Email Search Demo +

+ +Before running the example below, you need to grant full disk access to your terminal/VS Code in System Preferences β†’ Privacy & Security β†’ Full Disk Access. + +```bash +python -m apps.email_rag --query "What's the food I ordered by DoorDash or Uber Eats mostly?" +``` +**780K email chunks β†’ 78MB storage.** Finally, search your email like you search Google. + +
+πŸ“‹ Click to expand: Email-Specific Arguments + +#### Parameters +```bash +--mail-path PATH # Path to specific mail directory (auto-detects if omitted) +--include-html # Include HTML content in processing (useful for newsletters) +``` + +#### Example Commands +```bash +# Search work emails from a specific account +python -m apps.email_rag --mail-path "~/Library/Mail/V10/WORK_ACCOUNT" + +# Find all receipts and order confirmations (includes HTML) +python -m apps.email_rag --query "receipt order confirmation invoice" --include-html +``` + +
+ +
+πŸ“‹ Click to expand: Example queries you can try + +Once the index is built, you can ask questions like: +- "Find emails from my boss about deadlines" +- "What did John say about the project timeline?" +- "Show me emails about travel expenses" +
+ +### πŸ” Time Machine for the Web: RAG Your Entire Chrome Browser History! + +

+ LEANN Browser History Search Demo +

+ +```bash +python -m apps.browser_rag --query "Tell me my browser history about machine learning?" +``` +**38K browser entries β†’ 6MB storage.** Your browser history becomes your personal search engine. + +
+πŸ“‹ Click to expand: Browser-Specific Arguments + +#### Parameters +```bash +--chrome-profile PATH # Path to Chrome profile directory (auto-detects if omitted) +``` + +#### Example Commands +```bash +# Search academic research from your browsing history +python -m apps.browser_rag --query "arxiv papers machine learning transformer architecture" + +# Track competitor analysis across work profile +python -m apps.browser_rag --chrome-profile "~/Library/Application Support/Google/Chrome/Work Profile" --max-items 5000 +``` + +
+ +
+πŸ“‹ Click to expand: How to find your Chrome profile + +The default Chrome profile path is configured for a typical macOS setup. If you need to find your specific Chrome profile: + +1. Open Terminal +2. Run: `ls ~/Library/Application\ Support/Google/Chrome/` +3. Look for folders like "Default", "Profile 1", "Profile 2", etc. +4. Use the full path as your `--chrome-profile` argument + +**Common Chrome profile locations:** +- macOS: `~/Library/Application Support/Google/Chrome/Default` +- Linux: `~/.config/google-chrome/Default` + +
+ +
+πŸ’¬ Click to expand: Example queries you can try + +Once the index is built, you can ask questions like: + +- "What websites did I visit about machine learning?" +- "Find my search history about programming" +- "What YouTube videos did I watch recently?" +- "Show me websites I visited about travel planning" + +
+ +### πŸ’¬ WeChat Detective: Unlock Your Golden Memories! + +

+ LEANN WeChat Search Demo +

+ +```bash +python -m apps.wechat_rag --query "Show me all group chats about weekend plans" +``` +**400K messages β†’ 64MB storage** Search years of chat history in any language. + + +
+πŸ”§ Click to expand: Installation Requirements + +First, you need to install the [WeChat exporter](https://github.com/sunnyyoung/WeChatTweak-CLI), + +```bash +brew install sunnyyoung/repo/wechattweak-cli +``` + +or install it manually (if you have issues with Homebrew): + +```bash +sudo packages/wechat-exporter/wechattweak-cli install +``` + +**Troubleshooting:** +- **Installation issues**: Check the [WeChatTweak-CLI issues page](https://github.com/sunnyyoung/WeChatTweak-CLI/issues/41) +- **Export errors**: If you encounter the error below, try restarting WeChat + ```bash + Failed to export WeChat data. Please ensure WeChat is running and WeChatTweak is installed. + Failed to find or export WeChat data. Exiting. + ``` +
+ +
+πŸ“‹ Click to expand: WeChat-Specific Arguments + +#### Parameters +```bash +--export-dir DIR # Directory to store exported WeChat data (default: wechat_export_direct) +--force-export # Force re-export even if data exists +``` + +#### Example Commands +```bash +# Search for travel plans discussed in group chats +python -m apps.wechat_rag --query "travel plans" --max-items 10000 + +# Re-export and search recent chats (useful after new messages) +python -m apps.wechat_rag --force-export --query "work schedule" +``` + +
+ +
+πŸ’¬ Click to expand: Example queries you can try + +Once the index is built, you can ask questions like: + +- "ζˆ‘ζƒ³δΉ°ι­”ζœ―εΈˆηΊ¦ηΏ°ι€Šηš„ηƒθ‘£οΌŒη»™ζˆ‘δΈ€δΊ›ε―ΉεΊ”θŠε€©θ°ε½•?" (Chinese: Show me chat records about buying Magic Johnson's jersey) + +
+ +### πŸ€– ChatGPT Chat History: Your Personal AI Conversation Archive! + +Transform your ChatGPT conversations into a searchable knowledge base! Search through all your ChatGPT discussions about coding, research, brainstorming, and more. + +```bash +python -m apps.chatgpt_rag --export-path chatgpt_export.html --query "How do I create a list in Python?" +``` + +**Unlock your AI conversation history.** Never lose track of valuable insights from your ChatGPT discussions again. + +
+πŸ“‹ Click to expand: How to Export ChatGPT Data + +**Step-by-step export process:** + +1. **Sign in to ChatGPT** +2. **Click your profile icon** in the top right corner +3. **Navigate to Settings** β†’ **Data Controls** +4. **Click "Export"** under Export Data +5. **Confirm the export** request +6. **Download the ZIP file** from the email link (expires in 24 hours) +7. **Extract or use directly** with LEANN + +**Supported formats:** +- `.html` files from ChatGPT exports +- `.zip` archives from ChatGPT +- Directories with multiple export files + +
+ +
+πŸ“‹ Click to expand: ChatGPT-Specific Arguments + +#### Parameters +```bash +--export-path PATH # Path to ChatGPT export file (.html/.zip) or directory (default: ./chatgpt_export) +--separate-messages # Process each message separately instead of concatenated conversations +--chunk-size N # Text chunk size (default: 512) +--chunk-overlap N # Overlap between chunks (default: 128) +``` + +#### Example Commands +```bash +# Basic usage with HTML export +python -m apps.chatgpt_rag --export-path conversations.html + +# Process ZIP archive from ChatGPT +python -m apps.chatgpt_rag --export-path chatgpt_export.zip + +# Search with specific query +python -m apps.chatgpt_rag --export-path chatgpt_data.html --query "Python programming help" + +# Process individual messages for fine-grained search +python -m apps.chatgpt_rag --separate-messages --export-path chatgpt_export.html + +# Process directory containing multiple exports +python -m apps.chatgpt_rag --export-path ./chatgpt_exports/ --max-items 1000 +``` + +
+ +
+πŸ’‘ Click to expand: Example queries you can try + +Once your ChatGPT conversations are indexed, you can search with queries like: +- "What did I ask ChatGPT about Python programming?" +- "Show me conversations about machine learning algorithms" +- "Find discussions about web development frameworks" +- "What coding advice did ChatGPT give me?" +- "Search for conversations about debugging techniques" +- "Find ChatGPT's recommendations for learning resources" + +
+ +### πŸ€– Claude Chat History: Your Personal AI Conversation Archive! + +Transform your Claude conversations into a searchable knowledge base! Search through all your Claude discussions about coding, research, brainstorming, and more. + +```bash +python -m apps.claude_rag --export-path claude_export.json --query "What did I ask about Python dictionaries?" +``` + +**Unlock your AI conversation history.** Never lose track of valuable insights from your Claude discussions again. + +
+πŸ“‹ Click to expand: How to Export Claude Data + +**Step-by-step export process:** + +1. **Open Claude** in your browser +2. **Navigate to Settings** (look for gear icon or settings menu) +3. **Find Export/Download** options in your account settings +4. **Download conversation data** (usually in JSON format) +5. **Place the file** in your project directory + +*Note: Claude export methods may vary depending on the interface you're using. Check Claude's help documentation for the most current export instructions.* + +**Supported formats:** +- `.json` files (recommended) +- `.zip` archives containing JSON data +- Directories with multiple export files + +
+ +
+πŸ“‹ Click to expand: Claude-Specific Arguments + +#### Parameters +```bash +--export-path PATH # Path to Claude export file (.json/.zip) or directory (default: ./claude_export) +--separate-messages # Process each message separately instead of concatenated conversations +--chunk-size N # Text chunk size (default: 512) +--chunk-overlap N # Overlap between chunks (default: 128) +``` + +#### Example Commands +```bash +# Basic usage with JSON export +python -m apps.claude_rag --export-path my_claude_conversations.json + +# Process ZIP archive from Claude +python -m apps.claude_rag --export-path claude_export.zip + +# Search with specific query +python -m apps.claude_rag --export-path claude_data.json --query "machine learning advice" + +# Process individual messages for fine-grained search +python -m apps.claude_rag --separate-messages --export-path claude_export.json + +# Process directory containing multiple exports +python -m apps.claude_rag --export-path ./claude_exports/ --max-items 1000 +``` + +
+ +
+πŸ’‘ Click to expand: Example queries you can try + +Once your Claude conversations are indexed, you can search with queries like: +- "What did I ask Claude about Python programming?" +- "Show me conversations about machine learning algorithms" +- "Find discussions about software architecture patterns" +- "What debugging advice did Claude give me?" +- "Search for conversations about data structures" +- "Find Claude's recommendations for learning resources" + +
+ +### πŸ’¬ iMessage History: Your Personal Conversation Archive! + +Transform your iMessage conversations into a searchable knowledge base! Search through all your text messages, group chats, and conversations with friends, family, and colleagues. + +```bash +python -m apps.imessage_rag --query "What did we discuss about the weekend plans?" +``` + +**Unlock your message history.** Never lose track of important conversations, shared links, or memorable moments from your iMessage history. + +
+πŸ“‹ Click to expand: How to Access iMessage Data + +**iMessage data location:** + +iMessage conversations are stored in a SQLite database on your Mac at: +``` +~/Library/Messages/chat.db +``` + +**Important setup requirements:** + +1. **Grant Full Disk Access** to your terminal or IDE: + - Open **System Preferences** β†’ **Security & Privacy** β†’ **Privacy** + - Select **Full Disk Access** from the left sidebar + - Click the **+** button and add your terminal app (Terminal, iTerm2) or IDE (VS Code, etc.) + - Restart your terminal/IDE after granting access + +2. **Alternative: Use a backup database** + - If you have Time Machine backups or manual copies of the database + - Use `--db-path` to specify a custom location + +**Supported formats:** +- Direct access to `~/Library/Messages/chat.db` (default) +- Custom database path with `--db-path` +- Works with backup copies of the database + +
+ +
+πŸ“‹ Click to expand: iMessage-Specific Arguments + +#### Parameters +```bash +--db-path PATH # Path to chat.db file (default: ~/Library/Messages/chat.db) +--concatenate-conversations # Group messages by conversation (default: True) +--no-concatenate-conversations # Process each message individually +--chunk-size N # Text chunk size (default: 1000) +--chunk-overlap N # Overlap between chunks (default: 200) +``` + +#### Example Commands +```bash +# Basic usage (requires Full Disk Access) +python -m apps.imessage_rag + +# Search with specific query +python -m apps.imessage_rag --query "family dinner plans" + +# Use custom database path +python -m apps.imessage_rag --db-path /path/to/backup/chat.db + +# Process individual messages instead of conversations +python -m apps.imessage_rag --no-concatenate-conversations + +# Limit processing for testing +python -m apps.imessage_rag --max-items 100 --query "weekend" +``` + +
+ +
+πŸ’‘ Click to expand: Example queries you can try + +Once your iMessage conversations are indexed, you can search with queries like: +- "What did we discuss about vacation plans?" +- "Find messages about restaurant recommendations" +- "Show me conversations with John about the project" +- "Search for shared links about technology" +- "Find group chat discussions about weekend events" +- "What did mom say about the family gathering?" + +
+ +### MCP Integration: RAG on Live Data from Any Platform + +Connect to live data sources through the Model Context Protocol (MCP). LEANN now supports real-time RAG on platforms like Slack, Twitter, and more through standardized MCP servers. + +**Key Benefits:** +- **Live Data Access**: Fetch real-time data without manual exports +- **Standardized Protocol**: Use any MCP-compatible server +- **Easy Extension**: Add new platforms with minimal code +- **Secure Access**: MCP servers handle authentication + +#### πŸ’¬ Slack Messages: Search Your Team Conversations + +Transform your Slack workspace into a searchable knowledge base! Find discussions, decisions, and shared knowledge across all your channels. + +```bash +# Test MCP server connection +python -m apps.slack_rag --mcp-server "slack-mcp-server" --test-connection + +# Index and search Slack messages +python -m apps.slack_rag \ + --mcp-server "slack-mcp-server" \ + --workspace-name "my-team" \ + --channels general dev-team random \ + --query "What did we decide about the product launch?" +``` + +**πŸ“– Comprehensive Setup Guide**: For detailed setup instructions, troubleshooting common issues (like "users cache is not ready yet"), and advanced configuration options, see our [**Slack Setup Guide**](docs/slack-setup-guide.md). + +**Quick Setup:** +1. Install a Slack MCP server (e.g., `npm install -g slack-mcp-server`) +2. Create a Slack App and get API credentials (see detailed guide above) +3. Set environment variables: + ```bash + export SLACK_BOT_TOKEN="xoxb-your-bot-token" + export SLACK_APP_TOKEN="xapp-your-app-token" # Optional + ``` +4. Test connection with `--test-connection` flag + +**Arguments:** +- `--mcp-server`: Command to start the Slack MCP server +- `--workspace-name`: Slack workspace name for organization +- `--channels`: Specific channels to index (optional) +- `--concatenate-conversations`: Group messages by channel (default: true) +- `--max-messages-per-channel`: Limit messages per channel (default: 100) +- `--max-retries`: Maximum retries for cache sync issues (default: 5) +- `--retry-delay`: Initial delay between retries in seconds (default: 2.0) + +#### 🐦 Twitter Bookmarks: Your Personal Tweet Library + +Search through your Twitter bookmarks! Find that perfect article, thread, or insight you saved for later. + +```bash +# Test MCP server connection +python -m apps.twitter_rag --mcp-server "twitter-mcp-server" --test-connection + +# Index and search Twitter bookmarks +python -m apps.twitter_rag \ + --mcp-server "twitter-mcp-server" \ + --max-bookmarks 1000 \ + --query "What AI articles did I bookmark about machine learning?" +``` + +**Setup Requirements:** +1. Install a Twitter MCP server (e.g., `npm install -g twitter-mcp-server`) +2. Get Twitter API credentials: + - Apply for a Twitter Developer Account at [developer.twitter.com](https://developer.twitter.com) + - Create a new app in the Twitter Developer Portal + - Generate API keys and access tokens with "Read" permissions + - For bookmarks access, you may need Twitter API v2 with appropriate scopes + ```bash + export TWITTER_API_KEY="your-api-key" + export TWITTER_API_SECRET="your-api-secret" + export TWITTER_ACCESS_TOKEN="your-access-token" + export TWITTER_ACCESS_TOKEN_SECRET="your-access-token-secret" + ``` +3. Test connection with `--test-connection` flag + +**Arguments:** +- `--mcp-server`: Command to start the Twitter MCP server +- `--username`: Filter bookmarks by username (optional) +- `--max-bookmarks`: Maximum bookmarks to fetch (default: 1000) +- `--no-tweet-content`: Exclude tweet content, only metadata +- `--no-metadata`: Exclude engagement metadata + + + +
+πŸ’‘ Click to expand: Example queries you can try + +**Slack Queries:** +- "What did the team discuss about the project deadline?" +- "Find messages about the new feature launch" +- "Show me conversations about budget planning" +- "What decisions were made in the dev-team channel?" + +**Twitter Queries:** +- "What AI articles did I bookmark last month?" +- "Find tweets about machine learning techniques" +- "Show me bookmarked threads about startup advice" +- "What Python tutorials did I save?" + +
+πŸ”§ Using MCP with CLI Commands + +**Want to use MCP data with regular LEANN CLI?** You can combine MCP apps with CLI commands: + +```bash +# Step 1: Use MCP app to fetch and index data +python -m apps.slack_rag --mcp-server "slack-mcp-server" --workspace-name "my-team" + +# Step 2: The data is now indexed and available via CLI +leann search slack_messages "project deadline" +leann ask slack_messages "What decisions were made about the product launch?" + +# Same for Twitter bookmarks +python -m apps.twitter_rag --mcp-server "twitter-mcp-server" +leann search twitter_bookmarks "machine learning articles" +``` + +**MCP vs Manual Export:** +- **MCP**: Live data, automatic updates, requires server setup +- **Manual Export**: One-time setup, works offline, requires manual data export + + + +
+πŸ”§ Adding New MCP Platforms + +Want to add support for other platforms? LEANN's MCP integration is designed for easy extension: + +1. **Find or create an MCP server** for your platform +2. **Create a reader class** following the pattern in `apps/slack_data/slack_mcp_reader.py` +3. **Create a RAG application** following the pattern in `apps/slack_rag.py` +4. **Test and contribute** back to the community! + +**Popular MCP servers to explore:** +- GitHub repositories and issues +- Discord messages +- Notion pages +- Google Drive documents +- And many more in the MCP ecosystem! + +
+ +### πŸš€ Claude Code Integration: Transform Your Development Workflow! + +
+AST‑Aware Code Chunking + +LEANN features intelligent code chunking that preserves semantic boundaries (functions, classes, methods) for Python, Java, C#, and TypeScript, improving code understanding compared to text-based chunking. + +πŸ“– Read the [AST Chunking Guide β†’](docs/ast_chunking_guide.md) + +
+ +**The future of code assistance is here.** Transform your development workflow with LEANN's native MCP integration for Claude Code. Index your entire codebase and get intelligent code assistance directly in your IDE. + +**Key features:** +- πŸ” **Semantic code search** across your entire project, fully local index and lightweight +- 🧠 **AST-aware chunking** preserves code structure (functions, classes) +- πŸ“š **Context-aware assistance** for debugging and development +- πŸš€ **Zero-config setup** with automatic language detection + +```bash +# Install LEANN globally for MCP integration +uv tool install leann-core --with leann +claude mcp add --scope user leann-server -- leann_mcp +# Setup is automatic - just start using Claude Code! +``` +Try our fully agentic pipeline with auto query rewriting, semantic search planning, and more: + +![LEANN MCP Integration](assets/mcp_leann.png) + +**πŸ”₯ Ready to supercharge your coding?** [Complete Setup Guide β†’](packages/leann-mcp/README.md) + +## Command Line Interface + +LEANN includes a powerful CLI for document processing and search. Perfect for quick document indexing and interactive chat. + +### Installation + +If you followed the Quick Start, `leann` is already installed in your virtual environment: +```bash +source .venv/bin/activate +leann --help +``` + +**To make it globally available:** +```bash +# Install the LEANN CLI globally using uv tool +uv tool install leann-core --with leann + + +# Now you can use leann from anywhere without activating venv +leann --help +``` + +> **Note**: Global installation is required for Claude Code integration. The `leann_mcp` server depends on the globally available `leann` command. + + + +### Usage Examples + +```bash +# build from a specific directory, and my_docs is the index name(Here you can also build from multiple dict or multiple files) +leann build my-docs --docs ./your_documents + +# Search your documents +leann search my-docs "machine learning concepts" + +# Interactive chat with your documents +leann ask my-docs --interactive + +# Ask a single question (non-interactive) +leann ask my-docs "Where are prompts configured?" + +# Detect file changes since last build/watch checkpoint +leann watch my-docs + +# List all your indexes +leann list + +# Remove an index +leann remove my-docs +``` + +**Key CLI features:** +- Auto-detects document formats (PDF, TXT, MD, DOCX, PPTX + code files) +- **🧠 AST-aware chunking** for Python, Java, C#, TypeScript files +- Smart text chunking with overlap for all other content +- **πŸ“‚ File change detection** via Merkle tree snapshots (`leann watch`) +- Multiple LLM providers (Ollama, OpenAI, HuggingFace) +- Organized index storage in `.leann/indexes/` (project-local) +- Support for advanced search parameters + +
+πŸ“‹ Click to expand: Complete CLI Reference + +You can use `leann --help`, or `leann build --help`, `leann search --help`, `leann watch --help`, `leann ask --help`, `leann list --help`, `leann remove --help` to get the complete CLI reference. + +**Build Command:** +```bash +leann build INDEX_NAME --docs DIRECTORY|FILE [DIRECTORY|FILE ...] [OPTIONS] + +Options: + --backend {hnsw,diskann} Backend to use (default: hnsw) + --embedding-model MODEL Embedding model (default: facebook/contriever) + --graph-degree N Graph degree (default: 32) + --complexity N Build complexity (default: 64) + --force Force rebuild existing index + --compact / --no-compact Use compact storage (default: true). Must be `no-compact` for `no-recompute` build. + --recompute / --no-recompute Enable recomputation (default: true) +``` + +**Search Command:** +```bash +leann search INDEX_NAME QUERY [OPTIONS] + +Options: + --top-k N Number of results (default: 5) + --complexity N Search complexity (default: 64) + --recompute / --no-recompute Enable/disable embedding recomputation (default: enabled). Should not do a `no-recompute` search in a `recompute` build. + --pruning-strategy {global,local,proportional} +``` + +**Watch Command:** +```bash +leann watch INDEX_NAME + +# Compares the current file system state against the last checkpoint (Merkle tree snapshot) +# and reports which files have been added, removed, or modified, along with their chunk IDs. +# +# - Automatically saves a new checkpoint after detecting changes +# - Each subsequent run compares against the most recent checkpoint +# - File change detection uses SHA-256 content hashing via a Merkle tree +# +# Example output: +# === Changes since last checkpoint === +# modified (1): +# - /path/to/file.py +# chunks: 42, 43, 44 +``` + +**Ask Command:** +```bash +leann ask INDEX_NAME [OPTIONS] + +Options: + --llm {ollama,openai,hf,anthropic} LLM provider (default: ollama) + --model MODEL Model name (default: qwen3:8b) + --interactive Interactive chat mode + --top-k N Retrieval count (default: 20) +``` + +**List Command:** +```bash +leann list + +# Lists all indexes across all projects with status indicators: +# βœ… - Index is complete and ready to use +# ❌ - Index is incomplete or corrupted +# πŸ“ - CLI-created index (in .leann/indexes/) +# πŸ“„ - App-created index (*.leann.meta.json files) +``` + +**Remove Command:** +```bash +leann remove INDEX_NAME [OPTIONS] + +Options: + --force, -f Force removal without confirmation + +# Smart removal: automatically finds and safely removes indexes +# - Shows all matching indexes across projects +# - Requires confirmation for cross-project removal +# - Interactive selection when multiple matches found +# - Supports both CLI and app-created indexes +``` + +
+ +## πŸš€ Advanced Features + +### 🎯 Metadata Filtering + +LEANN supports a simple metadata filtering system to enable sophisticated use cases like document filtering by date/type, code search by file extension, and content management based on custom criteria. + +```python +# Add metadata during indexing +builder.add_text( + "def authenticate_user(token): ...", + metadata={"file_extension": ".py", "lines_of_code": 25} +) + +# Search with filters +results = searcher.search( + query="authentication function", + metadata_filters={ + "file_extension": {"==": ".py"}, + "lines_of_code": {"<": 100} + } +) +``` + +**Supported operators**: `==`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `not_in`, `contains`, `starts_with`, `ends_with`, `is_true`, `is_false` + +πŸ“– **[Complete Metadata filtering guide β†’](docs/metadata_filtering.md)** + +### πŸ” Grep Search + +For exact text matching instead of semantic search, use the `use_grep` parameter: + +```python +# Exact text search +results = searcher.search("banana‑crocodile", use_grep=True, top_k=1) +``` + +**Use cases**: Finding specific code patterns, error messages, function names, or exact phrases where semantic similarity isn't needed. + +πŸ“– **[Complete grep search guide β†’](docs/grep_search.md)** + +## πŸ—οΈ Architecture & How It Works + +

+ LEANN Architecture +

+ +**The magic:** Most vector DBs store every single embedding (expensive). LEANN stores a pruned graph structure (cheap) and recomputes embeddings only when needed (fast). + +**Core techniques:** +- **Graph-based selective recomputation:** Only compute embeddings for nodes in the search path +- **High-degree preserving pruning:** Keep important "hub" nodes while removing redundant connections +- **Dynamic batching:** Efficiently batch embedding computations for GPU utilization +- **Two-level search:** Smart graph traversal that prioritizes promising nodes + +**Backends:** +- **HNSW** (default): Ideal for most datasets with maximum storage savings through full recomputation +- **DiskANN**: Advanced option with superior search performance, using PQ-based graph traversal with real-time reranking for the best speed-accuracy trade-off + +## Benchmarks + +**[DiskANN vs HNSW Performance Comparison β†’](benchmarks/diskann_vs_hnsw_speed_comparison.py)** - Compare search performance between both backends + +**[Simple Example: Compare LEANN vs FAISS β†’](benchmarks/compare_faiss_vs_leann.py)** - See storage savings in action + +### πŸ“Š Storage Comparison + +| System | DPR (2.1M) | Wiki (60M) | Chat (400K) | Email (780K) | Browser (38K) | +|--------|-------------|------------|-------------|--------------|---------------| +| Traditional vector database (e.g., FAISS) | 3.8 GB | 201 GB | 1.8 GB | 2.4 GB | 130 MB | +| LEANN | 324 MB | 6 GB | 64 MB | 79 MB | 6.4 MB | +| Savings| 91% | 97% | 97% | 97% | 95% | + + + +## Reproduce Our Results + +```bash +uv run benchmarks/run_evaluation.py # Will auto-download evaluation data and run benchmarks +uv run benchmarks/run_evaluation.py benchmarks/data/indices/rpj_wiki/rpj_wiki --num-queries 2000 # After downloading data, you can run the benchmark with our biggest index +``` + +The evaluation script downloads data automatically on first run. The last three results were tested with partial personal data, and you can reproduce them with your own data! +## πŸ”¬ Paper + +If you find Leann useful, please cite: + +**[LEANN: A Low-Storage Vector Index](https://arxiv.org/abs/2506.08276)** + +```bibtex +@misc{wang2025leannlowstoragevectorindex, + title={LEANN: A Low-Storage Vector Index}, + author={Yichuan Wang and Shu Liu and Zhifei Li and Yongji Wu and Ziming Mao and Yilong Zhao and Xiao Yan and Zhiying Xu and Yang Zhou and Ion Stoica and Sewon Min and Matei Zaharia and Joseph E. Gonzalez}, + year={2025}, + eprint={2506.08276}, + archivePrefix={arXiv}, + primaryClass={cs.DB}, + url={https://arxiv.org/abs/2506.08276}, +} +``` + +## ✨ [Detailed Features β†’](docs/features.md) + +## 🀝 [CONTRIBUTING β†’](docs/CONTRIBUTING.md) + + +## ❓ [FAQ β†’](docs/faq.md) + + +## πŸ“ˆ [Roadmap β†’](docs/roadmap.md) + +## πŸ“„ License + +MIT License - see [LICENSE](LICENSE) for details. + +## πŸ™ Acknowledgments + +Core Contributors: [Yichuan Wang](https://yichuan-w.github.io/) & [Zhifei Li](https://github.com/andylizf). + +Active Contributors: [Gabriel Dehan](https://github.com/gabriel-dehan), [Aakash Suresh](https://github.com/ASuresh0524) + + +We welcome more contributors! Feel free to open issues or submit PRs. + +This work is done at [**Berkeley Sky Computing Lab**](https://sky.cs.berkeley.edu/). + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=yichuan-w/LEANN&type=Date)](https://www.star-history.com/#yichuan-w/LEANN&Date) +

+ ⭐ Star us on GitHub if Leann is useful for your research or applications! +

+ +

+ Made with ❀️ by the Leann team +

+ +## πŸ€– Explore LEANN with AI + +LEANN is indexed on [DeepWiki](https://deepwiki.com/yichuan-w/LEANN), so you can ask questions to LLMs using Deep Research to explore the codebase and get help to add new features. diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..404be51 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub ζ₯源说明 + +- εŽŸε§‹ι‘Ήη›οΌš`StarTrail-org/LEANN` +- εŽŸε§‹δ»“εΊ“οΌšhttps://github.com/StarTrail-org/LEANN +- ε―Όε…₯ζ–ΉεΌοΌšδΈŠζΈΈι»˜θ€εˆ†ζ”―ηš„ζœ€ζ–°εΏ«η…§ +- εŽŸδ½œθ€…γ€η‰ˆζƒε’ŒθΈε―证俑息δ»₯εŽŸε§‹δ»“εΊ“εŠζœ¬δ»“εΊ“ LICENSE 为准 +- ζœ¬ζ–‡δ»Άδ»…η”¨δΊŽθ°ε½•ζ₯源,不代葨 WeHub 是原鑹η›δ½œθ€… diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/base_rag_example.py b/apps/base_rag_example.py new file mode 100644 index 0000000..67e198b --- /dev/null +++ b/apps/base_rag_example.py @@ -0,0 +1,440 @@ +""" +Base class for unified RAG examples interface. +Provides common parameters and functionality for all RAG examples. +""" + +import argparse +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any + +import dotenv +from leann.api import LeannBuilder, LeannChat + +# Optional import: older PyPI builds may not include interactive_utils +try: + from leann.interactive_utils import create_rag_session +except ImportError: + + def create_rag_session(app_name: str, data_description: str): + class _SimpleSession: + def run_interactive_loop(self, handler): + print(f"Interactive session for {app_name}: {data_description}") + print("Interactive mode not available in this build") + + return _SimpleSession() + + +from leann.registry import register_project_directory + +# Optional import: older PyPI builds may not include settings +try: + from leann.settings import resolve_ollama_host, resolve_openai_api_key, resolve_openai_base_url +except ImportError: + # Minimal fallbacks if settings helpers are unavailable + import os + + def resolve_ollama_host(value: str | None) -> str | None: + return value or os.getenv("LEANN_OLLAMA_HOST") or os.getenv("OLLAMA_HOST") + + def resolve_openai_api_key(value: str | None) -> str | None: + return value or os.getenv("OPENAI_API_KEY") + + def resolve_openai_base_url(value: str | None) -> str | None: + return value or os.getenv("OPENAI_BASE_URL") + + +dotenv.load_dotenv() + + +class BaseRAGExample(ABC): + """Base class for all RAG examples with unified interface.""" + + def __init__( + self, + name: str, + description: str, + default_index_name: str, + ): + self.name = name + self.description = description + self.default_index_name = default_index_name + self.parser = self._create_parser() + + def _create_parser(self) -> argparse.ArgumentParser: + """Create argument parser with common parameters.""" + parser = argparse.ArgumentParser( + description=self.description, formatter_class=argparse.RawDescriptionHelpFormatter + ) + + # Core parameters (all examples share these) + core_group = parser.add_argument_group("Core Parameters") + core_group.add_argument( + "--index-dir", + type=str, + default=f"./{self.default_index_name}", + help=f"Directory to store the index (default: ./{self.default_index_name})", + ) + core_group.add_argument( + "--query", + type=str, + default=None, + help="Query to run (if not provided, will run in interactive mode)", + ) + # Allow subclasses to override default max_items + max_items_default = getattr(self, "max_items_default", -1) + core_group.add_argument( + "--max-items", + type=int, + default=max_items_default, + help="Maximum number of items to process -1 for all, means index all documents, and you should set it to a reasonable number if you have a large dataset and try at the first time)", + ) + core_group.add_argument( + "--force-rebuild", action="store_true", help="Force rebuild index even if it exists" + ) + + # Embedding parameters + embedding_group = parser.add_argument_group("Embedding Parameters") + # Allow subclasses to override default embedding_model + embedding_model_default = getattr(self, "embedding_model_default", "facebook/contriever") + embedding_group.add_argument( + "--embedding-model", + type=str, + default=embedding_model_default, + help=f"Embedding model to use (default: {embedding_model_default}), we provide facebook/contriever, text-embedding-3-small,mlx-community/Qwen3-Embedding-0.6B-8bit or nomic-embed-text", + ) + embedding_group.add_argument( + "--embedding-mode", + type=str, + default="sentence-transformers", + choices=["sentence-transformers", "openai", "mlx", "ollama"], + help="Embedding backend mode (default: sentence-transformers), we provide sentence-transformers, openai, mlx, or ollama", + ) + embedding_group.add_argument( + "--embedding-host", + type=str, + default=None, + help="Override Ollama-compatible embedding host", + ) + embedding_group.add_argument( + "--embedding-api-base", + type=str, + default=None, + help="Base URL for OpenAI-compatible embedding services", + ) + embedding_group.add_argument( + "--embedding-api-key", + type=str, + default=None, + help="API key for embedding service (defaults to OPENAI_API_KEY)", + ) + + # LLM parameters + llm_group = parser.add_argument_group("LLM Parameters") + llm_group.add_argument( + "--llm", + type=str, + default="openai", + choices=["openai", "ollama", "hf", "simulated"], + help="LLM backend: openai, ollama, or hf (default: openai)", + ) + llm_group.add_argument( + "--llm-model", + type=str, + default=None, + help="Model name (default: gpt-4o) e.g., gpt-4o-mini, llama3.2:1b, Qwen/Qwen2.5-1.5B-Instruct", + ) + llm_group.add_argument( + "--llm-host", + type=str, + default=None, + help="Host for Ollama-compatible APIs (defaults to LEANN_OLLAMA_HOST/OLLAMA_HOST)", + ) + llm_group.add_argument( + "--thinking-budget", + type=str, + choices=["low", "medium", "high"], + default=None, + help="Thinking budget for reasoning models (low/medium/high). Supported by GPT-Oss:20b and other reasoning models.", + ) + llm_group.add_argument( + "--llm-api-base", + type=str, + default=None, + help="Base URL for OpenAI-compatible APIs", + ) + llm_group.add_argument( + "--llm-api-key", + type=str, + default=None, + help="API key for OpenAI-compatible APIs (defaults to OPENAI_API_KEY)", + ) + + # AST Chunking parameters + ast_group = parser.add_argument_group("AST Chunking Parameters") + ast_group.add_argument( + "--use-ast-chunking", + action="store_true", + help="Enable AST-aware chunking for code files (requires astchunk)", + ) + ast_group.add_argument( + "--ast-chunk-size", + type=int, + default=300, + help="Maximum CHARACTERS per AST chunk (default: 300). Final chunks may be larger due to overlap. For 512 token models: recommended 300 chars", + ) + ast_group.add_argument( + "--ast-chunk-overlap", + type=int, + default=64, + help="Overlap between AST chunks in CHARACTERS (default: 64). Added to chunk size, not included in it", + ) + ast_group.add_argument( + "--code-file-extensions", + nargs="+", + default=None, + help="Additional code file extensions to process with AST chunking (e.g., .py .java .cs .ts)", + ) + ast_group.add_argument( + "--ast-fallback-traditional", + action="store_true", + default=True, + help="Fall back to traditional chunking if AST chunking fails (default: True)", + ) + + # Search parameters + search_group = parser.add_argument_group("Search Parameters") + search_group.add_argument( + "--top-k", type=int, default=20, help="Number of results to retrieve (default: 20)" + ) + search_group.add_argument( + "--search-complexity", + type=int, + default=32, + help="Search complexity for graph traversal (default: 64)", + ) + + # Index building parameters + index_group = parser.add_argument_group("Index Building Parameters") + index_group.add_argument( + "--backend-name", + type=str, + default="hnsw", + choices=["hnsw", "diskann", "ivf", "flashlib"], + help="Backend to use for index (default: hnsw). 'flashlib' requires a CUDA GPU.", + ) + index_group.add_argument( + "--graph-degree", + type=int, + default=32, + help="Graph degree for index construction (default: 32)", + ) + index_group.add_argument( + "--build-complexity", + type=int, + default=64, + help="Build complexity for index construction (default: 64)", + ) + index_group.add_argument( + "--no-compact", + action="store_true", + help="Disable compact index storage", + ) + index_group.add_argument( + "--no-recompute", + action="store_true", + help="Disable embedding recomputation", + ) + + # Add source-specific parameters + self._add_specific_arguments(parser) + + return parser + + @abstractmethod + def _add_specific_arguments(self, parser: argparse.ArgumentParser): + """Add source-specific arguments. Override in subclasses.""" + pass + + @abstractmethod + async def load_data(self, args) -> list[dict[str, Any]]: + """Load data from the source. Returns list of text chunks as dicts with 'text' and 'metadata' keys.""" + pass + + def get_llm_config(self, args) -> dict[str, Any]: + """Get LLM configuration based on arguments.""" + config = {"type": args.llm} + + if args.llm == "openai": + config["model"] = args.llm_model or "gpt-4o" + config["base_url"] = resolve_openai_base_url(args.llm_api_base) + resolved_key = resolve_openai_api_key(args.llm_api_key) + if resolved_key: + config["api_key"] = resolved_key + elif args.llm == "ollama": + config["model"] = args.llm_model or "llama3.2:1b" + config["host"] = resolve_ollama_host(args.llm_host) + elif args.llm == "hf": + config["model"] = args.llm_model or "Qwen/Qwen2.5-1.5B-Instruct" + elif args.llm == "simulated": + # Simulated LLM doesn't need additional configuration + pass + + return config + + @staticmethod + def _resolve_chunk_token_limit(args) -> int | None: + """Resolve the embedding model's token limit for token-aware chunking. + + Returns ``None`` if the limit cannot be determined (e.g. model unknown). + Apps can pass the result as ``max_tokens_per_chunk=`` to + ``create_text_chunks()``. + """ + try: + from leann.embedding_compute import get_model_token_limit + + base_url = getattr(args, "embedding_api_base", None) + return get_model_token_limit(args.embedding_model, base_url) + except Exception: + return None + + async def build_index(self, args, texts: list[dict[str, Any]]) -> str: + """Build LEANN index from text chunks (dicts with 'text' and 'metadata' keys).""" + index_path = str(Path(args.index_dir) / f"{self.default_index_name}.leann") + + print(f"\n[Building Index] Creating {self.name} index...") + print(f"Total text chunks: {len(texts)}") + + # Warn if any chunks may exceed the embedding model's token limit + limit = self._resolve_chunk_token_limit(args) + if limit: + try: + from leann.chunking_utils import validate_chunk_token_limits + + _texts = [t["text"] if isinstance(t, dict) else t for t in texts] + validate_chunk_token_limits(_texts, limit) + except Exception: + pass + + embedding_options: dict[str, Any] = {} + if args.embedding_mode == "ollama": + embedding_options["host"] = resolve_ollama_host(args.embedding_host) + elif args.embedding_mode == "openai": + embedding_options["base_url"] = resolve_openai_base_url(args.embedding_api_base) + resolved_embedding_key = resolve_openai_api_key(args.embedding_api_key) + if resolved_embedding_key: + embedding_options["api_key"] = resolved_embedding_key + + builder = LeannBuilder( + backend_name=args.backend_name, + embedding_model=args.embedding_model, + embedding_mode=args.embedding_mode, + embedding_options=embedding_options or None, + graph_degree=args.graph_degree, + complexity=args.build_complexity, + is_compact=not args.no_compact, + is_recompute=not args.no_recompute, + num_threads=1, # Force single-threaded mode + ) + + # Add texts in batches for better progress tracking + batch_size = 1000 + for i in range(0, len(texts), batch_size): + batch = texts[i : i + batch_size] + for item in batch: + # Handle both dict format (from create_text_chunks) and plain strings + if isinstance(item, dict): + text = item.get("text", "") + metadata = item.get("metadata") + builder.add_text(text, metadata) + else: + builder.add_text(item) + print(f"Added {min(i + batch_size, len(texts))}/{len(texts)} texts...") + + print("Building index structure...") + builder.build_index(index_path) + print(f"Index saved to: {index_path}") + + # Register project directory so leann list can discover this index + # The index is saved as args.index_dir/index_name.leann + # We want to register the current working directory where the app is run + register_project_directory(Path.cwd()) + + return index_path + + async def run_interactive_chat(self, args, index_path: str): + """Run interactive chat with the index.""" + chat = LeannChat( + index_path, + llm_config=self.get_llm_config(args), + system_prompt=f"You are a helpful assistant that answers questions about {self.name} data.", + complexity=args.search_complexity, + ) + + # Create interactive session + session = create_rag_session( + app_name=self.name.lower().replace(" ", "_"), data_description=self.name + ) + + def handle_query(query: str): + # Prepare LLM kwargs with thinking budget if specified + llm_kwargs = {} + if hasattr(args, "thinking_budget") and args.thinking_budget: + llm_kwargs["thinking_budget"] = args.thinking_budget + + response = chat.ask( + query, + top_k=args.top_k, + complexity=args.search_complexity, + llm_kwargs=llm_kwargs, + ) + print(f"\nAssistant: {response}\n") + + session.run_interactive_loop(handle_query) + + async def run_single_query(self, args, index_path: str, query: str): + """Run a single query against the index.""" + chat = LeannChat( + index_path, + llm_config=self.get_llm_config(args), + complexity=args.search_complexity, + ) + + print(f"\n[Query]: \033[36m{query}\033[0m") + + # Prepare LLM kwargs with thinking budget if specified + llm_kwargs = {} + if hasattr(args, "thinking_budget") and args.thinking_budget: + llm_kwargs["thinking_budget"] = args.thinking_budget + + response = chat.ask( + query, top_k=args.top_k, complexity=args.search_complexity, llm_kwargs=llm_kwargs + ) + print(f"\n[Response]: \033[36m{response}\033[0m") + + async def run(self): + """Main entry point for the example.""" + args = self.parser.parse_args() + + # Check if index exists + index_path = str(Path(args.index_dir) / f"{self.default_index_name}.leann") + index_exists = Path(f"{index_path}.meta.json").exists() + + if not index_exists or args.force_rebuild: + # Load data and build index + print(f"\n{'Rebuilding' if index_exists else 'Building'} index...") + texts = await self.load_data(args) + + if not texts: + print("No data found to index!") + return + + index_path = await self.build_index(args, texts) + else: + print(f"\nUsing existing index in {args.index_dir}") + + # Run query or interactive mode + if args.query: + await self.run_single_query(args, index_path, args.query) + else: + await self.run_interactive_chat(args, index_path) diff --git a/apps/browser_rag.py b/apps/browser_rag.py new file mode 100644 index 0000000..00bb3f5 --- /dev/null +++ b/apps/browser_rag.py @@ -0,0 +1,172 @@ +""" +Browser History RAG example using the unified interface. +Supports Chrome browser history. +""" + +import os +import sys +from pathlib import Path +from typing import Any + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from base_rag_example import BaseRAGExample +from chunking import create_text_chunks + +from .history_data.history import ChromeHistoryReader + + +class BrowserRAG(BaseRAGExample): + """RAG example for Chrome browser history.""" + + def __init__(self): + # Set default values BEFORE calling super().__init__ + self.embedding_model_default = ( + "sentence-transformers/all-MiniLM-L6-v2" # Fast 384-dim model + ) + + super().__init__( + name="Browser History", + description="Process and query Chrome browser history with LEANN", + default_index_name="google_history_index", + ) + + def _add_specific_arguments(self, parser): + """Add browser-specific arguments.""" + browser_group = parser.add_argument_group("Browser Parameters") + browser_group.add_argument( + "--chrome-profile", + type=str, + default=None, + help="Path to Chrome profile directory (auto-detected if not specified)", + ) + browser_group.add_argument( + "--auto-find-profiles", + action="store_true", + default=True, + help="Automatically find all Chrome profiles (default: True)", + ) + browser_group.add_argument( + "--chunk-size", type=int, default=256, help="Text chunk size (default: 256)" + ) + browser_group.add_argument( + "--chunk-overlap", type=int, default=128, help="Text chunk overlap (default: 128)" + ) + + def _get_chrome_base_path(self) -> Path: + """Get the base Chrome profile path based on OS.""" + if sys.platform == "darwin": + return Path.home() / "Library" / "Application Support" / "Google" / "Chrome" + elif sys.platform.startswith("linux"): + return Path.home() / ".config" / "google-chrome" + elif sys.platform == "win32": + return Path(os.environ["LOCALAPPDATA"]) / "Google" / "Chrome" / "User Data" + else: + raise ValueError(f"Unsupported platform: {sys.platform}") + + def _find_chrome_profiles(self) -> list[Path]: + """Auto-detect all Chrome profiles.""" + base_path = self._get_chrome_base_path() + if not base_path.exists(): + return [] + + profiles = [] + + # Check Default profile + default_profile = base_path / "Default" + if default_profile.exists() and (default_profile / "History").exists(): + profiles.append(default_profile) + + # Check numbered profiles + for item in base_path.iterdir(): + if item.is_dir() and item.name.startswith("Profile "): + if (item / "History").exists(): + profiles.append(item) + + return profiles + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load browser history and convert to text chunks.""" + # Determine Chrome profiles + if args.chrome_profile and not args.auto_find_profiles: + profile_dirs = [Path(args.chrome_profile)] + else: + print("Auto-detecting Chrome profiles...") + profile_dirs = self._find_chrome_profiles() + + # If specific profile given, filter to just that one + if args.chrome_profile: + profile_path = Path(args.chrome_profile) + profile_dirs = [p for p in profile_dirs if p == profile_path] + + if not profile_dirs: + print("No Chrome profiles found!") + print("Please specify --chrome-profile manually") + return [] + + print(f"Found {len(profile_dirs)} Chrome profiles") + + # Create reader + reader = ChromeHistoryReader() + + # Process each profile + all_documents = [] + total_processed = 0 + + for i, profile_dir in enumerate(profile_dirs): + print(f"\nProcessing profile {i + 1}/{len(profile_dirs)}: {profile_dir.name}") + + try: + # Apply max_items limit per profile + max_per_profile = -1 + if args.max_items > 0: + remaining = args.max_items - total_processed + if remaining <= 0: + break + max_per_profile = remaining + + # Load history + documents = reader.load_data( + chrome_profile_path=str(profile_dir), + max_count=max_per_profile, + ) + + if documents: + all_documents.extend(documents) + total_processed += len(documents) + print(f"Processed {len(documents)} history entries from this profile") + + except Exception as e: + print(f"Error processing {profile_dir}: {e}") + continue + + if not all_documents: + print("No browser history found to process!") + return [] + + print(f"\nTotal history entries processed: {len(all_documents)}") + + # Convert to text chunks + all_texts = create_text_chunks( + all_documents, chunk_size=args.chunk_size, chunk_overlap=args.chunk_overlap + ) + + return all_texts + + +if __name__ == "__main__": + import asyncio + + # Example queries for browser history RAG + print("\n🌐 Browser History RAG Example") + print("=" * 50) + print("\nExample queries you can try:") + print("- 'What websites did I visit about machine learning?'") + print("- 'Find my search history about programming'") + print("- 'What YouTube videos did I watch recently?'") + print("- 'Show me websites about travel planning'") + print("\nNote: Make sure Chrome is closed before running\n") + + rag = BrowserRAG() + asyncio.run(rag.run()) diff --git a/apps/chatgpt_data/__init__.py b/apps/chatgpt_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/chatgpt_data/chatgpt_reader.py b/apps/chatgpt_data/chatgpt_reader.py new file mode 100644 index 0000000..c52ce22 --- /dev/null +++ b/apps/chatgpt_data/chatgpt_reader.py @@ -0,0 +1,413 @@ +""" +ChatGPT export data reader. + +Reads and processes ChatGPT export data from chat.html files. +""" + +import re +from pathlib import Path +from typing import Any +from zipfile import ZipFile + +from bs4 import BeautifulSoup +from llama_index.core import Document +from llama_index.core.readers.base import BaseReader + + +class ChatGPTReader(BaseReader): + """ + ChatGPT export data reader. + + Reads ChatGPT conversation data from exported chat.html files or zip archives. + Processes conversations into structured documents with metadata. + """ + + def __init__(self, concatenate_conversations: bool = True) -> None: + """ + Initialize. + + Args: + concatenate_conversations: Whether to concatenate messages within conversations for better context + """ + try: + from bs4 import BeautifulSoup # noqa + except ImportError: + raise ImportError("`beautifulsoup4` package not found: `pip install beautifulsoup4`") + + self.concatenate_conversations = concatenate_conversations + + def _extract_html_from_zip(self, zip_path: Path) -> str | None: + """ + Extract chat.html from ChatGPT export zip file. + + Args: + zip_path: Path to the ChatGPT export zip file + + Returns: + HTML content as string, or None if not found + """ + try: + with ZipFile(zip_path, "r") as zip_file: + # Look for chat.html or conversations.html + html_files = [ + f + for f in zip_file.namelist() + if f.endswith(".html") and ("chat" in f.lower() or "conversation" in f.lower()) + ] + + if not html_files: + print(f"No HTML chat file found in {zip_path}") + return None + + # Use the first HTML file found + html_file = html_files[0] + print(f"Found HTML file: {html_file}") + + with zip_file.open(html_file) as f: + return f.read().decode("utf-8", errors="ignore") + + except Exception as e: + print(f"Error extracting HTML from zip {zip_path}: {e}") + return None + + def _parse_chatgpt_html(self, html_content: str) -> list[dict]: + """ + Parse ChatGPT HTML export to extract conversations. + + Args: + html_content: HTML content from ChatGPT export + + Returns: + List of conversation dictionaries + """ + soup = BeautifulSoup(html_content, "html.parser") + conversations = [] + + # Try different possible structures for ChatGPT exports + # Structure 1: Look for conversation containers + conversation_containers = soup.find_all( + ["div", "section"], class_=re.compile(r"conversation|chat", re.I) + ) + + if not conversation_containers: + # Structure 2: Look for message containers directly + conversation_containers = [soup] # Use the entire document as one conversation + + for container in conversation_containers: + conversation = self._extract_conversation_from_container(container) + if conversation and conversation.get("messages"): + conversations.append(conversation) + + # If no structured conversations found, try to extract all text as one conversation + if not conversations: + all_text = soup.get_text(separator="\n", strip=True) + if all_text: + conversations.append( + { + "title": "ChatGPT Conversation", + "messages": [{"role": "mixed", "content": all_text, "timestamp": None}], + "timestamp": None, + } + ) + + return conversations + + def _extract_conversation_from_container(self, container) -> dict | None: + """ + Extract conversation data from a container element. + + Args: + container: BeautifulSoup element containing conversation + + Returns: + Dictionary with conversation data or None + """ + messages = [] + + # Look for message elements with various possible structures + message_selectors = ['[class*="message"]', '[class*="chat"]', "[data-message]", "p", "div"] + + for selector in message_selectors: + message_elements = container.select(selector) + if message_elements: + break + else: + message_elements = [] + + # If no structured messages found, treat the entire container as one message + if not message_elements: + text_content = container.get_text(separator="\n", strip=True) + if text_content: + messages.append({"role": "mixed", "content": text_content, "timestamp": None}) + else: + for element in message_elements: + message = self._extract_message_from_element(element) + if message: + messages.append(message) + + if not messages: + return None + + # Try to extract conversation title + title_element = container.find(["h1", "h2", "h3", "title"]) + title = title_element.get_text(strip=True) if title_element else "ChatGPT Conversation" + + # Try to extract timestamp from various possible locations + timestamp = self._extract_timestamp_from_container(container) + + return {"title": title, "messages": messages, "timestamp": timestamp} + + def _extract_message_from_element(self, element) -> dict | None: + """ + Extract message data from an element. + + Args: + element: BeautifulSoup element containing message + + Returns: + Dictionary with message data or None + """ + text_content = element.get_text(separator=" ", strip=True) + + # Skip empty or very short messages + if not text_content or len(text_content.strip()) < 3: + return None + + # Try to determine role (user/assistant) from class names or content + role = "mixed" # Default role + + class_names = " ".join(element.get("class", [])).lower() + if "user" in class_names or "human" in class_names: + role = "user" + elif "assistant" in class_names or "ai" in class_names or "gpt" in class_names: + role = "assistant" + elif text_content.lower().startswith(("you:", "user:", "me:")): + role = "user" + text_content = re.sub(r"^(you|user|me):\s*", "", text_content, flags=re.IGNORECASE) + elif text_content.lower().startswith(("chatgpt:", "assistant:", "ai:")): + role = "assistant" + text_content = re.sub( + r"^(chatgpt|assistant|ai):\s*", "", text_content, flags=re.IGNORECASE + ) + + # Try to extract timestamp + timestamp = self._extract_timestamp_from_element(element) + + return {"role": role, "content": text_content, "timestamp": timestamp} + + def _extract_timestamp_from_element(self, element) -> str | None: + """Extract timestamp from element.""" + # Look for timestamp in various attributes and child elements + timestamp_attrs = ["data-timestamp", "timestamp", "datetime"] + for attr in timestamp_attrs: + if element.get(attr): + return element.get(attr) + + # Look for time elements + time_element = element.find("time") + if time_element: + return time_element.get("datetime") or time_element.get_text(strip=True) + + # Look for date-like text patterns + text = element.get_text() + date_patterns = [r"\d{4}-\d{2}-\d{2}", r"\d{1,2}/\d{1,2}/\d{4}", r"\w+ \d{1,2}, \d{4}"] + + for pattern in date_patterns: + match = re.search(pattern, text) + if match: + return match.group() + + return None + + def _extract_timestamp_from_container(self, container) -> str | None: + """Extract timestamp from conversation container.""" + return self._extract_timestamp_from_element(container) + + def _create_concatenated_content(self, conversation: dict) -> str: + """ + Create concatenated content from conversation messages. + + Args: + conversation: Dictionary containing conversation data + + Returns: + Formatted concatenated content + """ + title = conversation.get("title", "ChatGPT Conversation") + messages = conversation.get("messages", []) + timestamp = conversation.get("timestamp", "Unknown") + + # Build message content + message_parts = [] + for message in messages: + role = message.get("role", "mixed") + content = message.get("content", "") + msg_timestamp = message.get("timestamp", "") + + if role == "user": + prefix = "[You]" + elif role == "assistant": + prefix = "[ChatGPT]" + else: + prefix = "[Message]" + + # Add timestamp if available + if msg_timestamp: + prefix += f" ({msg_timestamp})" + + message_parts.append(f"{prefix}: {content}") + + concatenated_text = "\n\n".join(message_parts) + + # Create final document content + doc_content = f"""Conversation: {title} +Date: {timestamp} +Messages ({len(messages)} messages): + +{concatenated_text} +""" + return doc_content + + def load_data(self, input_dir: str | None = None, **load_kwargs: Any) -> list[Document]: + """ + Load ChatGPT export data. + + Args: + input_dir: Directory containing ChatGPT export files or path to specific file + **load_kwargs: + max_count (int): Maximum number of conversations to process + chatgpt_export_path (str): Specific path to ChatGPT export file/directory + include_metadata (bool): Whether to include metadata in documents + """ + docs: list[Document] = [] + max_count = load_kwargs.get("max_count", -1) + chatgpt_export_path = load_kwargs.get("chatgpt_export_path", input_dir) + include_metadata = load_kwargs.get("include_metadata", True) + + if not chatgpt_export_path: + print("No ChatGPT export path provided") + return docs + + export_path = Path(chatgpt_export_path) + + if not export_path.exists(): + print(f"ChatGPT export path not found: {export_path}") + return docs + + html_content = None + + # Handle different input types + if export_path.is_file(): + if export_path.suffix.lower() == ".zip": + # Extract HTML from zip file + html_content = self._extract_html_from_zip(export_path) + elif export_path.suffix.lower() == ".html": + # Read HTML file directly + try: + with open(export_path, encoding="utf-8", errors="ignore") as f: + html_content = f.read() + except Exception as e: + print(f"Error reading HTML file {export_path}: {e}") + return docs + else: + print(f"Unsupported file type: {export_path.suffix}") + return docs + + elif export_path.is_dir(): + # Look for HTML files in directory + html_files = list(export_path.glob("*.html")) + zip_files = list(export_path.glob("*.zip")) + + if html_files: + # Use first HTML file found + html_file = html_files[0] + print(f"Found HTML file: {html_file}") + try: + with open(html_file, encoding="utf-8", errors="ignore") as f: + html_content = f.read() + except Exception as e: + print(f"Error reading HTML file {html_file}: {e}") + return docs + + elif zip_files: + # Use first zip file found + zip_file = zip_files[0] + print(f"Found zip file: {zip_file}") + html_content = self._extract_html_from_zip(zip_file) + + else: + print(f"No HTML or zip files found in {export_path}") + return docs + + if not html_content: + print("No HTML content found to process") + return docs + + # Parse conversations from HTML + print("Parsing ChatGPT conversations from HTML...") + conversations = self._parse_chatgpt_html(html_content) + + if not conversations: + print("No conversations found in HTML content") + return docs + + print(f"Found {len(conversations)} conversations") + + # Process conversations into documents + count = 0 + for conversation in conversations: + if max_count > 0 and count >= max_count: + break + + if self.concatenate_conversations: + # Create one document per conversation with concatenated messages + doc_content = self._create_concatenated_content(conversation) + + metadata = {} + if include_metadata: + metadata = { + "title": conversation.get("title", "ChatGPT Conversation"), + "timestamp": conversation.get("timestamp", "Unknown"), + "message_count": len(conversation.get("messages", [])), + "source": "ChatGPT Export", + } + + doc = Document(text=doc_content, metadata=metadata) + docs.append(doc) + count += 1 + + else: + # Create separate documents for each message + for message in conversation.get("messages", []): + if max_count > 0 and count >= max_count: + break + + role = message.get("role", "mixed") + content = message.get("content", "") + msg_timestamp = message.get("timestamp", "") + + if not content.strip(): + continue + + # Create document content with context + doc_content = f"""Conversation: {conversation.get("title", "ChatGPT Conversation")} +Role: {role} +Timestamp: {msg_timestamp or conversation.get("timestamp", "Unknown")} +Message: {content} +""" + + metadata = {} + if include_metadata: + metadata = { + "conversation_title": conversation.get("title", "ChatGPT Conversation"), + "role": role, + "timestamp": msg_timestamp or conversation.get("timestamp", "Unknown"), + "source": "ChatGPT Export", + } + + doc = Document(text=doc_content, metadata=metadata) + docs.append(doc) + count += 1 + + print(f"Created {len(docs)} documents from ChatGPT export") + return docs diff --git a/apps/chatgpt_rag.py b/apps/chatgpt_rag.py new file mode 100644 index 0000000..c97d2cd --- /dev/null +++ b/apps/chatgpt_rag.py @@ -0,0 +1,187 @@ +""" +ChatGPT RAG example using the unified interface. +Supports ChatGPT export data from chat.html files. +""" + +import sys +from pathlib import Path +from typing import Any + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from base_rag_example import BaseRAGExample +from chunking import create_text_chunks + +from .chatgpt_data.chatgpt_reader import ChatGPTReader + + +class ChatGPTRAG(BaseRAGExample): + """RAG example for ChatGPT conversation data.""" + + def __init__(self): + # Set default values BEFORE calling super().__init__ + self.max_items_default = -1 # Process all conversations by default + self.embedding_model_default = ( + "sentence-transformers/all-MiniLM-L6-v2" # Fast 384-dim model + ) + + super().__init__( + name="ChatGPT", + description="Process and query ChatGPT conversation exports with LEANN", + default_index_name="chatgpt_conversations_index", + ) + + def _add_specific_arguments(self, parser): + """Add ChatGPT-specific arguments.""" + chatgpt_group = parser.add_argument_group("ChatGPT Parameters") + chatgpt_group.add_argument( + "--export-path", + type=str, + default="./chatgpt_export", + help="Path to ChatGPT export file (.zip or .html) or directory containing exports (default: ./chatgpt_export)", + ) + chatgpt_group.add_argument( + "--concatenate-conversations", + action="store_true", + default=True, + help="Concatenate messages within conversations for better context (default: True)", + ) + chatgpt_group.add_argument( + "--separate-messages", + action="store_true", + help="Process each message as a separate document (overrides --concatenate-conversations)", + ) + chatgpt_group.add_argument( + "--chunk-size", type=int, default=512, help="Text chunk size (default: 512)" + ) + chatgpt_group.add_argument( + "--chunk-overlap", type=int, default=128, help="Text chunk overlap (default: 128)" + ) + + def _find_chatgpt_exports(self, export_path: Path) -> list[Path]: + """ + Find ChatGPT export files in the given path. + + Args: + export_path: Path to search for exports + + Returns: + List of paths to ChatGPT export files + """ + export_files = [] + + if export_path.is_file(): + if export_path.suffix.lower() in [".zip", ".html"]: + export_files.append(export_path) + elif export_path.is_dir(): + # Look for zip and html files + export_files.extend(export_path.glob("*.zip")) + export_files.extend(export_path.glob("*.html")) + + return export_files + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load ChatGPT export data and convert to text chunks.""" + export_path = Path(args.export_path) + + if not export_path.exists(): + print(f"ChatGPT export path not found: {export_path}") + print( + "Please ensure you have exported your ChatGPT data and placed it in the correct location." + ) + print("\nTo export your ChatGPT data:") + print("1. Sign in to ChatGPT") + print("2. Click on your profile icon β†’ Settings β†’ Data Controls") + print("3. Click 'Export' under Export Data") + print("4. Download the zip file from the email link") + print("5. Extract or place the file/directory at the specified path") + return [] + + # Find export files + export_files = self._find_chatgpt_exports(export_path) + + if not export_files: + print(f"No ChatGPT export files (.zip or .html) found in: {export_path}") + return [] + + print(f"Found {len(export_files)} ChatGPT export files") + + # Create reader with appropriate settings + concatenate = args.concatenate_conversations and not args.separate_messages + reader = ChatGPTReader(concatenate_conversations=concatenate) + + # Process each export file + all_documents = [] + total_processed = 0 + + for i, export_file in enumerate(export_files): + print(f"\nProcessing export file {i + 1}/{len(export_files)}: {export_file.name}") + + try: + # Apply max_items limit per file + max_per_file = -1 + if args.max_items > 0: + remaining = args.max_items - total_processed + if remaining <= 0: + break + max_per_file = remaining + + # Load conversations + documents = reader.load_data( + chatgpt_export_path=str(export_file), + max_count=max_per_file, + include_metadata=True, + ) + + if documents: + all_documents.extend(documents) + total_processed += len(documents) + print(f"Processed {len(documents)} conversations from this file") + else: + print(f"No conversations loaded from {export_file}") + + except Exception as e: + print(f"Error processing {export_file}: {e}") + continue + + if not all_documents: + print("No conversations found to process!") + print("\nTroubleshooting:") + print("- Ensure the export file is a valid ChatGPT export") + print("- Check that the HTML file contains conversation data") + print("- Try extracting the zip file and pointing to the HTML file directly") + return [] + + print(f"\nTotal conversations processed: {len(all_documents)}") + print("Now starting to split into text chunks... this may take some time") + + # Convert to text chunks + all_texts = create_text_chunks( + all_documents, chunk_size=args.chunk_size, chunk_overlap=args.chunk_overlap + ) + + print(f"Created {len(all_texts)} text chunks from {len(all_documents)} conversations") + return all_texts + + +if __name__ == "__main__": + import asyncio + + # Example queries for ChatGPT RAG + print("\nπŸ€– ChatGPT RAG Example") + print("=" * 50) + print("\nExample queries you can try:") + print("- 'What did I ask about Python programming?'") + print("- 'Show me conversations about machine learning'") + print("- 'Find discussions about travel planning'") + print("- 'What advice did ChatGPT give me about career development?'") + print("- 'Search for conversations about cooking recipes'") + print("\nTo get started:") + print("1. Export your ChatGPT data from Settings β†’ Data Controls β†’ Export") + print("2. Place the downloaded zip file or extracted HTML in ./chatgpt_export/") + print("3. Run this script to build your personal ChatGPT knowledge base!") + print("\nOr run without --query for interactive mode\n") + + rag = ChatGPTRAG() + asyncio.run(rag.run()) diff --git a/apps/chunking/__init__.py b/apps/chunking/__init__.py new file mode 100644 index 0000000..17a7e4a --- /dev/null +++ b/apps/chunking/__init__.py @@ -0,0 +1,47 @@ +"""Unified chunking utilities facade. + +This module re-exports the packaged utilities from `leann.chunking_utils` so +that both repo apps (importing `chunking`) and installed wheels share one +single implementation. When running from the repo without installation, it +adds the `packages/leann-core/src` directory to `sys.path` as a fallback. +""" + +import sys +from pathlib import Path + +try: + from leann.chunking_utils import ( + CODE_EXTENSIONS, + _traditional_chunks_as_dicts, + create_ast_chunks, + create_text_chunks, + create_traditional_chunks, + detect_code_files, + get_language_from_extension, + ) +except Exception: # pragma: no cover - best-effort fallback for dev environment + repo_root = Path(__file__).resolve().parents[2] + leann_src = repo_root / "packages" / "leann-core" / "src" + if leann_src.exists(): + sys.path.insert(0, str(leann_src)) + from leann.chunking_utils import ( + CODE_EXTENSIONS, + _traditional_chunks_as_dicts, + create_ast_chunks, + create_text_chunks, + create_traditional_chunks, + detect_code_files, + get_language_from_extension, + ) + else: + raise + +__all__ = [ + "CODE_EXTENSIONS", + "_traditional_chunks_as_dicts", + "create_ast_chunks", + "create_text_chunks", + "create_traditional_chunks", + "detect_code_files", + "get_language_from_extension", +] diff --git a/apps/claude_data/__init__.py b/apps/claude_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/claude_data/claude_reader.py b/apps/claude_data/claude_reader.py new file mode 100644 index 0000000..1af1097 --- /dev/null +++ b/apps/claude_data/claude_reader.py @@ -0,0 +1,420 @@ +""" +Claude export data reader. + +Reads and processes Claude conversation data from exported JSON files. +""" + +import json +from pathlib import Path +from typing import Any +from zipfile import ZipFile + +from llama_index.core import Document +from llama_index.core.readers.base import BaseReader + + +class ClaudeReader(BaseReader): + """ + Claude export data reader. + + Reads Claude conversation data from exported JSON files or zip archives. + Processes conversations into structured documents with metadata. + """ + + def __init__(self, concatenate_conversations: bool = True) -> None: + """ + Initialize. + + Args: + concatenate_conversations: Whether to concatenate messages within conversations for better context + """ + self.concatenate_conversations = concatenate_conversations + + def _extract_json_from_zip(self, zip_path: Path) -> list[str]: + """ + Extract JSON files from Claude export zip file. + + Args: + zip_path: Path to the Claude export zip file + + Returns: + List of JSON content strings, or empty list if not found + """ + json_contents = [] + try: + with ZipFile(zip_path, "r") as zip_file: + # Look for JSON files + json_files = [f for f in zip_file.namelist() if f.endswith(".json")] + + if not json_files: + print(f"No JSON files found in {zip_path}") + return [] + + print(f"Found {len(json_files)} JSON files in archive") + + for json_file in json_files: + with zip_file.open(json_file) as f: + content = f.read().decode("utf-8", errors="ignore") + json_contents.append(content) + + except Exception as e: + print(f"Error extracting JSON from zip {zip_path}: {e}") + + return json_contents + + def _parse_claude_json(self, json_content: str) -> list[dict]: + """ + Parse Claude JSON export to extract conversations. + + Args: + json_content: JSON content from Claude export + + Returns: + List of conversation dictionaries + """ + try: + data = json.loads(json_content) + except json.JSONDecodeError as e: + print(f"Error parsing JSON: {e}") + return [] + + conversations = [] + + # Handle different possible JSON structures + if isinstance(data, list): + # If data is a list of conversations + for item in data: + conversation = self._extract_conversation_from_json(item) + if conversation: + conversations.append(conversation) + elif isinstance(data, dict): + # Check for common structures + if "conversations" in data: + # Structure: {"conversations": [...]} + for item in data["conversations"]: + conversation = self._extract_conversation_from_json(item) + if conversation: + conversations.append(conversation) + elif "messages" in data: + # Single conversation with messages + conversation = self._extract_conversation_from_json(data) + if conversation: + conversations.append(conversation) + else: + # Try to treat the whole object as a conversation + conversation = self._extract_conversation_from_json(data) + if conversation: + conversations.append(conversation) + + return conversations + + def _extract_conversation_from_json(self, conv_data: dict) -> dict | None: + """ + Extract conversation data from a JSON object. + + Args: + conv_data: Dictionary containing conversation data + + Returns: + Dictionary with conversation data or None + """ + if not isinstance(conv_data, dict): + return None + + messages = [] + + # Look for messages in various possible structures + message_sources = [] + if "messages" in conv_data: + message_sources = conv_data["messages"] + elif "chat" in conv_data: + message_sources = conv_data["chat"] + elif "conversation" in conv_data: + message_sources = conv_data["conversation"] + else: + # If no clear message structure, try to extract from the object itself + if "content" in conv_data and "role" in conv_data: + message_sources = [conv_data] + + for msg_data in message_sources: + message = self._extract_message_from_json(msg_data) + if message: + messages.append(message) + + if not messages: + return None + + # Extract conversation metadata + title = self._extract_title_from_conversation(conv_data, messages) + timestamp = self._extract_timestamp_from_conversation(conv_data) + + return {"title": title, "messages": messages, "timestamp": timestamp} + + def _extract_message_from_json(self, msg_data: dict) -> dict | None: + """ + Extract message data from a JSON message object. + + Args: + msg_data: Dictionary containing message data + + Returns: + Dictionary with message data or None + """ + if not isinstance(msg_data, dict): + return None + + # Extract content from various possible fields + content = "" + content_fields = ["content", "text", "message", "body"] + for field in content_fields: + if msg_data.get(field): + content = str(msg_data[field]) + break + + if not content or len(content.strip()) < 3: + return None + + # Extract role (user/assistant/human/ai/claude) + role = "mixed" # Default role + role_fields = ["role", "sender", "from", "author", "type"] + for field in role_fields: + if msg_data.get(field): + role_value = str(msg_data[field]).lower() + if role_value in ["user", "human", "person"]: + role = "user" + elif role_value in ["assistant", "ai", "claude", "bot"]: + role = "assistant" + break + + # Extract timestamp + timestamp = self._extract_timestamp_from_message(msg_data) + + return {"role": role, "content": content, "timestamp": timestamp} + + def _extract_timestamp_from_message(self, msg_data: dict) -> str | None: + """Extract timestamp from message data.""" + timestamp_fields = ["timestamp", "created_at", "date", "time"] + for field in timestamp_fields: + if msg_data.get(field): + return str(msg_data[field]) + return None + + def _extract_timestamp_from_conversation(self, conv_data: dict) -> str | None: + """Extract timestamp from conversation data.""" + timestamp_fields = ["timestamp", "created_at", "date", "updated_at", "last_updated"] + for field in timestamp_fields: + if conv_data.get(field): + return str(conv_data[field]) + return None + + def _extract_title_from_conversation(self, conv_data: dict, messages: list) -> str: + """Extract or generate title for conversation.""" + # Try to find explicit title + title_fields = ["title", "name", "subject", "topic"] + for field in title_fields: + if conv_data.get(field): + return str(conv_data[field]) + + # Generate title from first user message + for message in messages: + if message.get("role") == "user": + content = message.get("content", "") + if content: + # Use first 50 characters as title + title = content[:50].strip() + if len(content) > 50: + title += "..." + return title + + return "Claude Conversation" + + def _create_concatenated_content(self, conversation: dict) -> str: + """ + Create concatenated content from conversation messages. + + Args: + conversation: Dictionary containing conversation data + + Returns: + Formatted concatenated content + """ + title = conversation.get("title", "Claude Conversation") + messages = conversation.get("messages", []) + timestamp = conversation.get("timestamp", "Unknown") + + # Build message content + message_parts = [] + for message in messages: + role = message.get("role", "mixed") + content = message.get("content", "") + msg_timestamp = message.get("timestamp", "") + + if role == "user": + prefix = "[You]" + elif role == "assistant": + prefix = "[Claude]" + else: + prefix = "[Message]" + + # Add timestamp if available + if msg_timestamp: + prefix += f" ({msg_timestamp})" + + message_parts.append(f"{prefix}: {content}") + + concatenated_text = "\n\n".join(message_parts) + + # Create final document content + doc_content = f"""Conversation: {title} +Date: {timestamp} +Messages ({len(messages)} messages): + +{concatenated_text} +""" + return doc_content + + def load_data(self, input_dir: str | None = None, **load_kwargs: Any) -> list[Document]: + """ + Load Claude export data. + + Args: + input_dir: Directory containing Claude export files or path to specific file + **load_kwargs: + max_count (int): Maximum number of conversations to process + claude_export_path (str): Specific path to Claude export file/directory + include_metadata (bool): Whether to include metadata in documents + """ + docs: list[Document] = [] + max_count = load_kwargs.get("max_count", -1) + claude_export_path = load_kwargs.get("claude_export_path", input_dir) + include_metadata = load_kwargs.get("include_metadata", True) + + if not claude_export_path: + print("No Claude export path provided") + return docs + + export_path = Path(claude_export_path) + + if not export_path.exists(): + print(f"Claude export path not found: {export_path}") + return docs + + json_contents = [] + + # Handle different input types + if export_path.is_file(): + if export_path.suffix.lower() == ".zip": + # Extract JSON from zip file + json_contents = self._extract_json_from_zip(export_path) + elif export_path.suffix.lower() == ".json": + # Read JSON file directly + try: + with open(export_path, encoding="utf-8", errors="ignore") as f: + json_contents.append(f.read()) + except Exception as e: + print(f"Error reading JSON file {export_path}: {e}") + return docs + else: + print(f"Unsupported file type: {export_path.suffix}") + return docs + + elif export_path.is_dir(): + # Look for JSON files in directory + json_files = list(export_path.glob("*.json")) + zip_files = list(export_path.glob("*.zip")) + + if json_files: + print(f"Found {len(json_files)} JSON files in directory") + for json_file in json_files: + try: + with open(json_file, encoding="utf-8", errors="ignore") as f: + json_contents.append(f.read()) + except Exception as e: + print(f"Error reading JSON file {json_file}: {e}") + continue + + if zip_files: + print(f"Found {len(zip_files)} ZIP files in directory") + for zip_file in zip_files: + zip_contents = self._extract_json_from_zip(zip_file) + json_contents.extend(zip_contents) + + if not json_files and not zip_files: + print(f"No JSON or ZIP files found in {export_path}") + return docs + + if not json_contents: + print("No JSON content found to process") + return docs + + # Parse conversations from JSON content + print("Parsing Claude conversations from JSON...") + all_conversations = [] + for json_content in json_contents: + conversations = self._parse_claude_json(json_content) + all_conversations.extend(conversations) + + if not all_conversations: + print("No conversations found in JSON content") + return docs + + print(f"Found {len(all_conversations)} conversations") + + # Process conversations into documents + count = 0 + for conversation in all_conversations: + if max_count > 0 and count >= max_count: + break + + if self.concatenate_conversations: + # Create one document per conversation with concatenated messages + doc_content = self._create_concatenated_content(conversation) + + metadata = {} + if include_metadata: + metadata = { + "title": conversation.get("title", "Claude Conversation"), + "timestamp": conversation.get("timestamp", "Unknown"), + "message_count": len(conversation.get("messages", [])), + "source": "Claude Export", + } + + doc = Document(text=doc_content, metadata=metadata) + docs.append(doc) + count += 1 + + else: + # Create separate documents for each message + for message in conversation.get("messages", []): + if max_count > 0 and count >= max_count: + break + + role = message.get("role", "mixed") + content = message.get("content", "") + msg_timestamp = message.get("timestamp", "") + + if not content.strip(): + continue + + # Create document content with context + doc_content = f"""Conversation: {conversation.get("title", "Claude Conversation")} +Role: {role} +Timestamp: {msg_timestamp or conversation.get("timestamp", "Unknown")} +Message: {content} +""" + + metadata = {} + if include_metadata: + metadata = { + "conversation_title": conversation.get("title", "Claude Conversation"), + "role": role, + "timestamp": msg_timestamp or conversation.get("timestamp", "Unknown"), + "source": "Claude Export", + } + + doc = Document(text=doc_content, metadata=metadata) + docs.append(doc) + count += 1 + + print(f"Created {len(docs)} documents from Claude export") + return docs diff --git a/apps/claude_rag.py b/apps/claude_rag.py new file mode 100644 index 0000000..2cc80dd --- /dev/null +++ b/apps/claude_rag.py @@ -0,0 +1,190 @@ +""" +Claude RAG example using the unified interface. +Supports Claude export data from JSON files. +""" + +import sys +from pathlib import Path +from typing import Any + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from base_rag_example import BaseRAGExample +from chunking import create_text_chunks + +from .claude_data.claude_reader import ClaudeReader + + +class ClaudeRAG(BaseRAGExample): + """RAG example for Claude conversation data.""" + + def __init__(self): + # Set default values BEFORE calling super().__init__ + self.max_items_default = -1 # Process all conversations by default + self.embedding_model_default = ( + "sentence-transformers/all-MiniLM-L6-v2" # Fast 384-dim model + ) + + super().__init__( + name="Claude", + description="Process and query Claude conversation exports with LEANN", + default_index_name="claude_conversations_index", + ) + + def _add_specific_arguments(self, parser): + """Add Claude-specific arguments.""" + claude_group = parser.add_argument_group("Claude Parameters") + claude_group.add_argument( + "--export-path", + type=str, + default="./claude_export", + help="Path to Claude export file (.json or .zip) or directory containing exports (default: ./claude_export)", + ) + claude_group.add_argument( + "--concatenate-conversations", + action="store_true", + default=True, + help="Concatenate messages within conversations for better context (default: True)", + ) + claude_group.add_argument( + "--separate-messages", + action="store_true", + help="Process each message as a separate document (overrides --concatenate-conversations)", + ) + claude_group.add_argument( + "--chunk-size", type=int, default=512, help="Text chunk size (default: 512)" + ) + claude_group.add_argument( + "--chunk-overlap", type=int, default=128, help="Text chunk overlap (default: 128)" + ) + + def _find_claude_exports(self, export_path: Path) -> list[Path]: + """ + Find Claude export files in the given path. + + Args: + export_path: Path to search for exports + + Returns: + List of paths to Claude export files + """ + export_files = [] + + if export_path.is_file(): + if export_path.suffix.lower() in [".zip", ".json"]: + export_files.append(export_path) + elif export_path.is_dir(): + # Look for zip and json files + export_files.extend(export_path.glob("*.zip")) + export_files.extend(export_path.glob("*.json")) + + return export_files + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load Claude export data and convert to text chunks.""" + export_path = Path(args.export_path) + + if not export_path.exists(): + print(f"Claude export path not found: {export_path}") + print( + "Please ensure you have exported your Claude data and placed it in the correct location." + ) + print("\nTo export your Claude data:") + print("1. Open Claude in your browser") + print("2. Look for export/download options in settings or conversation menu") + print("3. Download the conversation data (usually in JSON format)") + print("4. Place the file/directory at the specified path") + print( + "\nNote: Claude export methods may vary. Check Claude's help documentation for current instructions." + ) + return [] + + # Find export files + export_files = self._find_claude_exports(export_path) + + if not export_files: + print(f"No Claude export files (.json or .zip) found in: {export_path}") + return [] + + print(f"Found {len(export_files)} Claude export files") + + # Create reader with appropriate settings + concatenate = args.concatenate_conversations and not args.separate_messages + reader = ClaudeReader(concatenate_conversations=concatenate) + + # Process each export file + all_documents = [] + total_processed = 0 + + for i, export_file in enumerate(export_files): + print(f"\nProcessing export file {i + 1}/{len(export_files)}: {export_file.name}") + + try: + # Apply max_items limit per file + max_per_file = -1 + if args.max_items > 0: + remaining = args.max_items - total_processed + if remaining <= 0: + break + max_per_file = remaining + + # Load conversations + documents = reader.load_data( + claude_export_path=str(export_file), + max_count=max_per_file, + include_metadata=True, + ) + + if documents: + all_documents.extend(documents) + total_processed += len(documents) + print(f"Processed {len(documents)} conversations from this file") + else: + print(f"No conversations loaded from {export_file}") + + except Exception as e: + print(f"Error processing {export_file}: {e}") + continue + + if not all_documents: + print("No conversations found to process!") + print("\nTroubleshooting:") + print("- Ensure the export file is a valid Claude export") + print("- Check that the JSON file contains conversation data") + print("- Try using a different export format or method") + print("- Check Claude's documentation for current export procedures") + return [] + + print(f"\nTotal conversations processed: {len(all_documents)}") + print("Now starting to split into text chunks... this may take some time") + + # Convert to text chunks + all_texts = create_text_chunks( + all_documents, chunk_size=args.chunk_size, chunk_overlap=args.chunk_overlap + ) + + print(f"Created {len(all_texts)} text chunks from {len(all_documents)} conversations") + return all_texts + + +if __name__ == "__main__": + import asyncio + + # Example queries for Claude RAG + print("\nπŸ€– Claude RAG Example") + print("=" * 50) + print("\nExample queries you can try:") + print("- 'What did I ask Claude about Python programming?'") + print("- 'Show me conversations about machine learning'") + print("- 'Find discussions about code optimization'") + print("- 'What advice did Claude give me about software design?'") + print("- 'Search for conversations about debugging techniques'") + print("\nTo get started:") + print("1. Export your Claude conversation data") + print("2. Place the JSON/ZIP file in ./claude_export/") + print("3. Run this script to build your personal Claude knowledge base!") + print("\nOr run without --query for interactive mode\n") + + rag = ClaudeRAG() + asyncio.run(rag.run()) diff --git a/apps/code_rag.py b/apps/code_rag.py new file mode 100644 index 0000000..452e0a6 --- /dev/null +++ b/apps/code_rag.py @@ -0,0 +1,207 @@ +""" +Code RAG example using AST-aware chunking for optimal code understanding. +Specialized for code repositories with automatic language detection and +optimized chunking parameters. +""" + +import sys +from pathlib import Path +from typing import Any + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from base_rag_example import BaseRAGExample +from chunking import CODE_EXTENSIONS, create_text_chunks +from llama_index.core import SimpleDirectoryReader + + +class CodeRAG(BaseRAGExample): + """Specialized RAG example for code repositories with AST-aware chunking.""" + + def __init__(self): + super().__init__( + name="Code", + description="Process and query code repositories with AST-aware chunking", + default_index_name="code_index", + ) + # Override defaults for code-specific usage + self.embedding_model_default = "facebook/contriever" # Good for code + self.max_items_default = -1 # Process all code files by default + + def _add_specific_arguments(self, parser): + """Add code-specific arguments.""" + code_group = parser.add_argument_group("Code Repository Parameters") + + code_group.add_argument( + "--repo-dir", + type=str, + default=".", + help="Code repository directory to index (default: current directory)", + ) + code_group.add_argument( + "--include-extensions", + nargs="+", + default=list(CODE_EXTENSIONS.keys()), + help="File extensions to include (default: supported code extensions)", + ) + code_group.add_argument( + "--exclude-dirs", + nargs="+", + default=[ + ".git", + "__pycache__", + "node_modules", + "venv", + ".venv", + "build", + "dist", + "target", + ], + help="Directories to exclude from indexing", + ) + code_group.add_argument( + "--max-file-size", + type=int, + default=1000000, # 1MB + help="Maximum file size in bytes to process (default: 1MB)", + ) + code_group.add_argument( + "--include-comments", + action="store_true", + help="Include comments in chunking (useful for documentation)", + ) + code_group.add_argument( + "--preserve-imports", + action="store_true", + default=True, + help="Try to preserve import statements in chunks (default: True)", + ) + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load code files and convert to AST-aware chunks.""" + print(f"πŸ” Scanning code repository: {args.repo_dir}") + print(f"πŸ“ Including extensions: {args.include_extensions}") + print(f"🚫 Excluding directories: {args.exclude_dirs}") + + # Check if repository directory exists + repo_path = Path(args.repo_dir) + if not repo_path.exists(): + raise ValueError(f"Repository directory not found: {args.repo_dir}") + + # Create exclusion filter + def file_filter(file_path: str) -> bool: + """Filter out unwanted files and directories.""" + path = Path(file_path) + + # Check file size + try: + if path.stat().st_size > args.max_file_size: + print(f"⚠️ Skipping large file: {path.name} ({path.stat().st_size} bytes)") + return False + except Exception: + return False + + # Check if in excluded directory + for exclude_dir in args.exclude_dirs: + if exclude_dir in path.parts: + return False + + return True + + try: + # Load documents with file filtering + documents = SimpleDirectoryReader( + args.repo_dir, + file_extractor=None, + recursive=True, + encoding="utf-8", + required_exts=args.include_extensions, + exclude_hidden=True, + ).load_data(show_progress=True) + + # Apply custom filtering + filtered_docs = [] + for doc in documents: + file_path = doc.metadata.get("file_path", "") + if file_filter(file_path): + filtered_docs.append(doc) + + documents = filtered_docs + + except Exception as e: + print(f"❌ Error loading code files: {e}") + return [] + + if not documents: + print( + f"❌ No code files found in {args.repo_dir} with extensions {args.include_extensions}" + ) + return [] + + print(f"βœ… Loaded {len(documents)} code files") + + # Show breakdown by language/extension + ext_counts = {} + for doc in documents: + file_path = doc.metadata.get("file_path", "") + if file_path: + ext = Path(file_path).suffix.lower() + ext_counts[ext] = ext_counts.get(ext, 0) + 1 + + print("πŸ“Š Files by extension:") + for ext, count in sorted(ext_counts.items()): + print(f" {ext}: {count} files") + + # Use AST-aware chunking by default for code + print( + f"🧠 Using AST-aware chunking (chunk_size: {args.ast_chunk_size}, overlap: {args.ast_chunk_overlap})" + ) + + all_texts = create_text_chunks( + documents, + chunk_size=256, # Fallback for non-code files + chunk_overlap=64, + use_ast_chunking=True, # Always use AST for code RAG + ast_chunk_size=args.ast_chunk_size, + ast_chunk_overlap=args.ast_chunk_overlap, + code_file_extensions=args.include_extensions, + ast_fallback_traditional=True, + ) + + # Apply max_items limit if specified + if args.max_items > 0 and len(all_texts) > args.max_items: + print(f"⏳ Limiting to {args.max_items} chunks (from {len(all_texts)})") + all_texts = all_texts[: args.max_items] + + print(f"βœ… Generated {len(all_texts)} code chunks") + return all_texts + + +if __name__ == "__main__": + import asyncio + + # Example queries for code RAG + print("\nπŸ’» Code RAG Example") + print("=" * 50) + print("\nExample queries you can try:") + print("- 'How does the embedding computation work?'") + print("- 'What are the main classes in this codebase?'") + print("- 'Show me the search implementation'") + print("- 'How is error handling implemented?'") + print("- 'What design patterns are used?'") + print("- 'Explain the chunking logic'") + print("\nπŸš€ Features:") + print("- βœ… AST-aware chunking preserves code structure") + print("- βœ… Automatic language detection") + print("- βœ… Smart filtering of large files and common excludes") + print("- βœ… Optimized for code understanding") + print("\nUsage examples:") + print(" python -m apps.code_rag --repo-dir ./my_project") + print( + " python -m apps.code_rag --include-extensions .py .js --query 'How does authentication work?'" + ) + print("\nOr run without --query for interactive mode\n") + + rag = CodeRAG() + asyncio.run(rag.run()) diff --git a/apps/colqwen_rag.py b/apps/colqwen_rag.py new file mode 100644 index 0000000..e2da48e --- /dev/null +++ b/apps/colqwen_rag.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 +""" +ColQwen RAG - Easy-to-use multimodal PDF retrieval with ColQwen2/ColPali + +Usage: + python -m apps.colqwen_rag build --pdfs ./my_pdfs/ --index my_index + python -m apps.colqwen_rag search my_index "How does attention work?" + python -m apps.colqwen_rag ask my_index --interactive +""" + +import argparse +import os +import sys +from pathlib import Path +from typing import Any, Optional, cast + +# Add LEANN packages to path +_repo_root = Path(__file__).resolve().parents[1] +_leann_core_src = _repo_root / "packages" / "leann-core" / "src" +_leann_hnsw_pkg = _repo_root / "packages" / "leann-backend-hnsw" +if str(_leann_core_src) not in sys.path: + sys.path.append(str(_leann_core_src)) +if str(_leann_hnsw_pkg) not in sys.path: + sys.path.append(str(_leann_hnsw_pkg)) + +import torch # noqa: E402 +from pdf2image import convert_from_path # noqa: E402 +from PIL import Image # noqa: E402 +from torch.utils.data import DataLoader # noqa: E402 +from tqdm import tqdm # noqa: E402 + +# Import the existing multi-vector implementation +sys.path.append(str(_repo_root / "apps" / "multimodal" / "vision-based-pdf-multi-vector")) +from leann_multi_vector import LeannMultiVector # noqa: E402 + + +class ColQwenRAG: + """Easy-to-use ColQwen RAG system for multimodal PDF retrieval.""" + + def __init__(self, model_type: str = "colpali"): + """ + Initialize ColQwen RAG system. + + Args: + model_type: "colqwen2" or "colpali" + """ + self._assert_supported_transformers() + self.model_type = model_type + self.device = self._get_device() + # Use float32 on MPS to avoid memory issues, float16 on CUDA, bfloat16 on CPU + if self.device.type == "mps": + self.dtype = torch.float32 + elif self.device.type == "cuda": + self.dtype = torch.float16 + else: + self.dtype = torch.bfloat16 + + print(f"πŸš€ Initializing {model_type.upper()} on {self.device} with {self.dtype}") + + # Load model and processor with MPS-optimized settings + try: + from colpali_engine import ( + ColPali, + ColPaliProcessor, + ColQwen2, + ColQwen2Processor, + ) + from colpali_engine.utils.torch_utils import ListDataset + + self._list_dataset_cls: type[Any] = ListDataset + + if model_type == "colqwen2": + self.model_name = "vidore/colqwen2-v1.0" + if self.device.type == "mps": + # For MPS, load on CPU first then move to avoid memory allocation issues + self.model = ColQwen2.from_pretrained( + self.model_name, + torch_dtype=self.dtype, + device_map="cpu", + low_cpu_mem_usage=True, + ).eval() + self.model = self.model.to(self.device) + else: + self.model = ColQwen2.from_pretrained( + self.model_name, + torch_dtype=self.dtype, + device_map=self.device, + low_cpu_mem_usage=True, + ).eval() + self.processor = ColQwen2Processor.from_pretrained(self.model_name) + else: # colpali + self.model_name = "vidore/colpali-v1.2" + if self.device.type == "mps": + # For MPS, load on CPU first then move to avoid memory allocation issues + self.model = ColPali.from_pretrained( + self.model_name, + torch_dtype=self.dtype, + device_map="cpu", + low_cpu_mem_usage=True, + ).eval() + self.model = self.model.to(self.device) + else: + self.model = ColPali.from_pretrained( + self.model_name, + torch_dtype=self.dtype, + device_map=self.device, + low_cpu_mem_usage=True, + ).eval() + self.processor = ColPaliProcessor.from_pretrained(self.model_name) + except Exception as e: + if "memory" in str(e).lower() or "offload" in str(e).lower(): + print(f"⚠️ Memory constraint on {self.device}, using CPU with optimizations...") + self.device = torch.device("cpu") + self.dtype = torch.float32 + + if model_type == "colqwen2": + self.model = ColQwen2.from_pretrained( + self.model_name, + torch_dtype=self.dtype, + device_map="cpu", + low_cpu_mem_usage=True, + ).eval() + self.processor = ColQwen2Processor.from_pretrained(self.model_name) + else: + self.model = ColPali.from_pretrained( + self.model_name, + torch_dtype=self.dtype, + device_map="cpu", + low_cpu_mem_usage=True, + ).eval() + self.processor = ColPaliProcessor.from_pretrained(self.model_name) + else: + raise + + def _assert_supported_transformers(self) -> None: + """Fail fast on transformers versions known to break ColPali/ColQwen2. + + colpali_engine (ColQwen2) requires a recent 4.x line (e.g. >=4.46.1); an + older guard here rejected all of 4.46+, which made that stack impossible + to run even when dependencies resolved correctly (see issue #308). + """ + from importlib.metadata import PackageNotFoundError, version + + try: + transformers_version = version("transformers") + except PackageNotFoundError: + return + + def _parse_semver(value: str) -> tuple[int, int, int]: + parts = value.split(".") + numbers: list[int] = [] + for part in parts[:3]: + digits = [] + for ch in part: + if ch.isdigit(): + digits.append(ch) + else: + break + numbers.append(int("".join(digits)) if digits else 0) + while len(numbers) < 3: + numbers.append(0) + return tuple(numbers) # type: ignore[return-value] + + if _parse_semver(transformers_version) >= (5, 0, 0): + raise RuntimeError( + "Unsupported transformers version detected. " + "LEANN's ColQwen/ColPali path is not tested with transformers 5.x " + "(e.g. API removals such as HybridCache). " + "Install a 4.x release that satisfies colpali_engine, e.g. " + '`pip install "transformers>=4.46.1,<5"`.' + ) + + def _get_device(self): + """Auto-select best available device.""" + if torch.cuda.is_available(): + return torch.device("cuda") + elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + return torch.device("mps") + else: + return torch.device("cpu") + + def build_index(self, pdf_paths: list[str], index_name: str, pages_dir: Optional[str] = None): + """ + Build multimodal index from PDF files. + + Args: + pdf_paths: List of PDF file paths + index_name: Name for the index + pages_dir: Directory to save page images (optional) + """ + print(f"Building index '{index_name}' from {len(pdf_paths)} PDFs...") + + # Convert PDFs to images + all_images = [] + all_metadata = [] + + if pages_dir: + os.makedirs(pages_dir, exist_ok=True) + + for pdf_path in tqdm(pdf_paths, desc="Converting PDFs"): + try: + images = convert_from_path(pdf_path, dpi=150) + pdf_name = Path(pdf_path).stem + + for i, image in enumerate(images): + # Save image if pages_dir specified + if pages_dir: + image_path = Path(pages_dir) / f"{pdf_name}_page_{i + 1}.png" + image.save(image_path) + + all_images.append(image) + all_metadata.append( + { + "pdf_path": pdf_path, + "pdf_name": pdf_name, + "page_number": i + 1, + "image_path": str(image_path) if pages_dir else None, + } + ) + + except Exception as e: + print(f"❌ Error processing {pdf_path}: {e}") + continue + + print(f"πŸ“„ Converted {len(all_images)} pages from {len(pdf_paths)} PDFs") + if len(all_images) == 0: + raise RuntimeError( + "No PDF pages were converted to images, so there is nothing to embed.\n" + "Common causes:\n" + "- `poppler`/`pdftoppm` is missing (required by `pdf2image`)\n" + "- The input PDFs are encrypted/corrupt or have zero pages\n\n" + "Try:\n" + "- Install poppler (macOS: `brew install poppler`, Ubuntu: `apt-get install poppler-utils`)\n" + "- Re-run with a known-good PDF\n" + ) + + # Generate embeddings + print("🧠 Generating embeddings...") + embeddings = self._embed_images(all_images) + + # Build LEANN index + print("πŸ” Building LEANN index...") + leann_mv = LeannMultiVector( + index_path=index_name, + dim=embeddings.shape[-1], + embedding_model_name=self.model_type, + ) + + # Create collection and insert data + leann_mv.create_collection() + for i, (embedding, metadata) in enumerate(zip(embeddings, all_metadata)): + data = { + "doc_id": i, + "filepath": metadata.get("image_path", ""), + "colbert_vecs": embedding.numpy(), # Convert tensor to numpy + } + leann_mv.insert(data) + + # Build the index + leann_mv.create_index() + print(f"βœ… Index '{index_name}' built successfully!") + + return leann_mv + + def search(self, index_name: str, query: str, top_k: int = 5): + """ + Search the index with a text query. + + Args: + index_name: Name of the index to search + query: Text query + top_k: Number of results to return + """ + print(f"πŸ” Searching '{index_name}' for: '{query}'") + + # Load index + leann_mv = LeannMultiVector( + index_path=index_name, + dim=128, # Will be updated when loading + embedding_model_name=self.model_type, + ) + + # Generate query embedding + query_embedding = self._embed_query(query) + + # Search (returns list of (score, doc_id) tuples) + search_results = leann_mv.search(query_embedding.numpy(), topk=top_k) + + # Display results + print(f"\nπŸ“‹ Top {len(search_results)} results:") + for i, (score, doc_id) in enumerate(search_results, 1): + # Get metadata for this doc_id (we need to load the metadata) + print(f"{i}. Score: {score:.3f} | Doc ID: {doc_id}") + + return search_results + + def ask(self, index_name: str, interactive: bool = False): + """ + Interactive Q&A with the indexed documents. + + Args: + index_name: Name of the index to query + interactive: Whether to run in interactive mode + """ + print(f"πŸ’¬ ColQwen Chat with '{index_name}'") + + if interactive: + print("Type 'quit' to exit, 'help' for commands") + while True: + try: + query = input("\nπŸ€” Your question: ").strip() + if query.lower() in ["quit", "exit", "q"]: + break + elif query.lower() == "help": + print("Commands: quit/exit/q (exit), help (this message)") + continue + elif not query: + continue + + self.search(index_name, query, top_k=3) + + # TODO: Add answer generation with Qwen-VL + print("\nπŸ’‘ For detailed answers, we can integrate Qwen-VL here!") + + except KeyboardInterrupt: + print("\nπŸ‘‹ Goodbye!") + break + else: + query = input("πŸ€” Your question: ").strip() + if query: + self.search(index_name, query) + + def _embed_images(self, images: list[Image.Image]) -> torch.Tensor: + """Generate embeddings for a list of images.""" + if not images: + raise RuntimeError("No images provided for embedding.") + + dataset = self._list_dataset_cls(images) + dataloader = DataLoader(dataset, batch_size=1, shuffle=False, collate_fn=lambda x: x) + + embeddings = [] + with torch.no_grad(): + for batch in tqdm(dataloader, desc="Embedding images"): + batch_images = cast(list, batch) + batch_inputs = self.processor.process_images(batch_images).to(self.device) + batch_embeddings = self.model(**batch_inputs) + embeddings.append(batch_embeddings.cpu()) + + if not embeddings: + raise RuntimeError( + "Image embedding produced no tensors (empty embedding list). " + "This usually indicates that no images were processed successfully." + ) + + return torch.cat(embeddings, dim=0) + + def _embed_query(self, query: str) -> torch.Tensor: + """Generate embedding for a text query.""" + with torch.no_grad(): + query_inputs = self.processor.process_queries([query]).to(self.device) + query_embedding = self.model(**query_inputs) + return query_embedding.cpu() + + +def main(): + parser = argparse.ArgumentParser(description="ColQwen RAG - Easy multimodal PDF retrieval") + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Build command + build_parser = subparsers.add_parser("build", help="Build index from PDFs") + build_parser.add_argument("--pdfs", required=True, help="Directory containing PDF files") + build_parser.add_argument("--index", required=True, help="Index name") + build_parser.add_argument( + "--model", choices=["colqwen2", "colpali"], default="colqwen2", help="Model to use" + ) + build_parser.add_argument("--pages-dir", help="Directory to save page images") + + # Search command + search_parser = subparsers.add_parser("search", help="Search the index") + search_parser.add_argument("index", help="Index name") + search_parser.add_argument("query", help="Search query") + search_parser.add_argument("--top-k", type=int, default=5, help="Number of results") + search_parser.add_argument( + "--model", choices=["colqwen2", "colpali"], default="colqwen2", help="Model to use" + ) + + # Ask command + ask_parser = subparsers.add_parser("ask", help="Interactive Q&A") + ask_parser.add_argument("index", help="Index name") + ask_parser.add_argument("--interactive", action="store_true", help="Interactive mode") + ask_parser.add_argument( + "--model", choices=["colqwen2", "colpali"], default="colqwen2", help="Model to use" + ) + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + # Initialize ColQwen RAG + if args.command == "build": + colqwen = ColQwenRAG(args.model) + + # Get PDF files + pdf_dir = Path(args.pdfs) + if pdf_dir.is_file() and pdf_dir.suffix.lower() == ".pdf": + pdf_paths = [str(pdf_dir)] + elif pdf_dir.is_dir(): + pdf_paths = [str(p) for p in pdf_dir.glob("*.pdf")] + else: + print(f"❌ Invalid PDF path: {args.pdfs}") + return + + if not pdf_paths: + print(f"❌ No PDF files found in {args.pdfs}") + return + + colqwen.build_index(pdf_paths, args.index, args.pages_dir) + + elif args.command == "search": + colqwen = ColQwenRAG(args.model) + colqwen.search(args.index, args.query, args.top_k) + + elif args.command == "ask": + colqwen = ColQwenRAG(args.model) + colqwen.ask(args.index, args.interactive) + + +if __name__ == "__main__": + main() diff --git a/apps/document_rag.py b/apps/document_rag.py new file mode 100644 index 0000000..fae1860 --- /dev/null +++ b/apps/document_rag.py @@ -0,0 +1,126 @@ +""" +Document RAG example using the unified interface. +Supports PDF, TXT, MD, and other document formats. +""" + +import sys +from pathlib import Path +from typing import Any + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from base_rag_example import BaseRAGExample +from chunking import create_text_chunks +from llama_index.core import SimpleDirectoryReader + + +class DocumentRAG(BaseRAGExample): + """RAG example for document processing (PDF, TXT, MD, etc.).""" + + def __init__(self): + super().__init__( + name="Document", + description="Process and query documents (PDF, TXT, MD, etc.) with LEANN", + default_index_name="test_doc_files", + ) + + def _add_specific_arguments(self, parser): + """Add document-specific arguments.""" + doc_group = parser.add_argument_group("Document Parameters") + doc_group.add_argument( + "--data-dir", + type=str, + default="data", + help="Directory containing documents to index (default: data)", + ) + doc_group.add_argument( + "--file-types", + nargs="+", + default=None, + help="Filter by file types (e.g., .pdf .txt .md). If not specified, all supported types are processed", + ) + doc_group.add_argument( + "--chunk-size", type=int, default=256, help="Text chunk size (default: 256)" + ) + doc_group.add_argument( + "--chunk-overlap", type=int, default=128, help="Text chunk overlap (default: 128)" + ) + doc_group.add_argument( + "--enable-code-chunking", + action="store_true", + help="Enable AST-aware chunking for code files in the data directory", + ) + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load documents and convert to text chunks.""" + print(f"Loading documents from: {args.data_dir}") + if args.file_types: + print(f"Filtering by file types: {args.file_types}") + else: + print("Processing all supported file types") + + # Check if data directory exists + data_path = Path(args.data_dir) + if not data_path.exists(): + raise ValueError(f"Data directory not found: {args.data_dir}") + + # Load documents + documents = SimpleDirectoryReader( + args.data_dir, + recursive=True, + encoding="utf-8", + required_exts=args.file_types if args.file_types else None, + ).load_data(show_progress=True) + + if not documents: + print(f"No documents found in {args.data_dir} with extensions {args.file_types}") + return [] + + print(f"Loaded {len(documents)} documents") + + # Determine chunking strategy + use_ast = args.enable_code_chunking or getattr(args, "use_ast_chunking", False) + + if use_ast: + print("Using AST-aware chunking for code files") + + # Convert to text chunks with optional AST support + all_texts = create_text_chunks( + documents, + chunk_size=args.chunk_size, + chunk_overlap=args.chunk_overlap, + use_ast_chunking=use_ast, + ast_chunk_size=getattr(args, "ast_chunk_size", 512), + ast_chunk_overlap=getattr(args, "ast_chunk_overlap", 64), + code_file_extensions=getattr(args, "code_file_extensions", None), + ast_fallback_traditional=getattr(args, "ast_fallback_traditional", True), + ) + + # Apply max_items limit if specified + if args.max_items > 0 and len(all_texts) > args.max_items: + print(f"Limiting to {args.max_items} chunks (from {len(all_texts)})") + all_texts = all_texts[: args.max_items] + + return all_texts + + +if __name__ == "__main__": + import asyncio + + # Example queries for document RAG + print("\nDocument RAG Example") + print("=" * 50) + print("\nExample queries you can try:") + print("- 'What are the main techniques LEANN uses?'") + print("- 'What is the technique DLPM?'") + print("- 'Who does Elizabeth Bennet marry?'") + print("- 'What challenges did Huawei face while developing the Pangu model?'") + print("\nNEW: Code-aware chunking available!") + print("- Use --enable-code-chunking to enable AST-aware chunking for code files") + print("- Supports Python, Java, C#, TypeScript files") + print("- Better semantic understanding of code structure") + print("\nOr run without --query for interactive mode\n") + + rag = DocumentRAG() + asyncio.run(rag.run()) diff --git a/apps/email_data/LEANN_email_reader.py b/apps/email_data/LEANN_email_reader.py new file mode 100644 index 0000000..394e7d5 --- /dev/null +++ b/apps/email_data/LEANN_email_reader.py @@ -0,0 +1,174 @@ +import email +import os +from pathlib import Path +from typing import Any + +from llama_index.core import Document +from llama_index.core.readers.base import BaseReader + + +def find_all_messages_directories(root: str | None = None) -> list[Path]: + """ + Recursively find all 'Messages' directories under the given root. + Returns a list of Path objects. + """ + if root is None: + # Auto-detect user's mail path + home_dir = os.path.expanduser("~") + root = os.path.join(home_dir, "Library", "Mail") + + messages_dirs = [] + for dirpath, _dirnames, _filenames in os.walk(root): + if os.path.basename(dirpath) == "Messages": + messages_dirs.append(Path(dirpath)) + return messages_dirs + + +class EmlxReader(BaseReader): + """ + Apple Mail .emlx file reader with embedded metadata. + + Reads individual .emlx files from Apple Mail's storage format. + """ + + def __init__(self, include_html: bool = False) -> None: + """ + Initialize. + + Args: + include_html: Whether to include HTML content in the email body (default: False) + """ + self.include_html = include_html + + def _payload_to_text(self, payload: object) -> str: + if isinstance(payload, bytes): + return payload.decode("utf-8", errors="ignore") + if isinstance(payload, str): + return payload + return "" + + def load_data(self, input_dir: str, **load_kwargs: Any) -> list[Document]: + """ + Load data from the input directory containing .emlx files. + + Args: + input_dir: Directory containing .emlx files + **load_kwargs: + max_count (int): Maximum amount of messages to read. + """ + docs: list[Document] = [] + max_count = load_kwargs.get("max_count", 1000) + count = 0 + total_files = 0 + successful_files = 0 + failed_files = 0 + + print(f"Starting to process directory: {input_dir}") + + # Walk through the directory recursively + for dirpath, dirnames, filenames in os.walk(input_dir): + # Skip hidden directories + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + + for filename in filenames: + # Check if we've reached the max count (skip if max_count == -1) + if max_count > 0 and count >= max_count: + break + + if filename.endswith(".emlx"): + total_files += 1 + filepath = os.path.join(dirpath, filename) + try: + # Read the .emlx file + with open(filepath, encoding="utf-8", errors="ignore") as f: + content = f.read() + + # .emlx files have a length prefix followed by the email content + # The first line contains the length, followed by the email + lines = content.split("\n", 1) + if len(lines) >= 2: + email_content = lines[1] + + # Parse the email using Python's email module + try: + msg = email.message_from_string(email_content) + + # Extract email metadata + subject = msg.get("Subject", "No Subject") + from_addr = msg.get("From", "Unknown") + to_addr = msg.get("To", "Unknown") + date = msg.get("Date", "Unknown") + + # Extract email body + body = "" + if msg.is_multipart(): + for part in msg.walk(): + if ( + part.get_content_type() == "text/plain" + or part.get_content_type() == "text/html" + ): + if ( + part.get_content_type() == "text/html" + and not self.include_html + ): + continue + try: + payload = part.get_payload(decode=True) + if payload: + body += self._payload_to_text(payload) + except Exception as e: + print(f"Error decoding payload: {e}") + continue + else: + try: + payload = msg.get_payload(decode=True) + if payload: + body = self._payload_to_text(payload) + except Exception as e: + print(f"Error decoding single part payload: {e}") + body = "" + + # Only create document if we have some content + if body.strip() or subject != "No Subject": + # Create document content with metadata embedded in text + doc_content = f""" +[File]: {filename} +[From]: {from_addr} +[To]: {to_addr} +[Subject]: {subject} +[Date]: {date} +[EMAIL BODY Start]: +{body} +""" + + # No separate metadata - everything is in the text + doc = Document(text=doc_content, metadata={}) + docs.append(doc) + count += 1 + successful_files += 1 + + # Print first few successful files for debugging + if successful_files <= 3: + print( + f"Successfully loaded: {filename} - Subject: {subject[:50]}..." + ) + + except Exception as e: + failed_files += 1 + if failed_files <= 5: # Only print first few errors + print(f"Error parsing email from {filepath}: {e}") + continue + + except Exception as e: + failed_files += 1 + if failed_files <= 5: # Only print first few errors + print(f"Error reading file {filepath}: {e}") + continue + + print("Processing summary:") + print(f" Total .emlx files found: {total_files}") + print(f" Successfully loaded: {successful_files}") + print(f" Failed to load: {failed_files}") + print(f" Final documents: {len(docs)}") + + return docs diff --git a/apps/email_data/email.py b/apps/email_data/email.py new file mode 100644 index 0000000..9b22f19 --- /dev/null +++ b/apps/email_data/email.py @@ -0,0 +1,195 @@ +""" +Mbox parser. + +Contains simple parser for mbox files. + +""" + +import logging +from pathlib import Path +from typing import Any + +from fsspec import AbstractFileSystem +from llama_index.core.readers.base import BaseReader +from llama_index.core.schema import Document + +logger = logging.getLogger(__name__) + + +class MboxReader(BaseReader): + """ + Mbox parser. + + Extract messages from mailbox files. + Returns string including date, subject, sender, receiver and + content for each message. + + """ + + DEFAULT_MESSAGE_FORMAT: str = ( + "Date: {_date}\nFrom: {_from}\nTo: {_to}\nSubject: {_subject}\nContent: {_content}" + ) + + def __init__( + self, + *args: Any, + max_count: int = 0, + message_format: str = DEFAULT_MESSAGE_FORMAT, + **kwargs: Any, + ) -> None: + """Init params.""" + try: + from bs4 import BeautifulSoup # noqa + except ImportError: + raise ImportError("`beautifulsoup4` package not found: `pip install beautifulsoup4`") + + super().__init__(*args, **kwargs) + self.max_count = max_count + self.message_format = message_format + + def _payload_to_text(self, payload: object) -> str: + if isinstance(payload, bytes): + return payload.decode("utf-8", errors="ignore") + if isinstance(payload, str): + return payload + return "" + + def load_data( + self, + file: Path, + extra_info: dict | None = None, + fs: AbstractFileSystem | None = None, + ) -> list[Document]: + """Parse file into string.""" + # Import required libraries + import mailbox + from email.parser import BytesParser + from email.policy import default + + from bs4 import BeautifulSoup + + if fs: + logger.warning( + "fs was specified but MboxReader doesn't support loading " + "from fsspec filesystems. Will load from local filesystem instead." + ) + + i = 0 + results: list[str] = [] + # Load file using mailbox + bytes_parser = BytesParser(policy=default).parse + mbox = mailbox.mbox(file, factory=bytes_parser) # type: ignore + + # Iterate through all messages + for _, _msg in enumerate(mbox): + try: + msg: mailbox.mboxMessage = _msg + # Parse multipart messages + if msg.is_multipart(): + for part in msg.walk(): + ctype = part.get_content_type() + cdispo = str(part.get("Content-Disposition")) + if "attachment" in cdispo: + print(f"Attachment found: {part.get_filename()}") + if ctype == "text/plain" and "attachment" not in cdispo: + content = part.get_payload(decode=True) # decode + break + # Get plain message payload for non-multipart messages + else: + content = msg.get_payload(decode=True) + + # Parse message HTML content and remove unneeded whitespace + content_text = self._payload_to_text(content) + soup = BeautifulSoup(content_text) + stripped_content = " ".join(soup.get_text().split()) + # Format message to include date, sender, receiver and subject + msg_string = self.message_format.format( + _date=msg["date"], + _from=msg["from"], + _to=msg["to"], + _subject=msg["subject"], + _content=stripped_content, + ) + # Add message string to results + results.append(msg_string) + except Exception as e: + logger.warning(f"Failed to parse message:\n{_msg}\n with exception {e}") + + # Increment counter and return if max count is met + i += 1 + if self.max_count > 0 and i >= self.max_count: + break + + return [Document(text=result, metadata=extra_info or {}) for result in results] + + +class EmlxMboxReader(MboxReader): + """ + EmlxMboxReader - Modified MboxReader that handles directories of .emlx files. + + Extends MboxReader to work with Apple Mail's .emlx format by: + 1. Reading .emlx files from a directory + 2. Converting them to mbox format in memory + 3. Using the parent MboxReader's parsing logic + """ + + def load_data( + self, + file: Path, # Note: for EmlxMboxReader, this is actually a directory + extra_info: dict | None = None, + fs: AbstractFileSystem | None = None, + ) -> list[Document]: + """Parse .emlx files from directory into strings using MboxReader logic.""" + directory = file # Rename for clarity - this is a directory of .emlx files + import os + import tempfile + + if fs: + logger.warning( + "fs was specified but EmlxMboxReader doesn't support loading " + "from fsspec filesystems. Will load from local filesystem instead." + ) + + # Find all .emlx files in the directory + emlx_files = list(directory.glob("*.emlx")) + logger.info(f"Found {len(emlx_files)} .emlx files in {directory}") + + if not emlx_files: + logger.warning(f"No .emlx files found in {directory}") + return [] + + # Create a temporary mbox file + with tempfile.NamedTemporaryFile(mode="w", suffix=".mbox", delete=False) as temp_mbox: + temp_mbox_path = temp_mbox.name + + # Convert .emlx files to mbox format + for emlx_file in emlx_files: + try: + # Read the .emlx file + with open(emlx_file, encoding="utf-8", errors="ignore") as f: + content = f.read() + + # .emlx format: first line is length, rest is email content + lines = content.split("\n", 1) + if len(lines) >= 2: + email_content = lines[1] # Skip the length line + + # Write to mbox format (each message starts with "From " and ends with blank line) + temp_mbox.write(f"From {emlx_file.name} {email_content}\n\n") + + except Exception as e: + logger.warning(f"Failed to process {emlx_file}: {e}") + continue + + # Close the temporary file so MboxReader can read it + temp_mbox.close() + + try: + # Use the parent MboxReader's logic to parse the mbox file + return super().load_data(Path(temp_mbox_path), extra_info, fs) + finally: + # Clean up temporary file + try: + os.unlink(temp_mbox_path) + except OSError: + pass diff --git a/apps/email_rag.py b/apps/email_rag.py new file mode 100644 index 0000000..0558678 --- /dev/null +++ b/apps/email_rag.py @@ -0,0 +1,158 @@ +""" +Email RAG example using the unified interface. +Supports Apple Mail on macOS. +""" + +import sys +from pathlib import Path +from typing import Any + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from base_rag_example import BaseRAGExample +from chunking import create_text_chunks + +from .email_data.LEANN_email_reader import EmlxReader + + +class EmailRAG(BaseRAGExample): + """RAG example for Apple Mail processing.""" + + def __init__(self): + # Set default values BEFORE calling super().__init__ + self.max_items_default = -1 # Process all emails by default + self.embedding_model_default = ( + "sentence-transformers/all-MiniLM-L6-v2" # Fast 384-dim model + ) + + super().__init__( + name="Email", + description="Process and query Apple Mail emails with LEANN", + default_index_name="mail_index", + ) + + def _add_specific_arguments(self, parser): + """Add email-specific arguments.""" + email_group = parser.add_argument_group("Email Parameters") + email_group.add_argument( + "--mail-path", + type=str, + default=None, + help="Path to Apple Mail directory (auto-detected if not specified)", + ) + email_group.add_argument( + "--include-html", action="store_true", help="Include HTML content in email processing" + ) + email_group.add_argument( + "--chunk-size", type=int, default=256, help="Text chunk size (default: 256)" + ) + email_group.add_argument( + "--chunk-overlap", type=int, default=25, help="Text chunk overlap (default: 25)" + ) + + def _find_mail_directories(self) -> list[Path]: + """Auto-detect all Apple Mail directories.""" + mail_base = Path.home() / "Library" / "Mail" + if not mail_base.exists(): + return [] + + # Find all Messages directories + messages_dirs = [] + for item in mail_base.rglob("Messages"): + if item.is_dir(): + messages_dirs.append(item) + + return messages_dirs + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load emails and convert to text chunks.""" + # Determine mail directories + if args.mail_path: + messages_dirs = [Path(args.mail_path)] + else: + print("Auto-detecting Apple Mail directories...") + messages_dirs = self._find_mail_directories() + + if not messages_dirs: + print("No Apple Mail directories found!") + print("Please specify --mail-path manually") + return [] + + print(f"Found {len(messages_dirs)} mail directories") + + # Create reader + reader = EmlxReader(include_html=args.include_html) + + # Process each directory + all_documents = [] + total_processed = 0 + + for i, messages_dir in enumerate(messages_dirs): + print(f"\nProcessing directory {i + 1}/{len(messages_dirs)}: {messages_dir}") + + try: + # Count emlx files + emlx_files = list(messages_dir.glob("*.emlx")) + print(f"Found {len(emlx_files)} email files") + + # Apply max_items limit per directory + max_per_dir = -1 # Default to process all + if args.max_items > 0: + remaining = args.max_items - total_processed + if remaining <= 0: + break + max_per_dir = remaining + # If args.max_items == -1, max_per_dir stays -1 (process all) + + # Load emails - fix the parameter passing + documents = reader.load_data( + input_dir=str(messages_dir), + max_count=max_per_dir, + ) + + if documents: + all_documents.extend(documents) + total_processed += len(documents) + print(f"Processed {len(documents)} emails from this directory") + + except Exception as e: + print(f"Error processing {messages_dir}: {e}") + continue + + if not all_documents: + print("No emails found to process!") + return [] + + print(f"\nTotal emails processed: {len(all_documents)}") + print("now starting to split into text chunks ... take some time") + + # Convert to text chunks + # Email reader uses chunk_overlap=25 as in original + all_texts = create_text_chunks( + all_documents, chunk_size=args.chunk_size, chunk_overlap=args.chunk_overlap + ) + + return all_texts + + +if __name__ == "__main__": + import asyncio + + # Check platform + if sys.platform != "darwin": + print("\n⚠️ Warning: This example is designed for macOS (Apple Mail)") + print(" Windows/Linux support coming soon!\n") + + # Example queries for email RAG + print("\nπŸ“§ Email RAG Example") + print("=" * 50) + print("\nExample queries you can try:") + print("- 'What did my boss say about deadlines?'") + print("- 'Find emails about travel expenses'") + print("- 'Show me emails from last month about the project'") + print("- 'What food did I order from DoorDash?'") + print("\nNote: You may need to grant Full Disk Access to your terminal\n") + + rag = EmailRAG() + asyncio.run(rag.run()) diff --git a/apps/gemini_data/__init__.py b/apps/gemini_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/gemini_data/gemini_reader.py b/apps/gemini_data/gemini_reader.py new file mode 100644 index 0000000..a4e23b8 --- /dev/null +++ b/apps/gemini_data/gemini_reader.py @@ -0,0 +1,149 @@ +import json +import logging +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + + +class GeminiReader: + """Reader for Gemini CLI history files.""" + + def __init__(self): + pass + + def load_data(self, history_dir: str, max_count: int = -1) -> list[dict[str, Any]]: + """ + Load data from Gemini history directory. + + Args: + history_dir: Path to .gemini directory + max_count: Max number of conversations to load + + Returns: + List of dictionaries with 'text' and 'metadata' keys + """ + history_path = Path(history_dir).expanduser() + if not history_path.exists(): + print(f"Gemini history directory not found: {history_path}") + return [] + + documents = [] + + # 1. Load Memory (GEMINI.md) + memory_file = history_path / "GEMINI.md" + if memory_file.exists(): + try: + text = memory_file.read_text(encoding="utf-8") + if text.strip(): + documents.append( + { + "text": f"Gemini Memory:\n{text}", + "metadata": {"source": str(memory_file), "type": "memory"}, + } + ) + except Exception as e: + print(f"Error reading memory file: {e}") + + # 2. Find Session Files + # Legacy JSON sessions + session_files = list(history_path.glob("session-*.json")) + # New JSONL sessions + session_files.extend(list(history_path.glob("session-*.jsonl"))) + # Checkpoints + session_files.extend(list(history_path.glob("checkpoint-*.json"))) + + # Sort by modification time (newest first) + session_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) + + print(f"Found {len(session_files)} session files.") + + count = 0 + for file_path in session_files: + if max_count > 0 and count >= max_count: + break + + try: + content = "" + if file_path.suffix == ".jsonl": + content = self._parse_jsonl_session(file_path) + elif file_path.suffix == ".json": + content = self._parse_json_session(file_path) + + if content: + documents.append( + { + "text": content, + "metadata": { + "source": str(file_path), + "type": "session", + "filename": file_path.name, + }, + } + ) + count += 1 + except Exception as e: + print(f"Error reading {file_path.name}: {e}") + + print(f"Successfully loaded {len(documents)} items from Gemini history.") + return documents + + def _parse_json_session(self, file_path: Path) -> str: + """Parse legacy JSON session file.""" + data = json.loads(file_path.read_text(encoding="utf-8")) + + # Handle dict format (standard session) + messages = [] + if isinstance(data, dict): + # Check for 'messages' key (standard format) + if "messages" in data: + for msg in data["messages"]: + role = msg.get("role", "unknown") + content = msg.get("content", "") + if content: + messages.append(f"{role.upper()}: {content}") + # Check for 'parts' key (checkpoint format sometimes) + elif "parts" in data: + messages.append(f"Saved Session Content: {data['parts']}") + + # Handle list format (some older array-based sessions) + elif isinstance(data, list): + for item in data: + if isinstance(item, dict): + role = item.get("role", "unknown") + content = item.get("content", "") or item.get("parts", "") + if content: + messages.append(f"{role.upper()}: {content}") + + if not messages: + return "" + + return f"File: {file_path.name}\n\n" + "\n\n".join(messages) + + def _parse_jsonl_session(self, file_path: Path) -> str: + """Parse JSONL session file.""" + messages = [] + try: + with open(file_path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + data = json.loads(line) + # Skip metadata lines if they don't have content + if "role" in data and "content" in data: + messages.append(f"{data['role'].upper()}: {data['content']}") + elif "parts" in data: # sometimes parts is used + messages.append( + f"{data.get('role', 'unknown').upper()}: {data['parts']}" + ) + except json.JSONDecodeError: + continue + except Exception: + return "" + + if not messages: + return "" + + return f"File: {file_path.name}\n\n" + "\n\n".join(messages) diff --git a/apps/gemini_rag.py b/apps/gemini_rag.py new file mode 100644 index 0000000..9578850 --- /dev/null +++ b/apps/gemini_rag.py @@ -0,0 +1,69 @@ +""" +Gemini CLI RAG example. +Indexes and searches Gemini CLI history (~/.gemini). +""" + +import sys +from pathlib import Path +from typing import Any + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from base_rag_example import BaseRAGExample +from chunking import create_text_chunks + +from .gemini_data.gemini_reader import GeminiReader + + +class GeminiRAG(BaseRAGExample): + """RAG example for Gemini CLI history.""" + + def __init__(self): + super().__init__( + name="Gemini CLI", + description="Process and query Gemini CLI history with LEANN", + default_index_name="gemini_index", + ) + + def _add_specific_arguments(self, parser): + """Add Gemini-specific arguments.""" + group = parser.add_argument_group("Gemini Parameters") + group.add_argument( + "--gemini-path", + type=str, + default="~/.gemini", + help="Path to .gemini directory (default: ~/.gemini)", + ) + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load Gemini history and convert to text chunks.""" + print(f"Loading Gemini history from: {args.gemini_path}") + + reader = GeminiReader() + documents = reader.load_data(history_dir=args.gemini_path, max_count=args.max_items) + + if not documents: + print("No documents found! Check if ~/.gemini exists and has history.") + return [] + + # Convert dicts to Document objects for chunking + from llama_index.core import Document + + docs = [Document(text=d["text"], metadata=d["metadata"]) for d in documents] + + # Convert to text chunks + print(f"splitting {len(documents)} documents into chunks...") + chunks = create_text_chunks(docs) + + return chunks + + +if __name__ == "__main__": + import asyncio + + print("\n✨ Gemini CLI RAG") + print("=" * 50) + + rag = GeminiRAG() + asyncio.run(rag.run()) diff --git a/apps/history_data/__init__.py b/apps/history_data/__init__.py new file mode 100644 index 0000000..d41184e --- /dev/null +++ b/apps/history_data/__init__.py @@ -0,0 +1,3 @@ +from .history import ChromeHistoryReader + +__all__ = ["ChromeHistoryReader"] diff --git a/apps/history_data/history.py b/apps/history_data/history.py new file mode 100644 index 0000000..db6cab9 --- /dev/null +++ b/apps/history_data/history.py @@ -0,0 +1,199 @@ +import os +import sqlite3 +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + +from llama_index.core import Document +from llama_index.core.readers.base import BaseReader + + +class ChromeHistoryReader(BaseReader): + """ + Chrome browser history reader that extracts browsing data from SQLite database. + + Reads Chrome history from the default Chrome profile location and creates documents + with embedded metadata similar to the email reader structure. + """ + + def __init__(self) -> None: + """Initialize.""" + pass + + def load_data(self, input_dir: str | None = None, **load_kwargs: Any) -> list[Document]: + """ + Load Chrome history data from the default Chrome profile location. + + Args: + input_dir: Not used for Chrome history (kept for compatibility) + **load_kwargs: + max_count (int): Maximum amount of history entries to read. + chrome_profile_path (str): Custom path to Chrome profile directory. + """ + docs: list[Document] = [] + max_count = load_kwargs.get("max_count", 1000) + chrome_profile_path = load_kwargs.get("chrome_profile_path", None) + + # Default Chrome profile path on macOS + if chrome_profile_path is None: + chrome_profile_path = os.path.expanduser( + "~/Library/Application Support/Google/Chrome/Default" + ) + + history_db_path = os.path.join(chrome_profile_path, "History") + + if not os.path.exists(history_db_path): + print(f"Chrome history database not found at: {history_db_path}") + return docs + + try: + # Connect to the Chrome history database + print(f"Connecting to database: {history_db_path}") + conn = sqlite3.connect(history_db_path) + cursor = conn.cursor() + + # Query to get browsing history with metadata (removed created_time column) + query = """ + SELECT + datetime(last_visit_time/1000000-11644473600,'unixepoch','localtime') as last_visit, + url, + title, + visit_count, + typed_count, + hidden + FROM urls + ORDER BY last_visit_time DESC + """ + + print(f"Executing query on database: {history_db_path}") + cursor.execute(query) + rows = cursor.fetchall() + print(f"Query returned {len(rows)} rows") + + count = 0 + for row in rows: + if count >= max_count and max_count > 0: + break + + last_visit, url, title, visit_count, typed_count, _hidden = row + + # Create document content with metadata embedded in text + # (kept in body so semantic search still sees these fields) + doc_content = f""" +[Title]: {title} +[URL of the page]: {url} +[Last visited time]: {last_visit} +[Visit times]: {visit_count} +[Typed times]: {typed_count} +""" + + # Also expose structured fields via metadata so they can be used + # with metadata_filters at search time (see docs/metadata_filtering.md). + domain = urlparse(url).netloc if url else "" + + doc = Document( + text=doc_content, + metadata={ + "title": (title or "")[0:150], + "url": url or "", + "domain": domain, + "last_visited": str(last_visit) if last_visit is not None else "", + "visit_count": int(visit_count) if visit_count is not None else 0, + "typed_count": int(typed_count) if typed_count is not None else 0, + }, + ) + docs.append(doc) + count += 1 + + conn.close() + print(f"Loaded {len(docs)} Chrome history documents") + + except Exception as e: + print(f"Error reading Chrome history: {e}") + # add you may need to close your browser to make the database file available + # also highlight in red + print( + "\033[91mYou may need to close your browser to make the database file available\033[0m" + ) + return docs + + return docs + + @staticmethod + def find_chrome_profiles() -> list[Path]: + """ + Find all Chrome profile directories. + + Returns: + List of Path objects pointing to Chrome profile directories + """ + chrome_base_path = Path(os.path.expanduser("~/Library/Application Support/Google/Chrome")) + profile_dirs = [] + + if not chrome_base_path.exists(): + print(f"Chrome directory not found at: {chrome_base_path}") + return profile_dirs + + # Find all profile directories + for profile_dir in chrome_base_path.iterdir(): + if profile_dir.is_dir() and profile_dir.name != "System Profile": + history_path = profile_dir / "History" + if history_path.exists(): + profile_dirs.append(profile_dir) + print(f"Found Chrome profile: {profile_dir}") + + print(f"Found {len(profile_dirs)} Chrome profiles") + return profile_dirs + + @staticmethod + def export_history_to_file( + output_file: str = "chrome_history_export.txt", max_count: int = 1000 + ): + """ + Export Chrome history to a text file using the same SQL query format. + + Args: + output_file: Path to the output file + max_count: Maximum number of entries to export + """ + chrome_profile_path = os.path.expanduser( + "~/Library/Application Support/Google/Chrome/Default" + ) + history_db_path = os.path.join(chrome_profile_path, "History") + + if not os.path.exists(history_db_path): + print(f"Chrome history database not found at: {history_db_path}") + return + + try: + conn = sqlite3.connect(history_db_path) + cursor = conn.cursor() + + query = """ + SELECT + datetime(last_visit_time/1000000-11644473600,'unixepoch','localtime') as last_visit, + url, + title, + visit_count, + typed_count, + hidden + FROM urls + ORDER BY last_visit_time DESC + LIMIT ? + """ + + cursor.execute(query, (max_count,)) + rows = cursor.fetchall() + + with open(output_file, "w", encoding="utf-8") as f: + for row in rows: + last_visit, url, title, visit_count, typed_count, hidden = row + f.write( + f"{last_visit}\t{url}\t{title}\t{visit_count}\t{typed_count}\t{hidden}\n" + ) + + conn.close() + print(f"Exported {len(rows)} history entries to {output_file}") + + except Exception as e: + print(f"Error exporting Chrome history: {e}") diff --git a/apps/history_data/wechat_history.py b/apps/history_data/wechat_history.py new file mode 100644 index 0000000..9c99f77 --- /dev/null +++ b/apps/history_data/wechat_history.py @@ -0,0 +1,776 @@ +import json +import os +import re +import subprocess +import time +from datetime import datetime +from pathlib import Path +from typing import Any + +from llama_index.core import Document +from llama_index.core.readers.base import BaseReader + + +class WeChatHistoryReader(BaseReader): + """ + WeChat chat history reader that extracts chat data from exported JSON files. + + Reads WeChat chat history from exported JSON files (from wechat-exporter tool) + and creates documents with embedded metadata similar to the Chrome history reader structure. + + Also includes utilities for automatic WeChat chat history export. + """ + + def __init__(self) -> None: + """Initialize.""" + self.packages_dir = Path(__file__).parent.parent.parent / "packages" + self.wechat_exporter_dir = self.packages_dir / "wechat-exporter" + self.wechat_decipher_dir = self.packages_dir / "wechat-decipher-macos" + + def check_wechat_running(self) -> bool: + """Check if WeChat is currently running.""" + try: + result = subprocess.run(["pgrep", "-f", "WeChat"], capture_output=True, text=True) + return result.returncode == 0 + except Exception: + return False + + def install_wechattweak(self) -> bool: + """Install WeChatTweak CLI tool.""" + try: + # Create wechat-exporter directory if it doesn't exist + self.wechat_exporter_dir.mkdir(parents=True, exist_ok=True) + + wechattweak_path = self.wechat_exporter_dir / "wechattweak-cli" + if not wechattweak_path.exists(): + print("Downloading WeChatTweak CLI...") + subprocess.run( + [ + "curl", + "-L", + "-o", + str(wechattweak_path), + "https://github.com/JettChenT/WeChatTweak-CLI/releases/latest/download/wechattweak-cli", + ], + check=True, + ) + + # Make executable + wechattweak_path.chmod(0o755) + + # Install WeChatTweak + print("Installing WeChatTweak...") + subprocess.run(["sudo", str(wechattweak_path), "install"], check=True) + return True + except Exception as e: + print(f"Error installing WeChatTweak: {e}") + return False + + def restart_wechat(self): + """Restart WeChat to apply WeChatTweak.""" + try: + print("Restarting WeChat...") + subprocess.run(["pkill", "-f", "WeChat"], check=False) + time.sleep(2) + subprocess.run(["open", "-a", "WeChat"], check=True) + time.sleep(5) # Wait for WeChat to start + except Exception as e: + print(f"Error restarting WeChat: {e}") + + def check_api_available(self) -> bool: + """Check if WeChatTweak API is available.""" + try: + result = subprocess.run( + ["curl", "-s", "http://localhost:48065/wechat/allcontacts"], + capture_output=True, + text=True, + timeout=5, + ) + return result.returncode == 0 and bool(result.stdout.strip()) + except Exception: + return False + + def _extract_readable_text(self, content: str) -> str: + """ + Extract readable text from message content, removing XML and system messages. + + Args: + content: The raw message content (can be string or dict) + + Returns: + Cleaned, readable text + """ + if not content: + return "" + + # Handle dictionary content (like quoted messages) + if isinstance(content, dict): + # Extract text from dictionary structure + text_parts = [] + if "title" in content: + text_parts.append(str(content["title"])) + if "quoted" in content: + text_parts.append(str(content["quoted"])) + if "content" in content: + text_parts.append(str(content["content"])) + if "text" in content: + text_parts.append(str(content["text"])) + + if text_parts: + return " | ".join(text_parts) + else: + # If we can't extract meaningful text from dict, return empty + return "" + + # Handle string content + if not isinstance(content, str): + return "" + + # Remove common prefixes like "wxid_xxx:\n" + clean_content = re.sub(r"^wxid_[^:]+:\s*", "", content) + clean_content = re.sub(r"^[^:]+:\s*", "", clean_content) + + # If it's just XML or system message, return empty + if clean_content.strip().startswith("<") or "recalled a message" in clean_content: + return "" + + return clean_content.strip() + + def _is_text_message(self, content: str) -> bool: + """ + Check if a message contains readable text content. + + Args: + content: The message content (can be string or dict) + + Returns: + True if the message contains readable text, False otherwise + """ + if not content: + return False + + # Handle dictionary content + if isinstance(content, dict): + # Check if dict has any readable text fields + text_fields = ["title", "quoted", "content", "text"] + for field in text_fields: + if content.get(field): + return True + return False + + # Handle string content + if not isinstance(content, str): + return False + + # Skip image messages (contain XML with img tags) + if " 0 and not clean_content.strip().startswith("<"): + return True + + return False + + def _concatenate_messages( + self, + messages: list[dict], + max_length: int = 128, + time_window_minutes: int = 30, + overlap_messages: int = 0, + ) -> list[dict]: + """ + Concatenate messages based on length and time rules. + + Args: + messages: List of message dictionaries + max_length: Maximum length for concatenated message groups. Use -1 to disable length constraint. + time_window_minutes: Time window in minutes to group messages together. Use -1 to disable time constraint. + overlap_messages: Number of messages to overlap between consecutive groups + + Returns: + List of concatenated message groups + """ + if not messages: + return [] + + concatenated_groups = [] + current_group = [] + current_length = 0 + last_timestamp = None + + for message in messages: + # Extract message info + content = message.get("content", "") + message_text = message.get("message", "") + create_time = message.get("createTime", 0) + message.get("fromUser", "") + message.get("toUser", "") + message.get("isSentFromSelf", False) + + # Extract readable text + readable_text = self._extract_readable_text(content) + if not readable_text: + readable_text = message_text + + # Skip empty messages + if not readable_text.strip(): + continue + + # Check time window constraint (only if time_window_minutes != -1) + if time_window_minutes != -1 and last_timestamp is not None and create_time > 0: + time_diff_minutes = (create_time - last_timestamp) / 60 + if time_diff_minutes > time_window_minutes: + # Time gap too large, start new group + if current_group: + concatenated_groups.append( + { + "messages": current_group, + "total_length": current_length, + "start_time": current_group[0].get("createTime", 0), + "end_time": current_group[-1].get("createTime", 0), + } + ) + # Keep last few messages for overlap + if overlap_messages > 0 and len(current_group) > overlap_messages: + current_group = current_group[-overlap_messages:] + current_length = sum( + len( + self._extract_readable_text(msg.get("content", "")) + or msg.get("message", "") + ) + for msg in current_group + ) + else: + current_group = [] + current_length = 0 + + # Check length constraint (only if max_length != -1) + message_length = len(readable_text) + if max_length != -1 and current_length + message_length > max_length and current_group: + # Current group would exceed max length, save it and start new + concatenated_groups.append( + { + "messages": current_group, + "total_length": current_length, + "start_time": current_group[0].get("createTime", 0), + "end_time": current_group[-1].get("createTime", 0), + } + ) + # Keep last few messages for overlap + if overlap_messages > 0 and len(current_group) > overlap_messages: + current_group = current_group[-overlap_messages:] + current_length = sum( + len( + self._extract_readable_text(msg.get("content", "")) + or msg.get("message", "") + ) + for msg in current_group + ) + else: + current_group = [] + current_length = 0 + + # Add message to current group + current_group.append(message) + current_length += message_length + last_timestamp = create_time + + # Add the last group if it exists + if current_group: + concatenated_groups.append( + { + "messages": current_group, + "total_length": current_length, + "start_time": current_group[0].get("createTime", 0), + "end_time": current_group[-1].get("createTime", 0), + } + ) + + return concatenated_groups + + def _create_concatenated_content( + self, message_group: dict, contact_name: str + ) -> tuple[str, str]: + """ + Create concatenated content from a group of messages. + + Args: + message_group: Dictionary containing messages and metadata + contact_name: Name of the contact + + Returns: + Formatted concatenated content + """ + messages = message_group["messages"] + start_time = message_group["start_time"] + end_time = message_group["end_time"] + + # Format timestamps + if start_time: + try: + start_timestamp = datetime.fromtimestamp(start_time) + start_time_str = start_timestamp.strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, OSError): + start_time_str = str(start_time) + else: + start_time_str = "Unknown" + + if end_time: + try: + end_timestamp = datetime.fromtimestamp(end_time) + end_time_str = end_timestamp.strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, OSError): + end_time_str = str(end_time) + else: + end_time_str = "Unknown" + + # Build concatenated message content + message_parts = [] + for message in messages: + content = message.get("content", "") + message_text = message.get("message", "") + create_time = message.get("createTime", 0) + is_sent_from_self = message.get("isSentFromSelf", False) + + # Extract readable text + readable_text = self._extract_readable_text(content) + if not readable_text: + readable_text = message_text + + # Format individual message + if create_time: + try: + timestamp = datetime.fromtimestamp(create_time) + # change to YYYY-MM-DD HH:MM:SS + time_str = timestamp.strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, OSError): + time_str = str(create_time) + else: + time_str = "Unknown" + + sender = "[Me]" if is_sent_from_self else "[Contact]" + message_parts.append(f"({time_str}) {sender}: {readable_text}") + + concatenated_text = "\n".join(message_parts) + + # Create final document content + doc_content = f""" +Contact: {contact_name} +Time Range: {start_time_str} - {end_time_str} +Messages ({len(messages)} messages, {message_group["total_length"]} chars): + +{concatenated_text} +""" + # TODO @yichuan give better format and rich info here! + doc_content = f""" +{concatenated_text} +""" + return doc_content, contact_name + + def load_data(self, input_dir: str | None = None, **load_kwargs: Any) -> list[Document]: + """ + Load WeChat chat history data from exported JSON files. + + Args: + input_dir: Directory containing exported WeChat JSON files + **load_kwargs: + max_count (int): Maximum amount of chat entries to read. + wechat_export_dir (str): Custom path to WeChat export directory. + include_non_text (bool): Whether to include non-text messages (images, emojis, etc.) + concatenate_messages (bool): Whether to concatenate messages based on length rules. + max_length (int): Maximum length for concatenated message groups (default: 1000). + time_window_minutes (int): Time window in minutes to group messages together (default: 30). + overlap_messages (int): Number of messages to overlap between consecutive groups (default: 2). + """ + docs: list[Document] = [] + max_count = load_kwargs.get("max_count", 1000) + wechat_export_dir = load_kwargs.get("wechat_export_dir", None) + include_non_text = load_kwargs.get("include_non_text", False) + concatenate_messages = load_kwargs.get("concatenate_messages", False) + max_length = load_kwargs.get("max_length", 1000) + time_window_minutes = load_kwargs.get("time_window_minutes", 30) + + # Default WeChat export path + if wechat_export_dir is None: + wechat_export_dir = "./wechat_export_test" + + if not os.path.exists(wechat_export_dir): + print(f"WeChat export directory not found at: {wechat_export_dir}") + return docs + + try: + # Find all JSON files in the export directory + json_files = list(Path(wechat_export_dir).glob("*.json")) + print(f"Found {len(json_files)} WeChat chat history files") + + count = 0 + for json_file in json_files: + if count >= max_count and max_count > 0: + break + + try: + with open(json_file, encoding="utf-8") as f: + chat_data = json.load(f) + + # Extract contact name from filename + contact_name = json_file.stem + + if concatenate_messages: + # Filter messages to only include readable text messages + readable_messages = [] + for message in chat_data: + try: + content = message.get("content", "") + if not include_non_text and not self._is_text_message(content): + continue + + readable_text = self._extract_readable_text(content) + if not readable_text and not include_non_text: + continue + + readable_messages.append(message) + except Exception as e: + print(f"Error processing message in {json_file}: {e}") + continue + + # Concatenate messages based on rules + message_groups = self._concatenate_messages( + readable_messages, + max_length=max_length, + time_window_minutes=time_window_minutes, + overlap_messages=0, # No overlap between groups + ) + + # Create documents from concatenated groups + for message_group in message_groups: + if count >= max_count and max_count > 0: + break + + doc_content, contact_name = self._create_concatenated_content( + message_group, contact_name + ) + doc = Document( + text=doc_content, + metadata={"contact_name": contact_name}, + ) + docs.append(doc) + count += 1 + + print( + f"Created {len(message_groups)} concatenated message groups for {contact_name}" + ) + + else: + # Original single-message processing + for message in chat_data: + if count >= max_count and max_count > 0: + break + + # Extract message information + message.get("fromUser", "") + message.get("toUser", "") + content = message.get("content", "") + message_text = message.get("message", "") + create_time = message.get("createTime", 0) + is_sent_from_self = message.get("isSentFromSelf", False) + + # Handle content that might be dict or string + try: + # Check if this is a readable text message + if not include_non_text and not self._is_text_message(content): + continue + + # Extract readable text + readable_text = self._extract_readable_text(content) + if not readable_text and not include_non_text: + continue + except Exception as e: + # Skip messages that cause processing errors + print(f"Error processing message in {json_file}: {e}") + continue + + # Convert timestamp to readable format + if create_time: + try: + timestamp = datetime.fromtimestamp(create_time) + time_str = timestamp.strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, OSError): + time_str = str(create_time) + else: + time_str = "Unknown" + + # Create document content with metadata header and contact info + doc_content = f""" +Contact: {contact_name} +Is sent from self: {is_sent_from_self} +Time: {time_str} +Message: {readable_text if readable_text else message_text} +""" + + # Create document with embedded metadata + doc = Document( + text=doc_content, metadata={"contact_name": contact_name} + ) + docs.append(doc) + count += 1 + + except Exception as e: + print(f"Error reading {json_file}: {e}") + continue + + print(f"Loaded {len(docs)} WeChat chat documents") + + except Exception as e: + print(f"Error reading WeChat history: {e}") + return docs + + return docs + + @staticmethod + def find_wechat_export_dirs() -> list[Path]: + """ + Find all WeChat export directories. + + Returns: + List of Path objects pointing to WeChat export directories + """ + export_dirs = [] + + # Look for common export directory names + possible_dirs = [ + Path("./wechat_export"), + Path("./wechat_export_direct"), + Path("./wechat_chat_history"), + Path("./chat_export"), + ] + + for export_dir in possible_dirs: + if export_dir.exists() and export_dir.is_dir(): + json_files = list(export_dir.glob("*.json")) + if json_files: + export_dirs.append(export_dir) + print( + f"Found WeChat export directory: {export_dir} with {len(json_files)} files" + ) + + print(f"Found {len(export_dirs)} WeChat export directories") + return export_dirs + + @staticmethod + def export_chat_to_file( + output_file: str = "wechat_chat_export.txt", + max_count: int = 1000, + export_dir: str | None = None, + include_non_text: bool = False, + ): + """ + Export WeChat chat history to a text file. + + Args: + output_file: Path to the output file + max_count: Maximum number of entries to export + export_dir: Directory containing WeChat JSON files + include_non_text: Whether to include non-text messages + """ + if export_dir is None: + export_dir = "./wechat_export_test" + + if not os.path.exists(export_dir): + print(f"WeChat export directory not found at: {export_dir}") + return + + try: + json_files = list(Path(export_dir).glob("*.json")) + + with open(output_file, "w", encoding="utf-8") as f: + count = 0 + for json_file in json_files: + if count >= max_count and max_count > 0: + break + + try: + with open(json_file, encoding="utf-8") as json_f: + chat_data = json.load(json_f) + + contact_name = json_file.stem + f.write(f"\n=== Chat with {contact_name} ===\n") + + for message in chat_data: + if count >= max_count and max_count > 0: + break + + from_user = message.get("fromUser", "") + content = message.get("content", "") + message_text = message.get("message", "") + create_time = message.get("createTime", 0) + + # Skip non-text messages unless requested + if not include_non_text: + reader = WeChatHistoryReader() + if not reader._is_text_message(content): + continue + readable_text = reader._extract_readable_text(content) + if not readable_text: + continue + message_text = readable_text + + if create_time: + try: + timestamp = datetime.fromtimestamp(create_time) + time_str = timestamp.strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, OSError): + time_str = str(create_time) + else: + time_str = "Unknown" + + f.write(f"[{time_str}] {from_user}: {message_text}\n") + count += 1 + + except Exception as e: + print(f"Error processing {json_file}: {e}") + continue + + print(f"Exported {count} chat entries to {output_file}") + + except Exception as e: + print(f"Error exporting WeChat chat history: {e}") + + def export_wechat_chat_history(self, export_dir: str = "./wechat_export_direct") -> Path | None: + """ + Export WeChat chat history using wechat-exporter tool. + + Args: + export_dir: Directory to save exported chat history + + Returns: + Path to export directory if successful, None otherwise + """ + try: + import subprocess + import sys + + # Create export directory + export_path = Path(export_dir) + export_path.mkdir(exist_ok=True) + + print(f"Exporting WeChat chat history to {export_path}...") + + # Check if wechat-exporter directory exists + if not self.wechat_exporter_dir.exists(): + print(f"wechat-exporter directory not found at: {self.wechat_exporter_dir}") + return None + + # Install requirements if needed + requirements_file = self.wechat_exporter_dir / "requirements.txt" + if requirements_file.exists(): + print("Installing wechat-exporter requirements...") + subprocess.run(["uv", "pip", "install", "-r", str(requirements_file)], check=True) + + # Run the export command + print("Running wechat-exporter...") + result = subprocess.run( + [ + sys.executable, + str(self.wechat_exporter_dir / "main.py"), + "export-all", + str(export_path), + ], + capture_output=True, + text=True, + check=True, + ) + + print("Export command output:") + print(result.stdout) + if result.stderr: + print("Export errors:") + print(result.stderr) + + # Check if export was successful + if export_path.exists() and any(export_path.glob("*.json")): + json_files = list(export_path.glob("*.json")) + print( + f"Successfully exported {len(json_files)} chat history files to {export_path}" + ) + return export_path + else: + print("Export completed but no JSON files found") + return None + + except subprocess.CalledProcessError as e: + print(f"Export command failed: {e}") + print(f"Command output: {e.stdout}") + print(f"Command errors: {e.stderr}") + return None + except Exception as e: + print(f"Export failed: {e}") + print("Please ensure WeChat is running and WeChatTweak is installed.") + return None + + def find_or_export_wechat_data(self, export_dir: str = "./wechat_export_direct") -> list[Path]: + """ + Find existing WeChat exports or create new ones. + + Args: + export_dir: Directory to save exported chat history if needed + + Returns: + List of Path objects pointing to WeChat export directories + """ + export_dirs = [] + + # Look for existing exports in common locations + possible_export_dirs = [ + Path("./wechat_database_export"), + Path("./wechat_export_test"), + Path("./wechat_export"), + Path("./wechat_export_direct"), + Path("./wechat_chat_history"), + Path("./chat_export"), + ] + + for export_dir_path in possible_export_dirs: + if export_dir_path.exists() and any(export_dir_path.glob("*.json")): + export_dirs.append(export_dir_path) + print(f"Found existing export: {export_dir_path}") + + # If no existing exports, try to export automatically + if not export_dirs: + print("No existing WeChat exports found. Starting direct export...") + + # Try to export using wechat-exporter + exported_path = self.export_wechat_chat_history(export_dir) + if exported_path: + export_dirs = [exported_path] + else: + print( + "Failed to export WeChat data. Please ensure WeChat is running and WeChatTweak is installed." + ) + + return export_dirs diff --git a/apps/image_rag.py b/apps/image_rag.py new file mode 100644 index 0000000..8dcd62b --- /dev/null +++ b/apps/image_rag.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +""" +CLIP Image RAG Application + +This application enables RAG (Retrieval-Augmented Generation) on images using CLIP embeddings. +You can index a directory of images and search them using text queries. + +Usage: + python -m apps.image_rag --image-dir ./my_images/ --query "a sunset over mountains" + python -m apps.image_rag --image-dir ./my_images/ --interactive +""" + +import argparse +import pickle +import tempfile +from pathlib import Path +from typing import Any + +import numpy as np +from PIL import Image +from sentence_transformers import SentenceTransformer +from tqdm import tqdm + +from apps.base_rag_example import BaseRAGExample + + +class ImageRAG(BaseRAGExample): + """ + RAG application for images using CLIP embeddings. + + This class provides a complete RAG pipeline for image data, including + CLIP embedding generation, indexing, and text-based image search. + """ + + def __init__(self): + super().__init__( + name="Image RAG", + description="RAG application for images using CLIP embeddings", + default_index_name="image_index", + ) + # Override default embedding model to use CLIP + self.embedding_model_default = "clip-ViT-L-14" + self.embedding_mode_default = "sentence-transformers" + self._image_data: list[dict] = [] + + def _add_specific_arguments(self, parser: argparse.ArgumentParser): + """Add image-specific arguments.""" + image_group = parser.add_argument_group("Image Parameters") + image_group.add_argument( + "--image-dir", + type=str, + required=True, + help="Directory containing images to index", + ) + image_group.add_argument( + "--image-extensions", + type=str, + nargs="+", + default=[".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"], + help="Image file extensions to process (default: .jpg .jpeg .png .gif .bmp .webp)", + ) + image_group.add_argument( + "--batch-size", + type=int, + default=32, + help="Batch size for CLIP embedding generation (default: 32)", + ) + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load images, generate CLIP embeddings, and return text descriptions.""" + self._image_data = self._load_images_and_embeddings(args) + return [entry["text"] for entry in self._image_data] + + def _load_images_and_embeddings(self, args) -> list[dict]: + """Helper to process images and produce embeddings/metadata.""" + image_dir = Path(args.image_dir) + if not image_dir.exists(): + raise ValueError(f"Image directory does not exist: {image_dir}") + + print(f"πŸ“Έ Loading images from {image_dir}...") + + # Find all image files + image_files = [] + for ext in args.image_extensions: + image_files.extend(image_dir.rglob(f"*{ext}")) + image_files.extend(image_dir.rglob(f"*{ext.upper()}")) + + if not image_files: + raise ValueError( + f"No images found in {image_dir} with extensions {args.image_extensions}" + ) + + print(f"βœ… Found {len(image_files)} images") + + # Limit if max_items is set + if args.max_items > 0: + image_files = image_files[: args.max_items] + print(f"πŸ“Š Processing {len(image_files)} images (limited by --max-items)") + + # Load CLIP model + print("πŸ” Loading CLIP model...") + model = SentenceTransformer(self.embedding_model_default) + + # Process images and generate embeddings + print("πŸ–ΌοΈ Processing images and generating embeddings...") + image_data = [] + batch_images = [] + batch_paths = [] + + for image_path in tqdm(image_files, desc="Processing images"): + try: + image = Image.open(image_path).convert("RGB") + batch_images.append(image) + batch_paths.append(image_path) + + # Process in batches + if len(batch_images) >= args.batch_size: + embeddings = model.encode( + batch_images, + convert_to_numpy=True, + normalize_embeddings=True, + batch_size=args.batch_size, + show_progress_bar=False, + ) + + for img_path, embedding in zip(batch_paths, embeddings): + image_data.append( + { + "text": f"Image: {img_path.name}\nPath: {img_path}", + "metadata": { + "image_path": str(img_path), + "image_name": img_path.name, + "image_dir": str(image_dir), + }, + "embedding": embedding.astype(np.float32), + } + ) + + batch_images = [] + batch_paths = [] + + except Exception as e: + print(f"⚠️ Failed to process {image_path}: {e}") + continue + + # Process remaining images + if batch_images: + embeddings = model.encode( + batch_images, + convert_to_numpy=True, + normalize_embeddings=True, + batch_size=len(batch_images), + show_progress_bar=False, + ) + + for img_path, embedding in zip(batch_paths, embeddings): + image_data.append( + { + "text": f"Image: {img_path.name}\nPath: {img_path}", + "metadata": { + "image_path": str(img_path), + "image_name": img_path.name, + "image_dir": str(image_dir), + }, + "embedding": embedding.astype(np.float32), + } + ) + + print(f"βœ… Processed {len(image_data)} images") + return image_data + + async def build_index(self, args, texts: list[dict[str, Any]]) -> str: + """Build index using pre-computed CLIP embeddings.""" + from leann.api import LeannBuilder + + if not self._image_data or len(self._image_data) != len(texts): + raise RuntimeError("No image data found. Make sure load_data() ran successfully.") + + print("πŸ”¨ Building LEANN index with CLIP embeddings...") + builder = LeannBuilder( + backend_name=args.backend_name, + embedding_model=self.embedding_model_default, + embedding_mode=self.embedding_mode_default, + is_recompute=False, + distance_metric="cosine", + graph_degree=args.graph_degree, + build_complexity=args.build_complexity, + is_compact=not args.no_compact, + ) + + for text, data in zip(texts, self._image_data): + builder.add_text(text=text, metadata=data["metadata"]) + + ids = [str(i) for i in range(len(self._image_data))] + embeddings = np.array([data["embedding"] for data in self._image_data], dtype=np.float32) + + with tempfile.NamedTemporaryFile(mode="wb", suffix=".pkl", delete=False) as f: + pickle.dump((ids, embeddings), f) + pkl_path = f.name + + try: + index_path = str(Path(args.index_dir) / f"{self.default_index_name}.leann") + builder.build_index_from_embeddings(index_path, pkl_path) + print(f"βœ… Index built successfully at {index_path}") + return index_path + finally: + Path(pkl_path).unlink() + + +def main(): + """Main entry point for the image RAG application.""" + import asyncio + + app = ImageRAG() + asyncio.run(app.run()) + + +if __name__ == "__main__": + main() diff --git a/apps/imessage_data/__init__.py b/apps/imessage_data/__init__.py new file mode 100644 index 0000000..9e9e3fc --- /dev/null +++ b/apps/imessage_data/__init__.py @@ -0,0 +1 @@ +"""iMessage data processing module.""" diff --git a/apps/imessage_data/imessage_reader.py b/apps/imessage_data/imessage_reader.py new file mode 100644 index 0000000..4dfc0af --- /dev/null +++ b/apps/imessage_data/imessage_reader.py @@ -0,0 +1,342 @@ +""" +iMessage data reader. + +Reads and processes iMessage conversation data from the macOS Messages database. +""" + +import sqlite3 +from datetime import datetime +from pathlib import Path +from typing import Any + +from llama_index.core import Document +from llama_index.core.readers.base import BaseReader + + +class IMessageReader(BaseReader): + """ + iMessage data reader. + + Reads iMessage conversation data from the macOS Messages database (chat.db). + Processes conversations into structured documents with metadata. + """ + + def __init__(self, concatenate_conversations: bool = True) -> None: + """ + Initialize. + + Args: + concatenate_conversations: Whether to concatenate messages within conversations for better context + """ + self.concatenate_conversations = concatenate_conversations + + def _get_default_chat_db_path(self) -> Path: + """ + Get the default path to the iMessage chat database. + + Returns: + Path to the chat.db file + """ + home = Path.home() + return home / "Library" / "Messages" / "chat.db" + + def _convert_cocoa_timestamp(self, cocoa_timestamp: int) -> str: + """ + Convert Cocoa timestamp to readable format. + + Args: + cocoa_timestamp: Timestamp in Cocoa format (nanoseconds since 2001-01-01) + + Returns: + Formatted timestamp string + """ + if cocoa_timestamp == 0: + return "Unknown" + + try: + # Cocoa timestamp is nanoseconds since 2001-01-01 00:00:00 UTC + # Convert to seconds and add to Unix epoch + cocoa_epoch = datetime(2001, 1, 1) + unix_timestamp = cocoa_timestamp / 1_000_000_000 # Convert nanoseconds to seconds + message_time = cocoa_epoch.timestamp() + unix_timestamp + return datetime.fromtimestamp(message_time).strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, OSError): + return "Unknown" + + def _get_contact_name(self, handle_id: str) -> str: + """ + Get a readable contact name from handle ID. + + Args: + handle_id: The handle ID (phone number or email) + + Returns: + Formatted contact name + """ + if not handle_id: + return "Unknown" + + # Clean up phone numbers and emails for display + if "@" in handle_id: + return handle_id # Email address + elif handle_id.startswith("+"): + return handle_id # International phone number + else: + # Try to format as phone number + digits = "".join(filter(str.isdigit, handle_id)) + if len(digits) == 10: + return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}" + elif len(digits) == 11 and digits[0] == "1": + return f"+1 ({digits[1:4]}) {digits[4:7]}-{digits[7:]}" + else: + return handle_id + + def _read_messages_from_db(self, db_path: Path) -> list[dict]: + """ + Read messages from the iMessage database. + + Args: + db_path: Path to the chat.db file + + Returns: + List of message dictionaries + """ + if not db_path.exists(): + print(f"iMessage database not found at: {db_path}") + return [] + + try: + # Connect to the database + conn = sqlite3.connect(str(db_path)) + cursor = conn.cursor() + + # Query to get messages with chat and handle information + query = """ + SELECT + m.ROWID as message_id, + m.text, + m.date, + m.is_from_me, + m.service, + c.chat_identifier, + c.display_name as chat_display_name, + h.id as handle_id, + c.ROWID as chat_id + FROM message m + LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id + LEFT JOIN chat c ON cmj.chat_id = c.ROWID + LEFT JOIN handle h ON m.handle_id = h.ROWID + WHERE m.text IS NOT NULL AND m.text != '' + ORDER BY c.ROWID, m.date + """ + + cursor.execute(query) + rows = cursor.fetchall() + + messages = [] + for row in rows: + ( + message_id, + text, + date, + is_from_me, + service, + chat_identifier, + chat_display_name, + handle_id, + chat_id, + ) = row + + message = { + "message_id": message_id, + "text": text, + "timestamp": self._convert_cocoa_timestamp(date), + "is_from_me": bool(is_from_me), + "service": service or "iMessage", + "chat_identifier": chat_identifier or "Unknown", + "chat_display_name": chat_display_name or "Unknown Chat", + "handle_id": handle_id or "Unknown", + "contact_name": self._get_contact_name(handle_id or ""), + "chat_id": chat_id, + } + messages.append(message) + + conn.close() + print(f"Found {len(messages)} messages in database") + return messages + + except sqlite3.Error as e: + print(f"Error reading iMessage database: {e}") + return [] + except Exception as e: + print(f"Unexpected error reading iMessage database: {e}") + return [] + + def _group_messages_by_chat(self, messages: list[dict]) -> dict[int, list[dict]]: + """ + Group messages by chat ID. + + Args: + messages: List of message dictionaries + + Returns: + Dictionary mapping chat_id to list of messages + """ + chats = {} + for message in messages: + chat_id = message["chat_id"] + if chat_id not in chats: + chats[chat_id] = [] + chats[chat_id].append(message) + + return chats + + def _create_concatenated_content(self, chat_id: int, messages: list[dict]) -> str: + """ + Create concatenated content from chat messages. + + Args: + chat_id: The chat ID + messages: List of messages in the chat + + Returns: + Concatenated text content + """ + if not messages: + return "" + + # Get chat info from first message + first_msg = messages[0] + chat_name = first_msg["chat_display_name"] + chat_identifier = first_msg["chat_identifier"] + + # Build message content + message_parts = [] + for message in messages: + timestamp = message["timestamp"] + is_from_me = message["is_from_me"] + text = message["text"] + contact_name = message["contact_name"] + + if is_from_me: + prefix = "[You]" + else: + prefix = f"[{contact_name}]" + + if timestamp != "Unknown": + prefix += f" ({timestamp})" + + message_parts.append(f"{prefix}: {text}") + + concatenated_text = "\n\n".join(message_parts) + + doc_content = f"""Chat: {chat_name} +Identifier: {chat_identifier} +Messages ({len(messages)} messages): + +{concatenated_text} +""" + return doc_content + + def _create_individual_content(self, message: dict) -> str: + """ + Create content for individual message. + + Args: + message: Message dictionary + + Returns: + Formatted message content + """ + timestamp = message["timestamp"] + is_from_me = message["is_from_me"] + text = message["text"] + contact_name = message["contact_name"] + chat_name = message["chat_display_name"] + + sender = "You" if is_from_me else contact_name + + return f"""Message from {sender} in chat "{chat_name}" +Time: {timestamp} +Content: {text} +""" + + def load_data(self, input_dir: str | None = None, **load_kwargs: Any) -> list[Document]: + """ + Load iMessage data and return as documents. + + Args: + input_dir: Optional path to directory containing chat.db file. + If not provided, uses default macOS location. + **load_kwargs: Additional arguments (unused) + + Returns: + List of Document objects containing iMessage data + """ + docs = [] + + # Determine database path + if input_dir: + db_path = Path(input_dir) / "chat.db" + else: + db_path = self._get_default_chat_db_path() + + print(f"Reading iMessage database from: {db_path}") + + # Read messages from database + messages = self._read_messages_from_db(db_path) + if not messages: + return docs + + if self.concatenate_conversations: + # Group messages by chat and create concatenated documents + chats = self._group_messages_by_chat(messages) + + for chat_id, chat_messages in chats.items(): + if not chat_messages: + continue + + content = self._create_concatenated_content(chat_id, chat_messages) + + # Create metadata + first_msg = chat_messages[0] + last_msg = chat_messages[-1] + + metadata = { + "source": "iMessage", + "chat_id": chat_id, + "chat_name": first_msg["chat_display_name"], + "chat_identifier": first_msg["chat_identifier"], + "message_count": len(chat_messages), + "first_message_date": first_msg["timestamp"], + "last_message_date": last_msg["timestamp"], + "participants": list( + {msg["contact_name"] for msg in chat_messages if not msg["is_from_me"]} + ), + } + + doc = Document(text=content, metadata=metadata) + docs.append(doc) + + else: + # Create individual documents for each message + for message in messages: + content = self._create_individual_content(message) + + metadata = { + "source": "iMessage", + "message_id": message["message_id"], + "chat_id": message["chat_id"], + "chat_name": message["chat_display_name"], + "chat_identifier": message["chat_identifier"], + "timestamp": message["timestamp"], + "is_from_me": message["is_from_me"], + "contact_name": message["contact_name"], + "service": message["service"], + } + + doc = Document(text=content, metadata=metadata) + docs.append(doc) + + print(f"Created {len(docs)} documents from iMessage data") + return docs diff --git a/apps/imessage_rag.py b/apps/imessage_rag.py new file mode 100644 index 0000000..bd4ab68 --- /dev/null +++ b/apps/imessage_rag.py @@ -0,0 +1,126 @@ +""" +iMessage RAG Example. + +This example demonstrates how to build a RAG system on your iMessage conversation history. +""" + +import asyncio +from pathlib import Path +from typing import Any + +from leann.chunking_utils import create_text_chunks + +from apps.base_rag_example import BaseRAGExample +from apps.imessage_data.imessage_reader import IMessageReader + + +class IMessageRAG(BaseRAGExample): + """RAG example for iMessage conversation history.""" + + def __init__(self): + super().__init__( + name="iMessage", + description="RAG on your iMessage conversation history", + default_index_name="imessage_index", + ) + + def _add_specific_arguments(self, parser): + """Add iMessage-specific arguments.""" + imessage_group = parser.add_argument_group("iMessage Parameters") + imessage_group.add_argument( + "--db-path", + type=str, + default=None, + help="Path to iMessage chat.db file (default: ~/Library/Messages/chat.db)", + ) + imessage_group.add_argument( + "--concatenate-conversations", + action="store_true", + default=True, + help="Concatenate messages within conversations for better context (default: True)", + ) + imessage_group.add_argument( + "--no-concatenate-conversations", + action="store_true", + help="Process each message individually instead of concatenating by conversation", + ) + imessage_group.add_argument( + "--chunk-size", + type=int, + default=1000, + help="Maximum characters per text chunk (default: 1000)", + ) + imessage_group.add_argument( + "--chunk-overlap", + type=int, + default=200, + help="Overlap between text chunks (default: 200)", + ) + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load iMessage history and convert to text chunks.""" + print("Loading iMessage conversation history...") + + # Determine concatenation setting + concatenate = args.concatenate_conversations and not args.no_concatenate_conversations + + # Initialize iMessage reader + reader = IMessageReader(concatenate_conversations=concatenate) + + # Load documents + try: + if args.db_path: + # Use custom database path + db_dir = str(Path(args.db_path).parent) + documents = reader.load_data(input_dir=db_dir) + else: + # Use default macOS location + documents = reader.load_data() + + except Exception as e: + print(f"Error loading iMessage data: {e}") + print("\nTroubleshooting tips:") + print("1. Make sure you have granted Full Disk Access to your terminal/IDE") + print("2. Check that the iMessage database exists at ~/Library/Messages/chat.db") + print("3. Try specifying a custom path with --db-path if you have a backup") + return [] + + if not documents: + print("No iMessage conversations found!") + return [] + + print(f"Loaded {len(documents)} iMessage documents") + + # Show some statistics + total_messages = sum(doc.metadata.get("message_count", 1) for doc in documents) + print(f"Total messages: {total_messages}") + + if concatenate: + # Show chat statistics + chat_names = [doc.metadata.get("chat_name", "Unknown") for doc in documents] + unique_chats = len(set(chat_names)) + print(f"Unique conversations: {unique_chats}") + + # Convert to text chunks + all_texts = create_text_chunks( + documents, + chunk_size=args.chunk_size, + chunk_overlap=args.chunk_overlap, + ) + + # Apply max_items limit if specified + if args.max_items > 0: + all_texts = all_texts[: args.max_items] + print(f"Limited to {len(all_texts)} text chunks (max_items={args.max_items})") + + return all_texts + + +async def main(): + """Main entry point.""" + app = IMessageRAG() + await app.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/apps/multimodal/vision-based-pdf-multi-vector/README.md b/apps/multimodal/vision-based-pdf-multi-vector/README.md new file mode 100644 index 0000000..652f954 --- /dev/null +++ b/apps/multimodal/vision-based-pdf-multi-vector/README.md @@ -0,0 +1,113 @@ +## Vision-based PDF Multi-Vector Demos (macOS/MPS) + +This folder contains two demos to index PDF pages as images and run multi-vector retrieval with ColPali/ColQwen2, plus optional similarity map visualization and answer generation. + +### What you’ll run +- `multi-vector-leann-paper-example.py`: local PDF β†’ pages β†’ embed β†’ build HNSW index β†’ search. +- `multi-vector-leann-similarity-map.py`: HF dataset (default) or local pages β†’ embed β†’ index β†’ retrieve β†’ similarity maps β†’ optional Qwen-VL answer. + +## Prerequisites (macOS) + +### 1) Homebrew poppler (for pdf2image) +```bash +brew install poppler +which pdfinfo && pdfinfo -v +``` + +### 2) Python environment +Use uv (recommended) or pip. Python 3.9+. + +Using uv: +```bash +uv pip install \ + colpali_engine \ + pdf2image \ + pillow \ + matplotlib qwen_vl_utils \ + einops \ + seaborn +``` + +Notes: +- On first run, models download from Hugging Face. Login/config if needed. +- The scripts auto-select device: CUDA > MPS > CPU. Verify MPS: +```bash +python -c "import torch; print('MPS available:', bool(getattr(torch.backends, 'mps', None) and torch.backends.mps.is_available()))" +``` + +## Run the demos + +### A) Local PDF example +Converts a local PDF into page images, embeds them, builds an index, and searches. + +```bash +cd apps/multimodal/vision-based-pdf-multi-vector +# If you don't have the sample PDF locally, download it (ignored by Git) +mkdir -p pdfs +curl -L -o pdfs/2004.12832v2.pdf https://arxiv.org/pdf/2004.12832.pdf +ls pdfs/2004.12832v2.pdf +# Ensure output dir exists +mkdir -p pages +python multi-vector-leann-paper-example.py +``` +Expected: +- Page images in `pages/`. +- Console prints like `Using device=mps, dtype=...` and retrieved file paths for queries. + +To use your own PDF: edit `pdf_path` near the top of the script. + +### B) Similarity map + answer demo +Uses HF dataset `weaviate/arXiv-AI-papers-multi-vector` by default; can switch to local pages. + +```bash +cd apps/multimodal/vision-based-pdf-multi-vector +python multi-vector-leann-similarity-map.py +``` +Artifacts (when enabled): +- Retrieved pages: `./figures/retrieved_page_rank{K}.png` +- Similarity maps: `./figures/similarity_map_rank{K}.png` + +Key knobs in the script (top of file): +- `QUERY`: your question +- `MODEL`: `"colqwen2"` or `"colpali"` +- `USE_HF_DATASET`: set `False` to use local pages +- `PDF`, `PAGES_DIR`: for local mode +- `INDEX_PATH`, `TOPK`, `FIRST_STAGE_K`, `REBUILD_INDEX` +- `SIMILARITY_MAP`, `SIM_TOKEN_IDX`, `SIM_OUTPUT` +- `ANSWER`, `MAX_NEW_TOKENS` (Qwen-VL) + +## Troubleshooting +- pdf2image errors on macOS: ensure `brew install poppler` and `pdfinfo` works in terminal. +- Slow or OOM on MPS: reduce dataset size (e.g., set `MAX_DOCS`) or switch to CPU. +- NaNs on MPS: keep fp32 on MPS (default in similarity-map script); avoid fp16 there. +- First-run model downloads can be large; ensure network access (HF mirrors if needed). + +## Notes +- Index files are under `./indexes/`. Delete or set `REBUILD_INDEX=True` to rebuild. +- For local PDFs, page images go to `./pages/`. + + +### Retrieval and Visualization Example + +Example settings in `multi-vector-leann-similarity-map.py`: +- `QUERY = "How does DeepSeek-V2 compare against the LLaMA family of LLMs?"` +- `SIMILARITY_MAP = True` (to generate heatmaps) +- `TOPK = 1` (save the top retrieved page and its similarity map) + +Run: +```bash +cd apps/multimodal/vision-based-pdf-multi-vector +python multi-vector-leann-similarity-map.py +``` + +Outputs (by default): +- Retrieved page: `./figures/retrieved_page_rank1.png` +- Similarity map: `./figures/similarity_map_rank1.png` + +Sample visualization (example result, and the query is "QUERY = "How does Vim model performance and efficiency compared to other models?" +"): +![Similarity map example](fig/image.png) + +Notes: +- Set `SIM_TOKEN_IDX` to visualize a specific token index; set `-1` to auto-select the most salient token. +- If you change `SIM_OUTPUT` to a file path (e.g., `./figures/my_map.png`), multiple ranks are saved as `my_map_rank{K}.png`. diff --git a/apps/multimodal/vision-based-pdf-multi-vector/colqwen_forward.py b/apps/multimodal/vision-based-pdf-multi-vector/colqwen_forward.py new file mode 100755 index 0000000..510b3ad --- /dev/null +++ b/apps/multimodal/vision-based-pdf-multi-vector/colqwen_forward.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Simple test script to test colqwen2 forward pass with a single image.""" + +import os +import sys +from pathlib import Path + +# Add the current directory to path to import leann_multi_vector +sys.path.insert(0, str(Path(__file__).parent)) + +import torch +from leann_multi_vector import _embed_images, _ensure_repo_paths_importable, _load_colvision +from PIL import Image + +# Ensure repo paths are importable +_ensure_repo_paths_importable(__file__) + +# Set environment variable +os.environ["TOKENIZERS_PARALLELISM"] = "false" + + +def create_test_image(): + """Create a simple test image.""" + # Create a simple RGB image (800x600) + img = Image.new("RGB", (800, 600), color="white") + return img + + +def load_test_image_from_file(): + """Try to load an image from the indexes directory if available.""" + # Try to find an existing image in the indexes directory + indexes_dir = Path(__file__).parent / "indexes" + + # Look for images in common locations + possible_paths = [ + indexes_dir / "vidore_fastplaid" / "images", + indexes_dir / "colvision_large.leann.images", + indexes_dir / "colvision.leann.images", + ] + + for img_dir in possible_paths: + if img_dir.exists(): + # Find first image file + for ext in [".png", ".jpg", ".jpeg"]: + for img_file in img_dir.glob(f"*{ext}"): + print(f"Loading test image from: {img_file}") + return Image.open(img_file) + + return None + + +def main(): + print("=" * 60) + print("Testing ColQwen2 Forward Pass") + print("=" * 60) + + # Step 1: Load or create test image + print("\n[Step 1] Loading test image...") + test_image = load_test_image_from_file() + if test_image is None: + print("No existing image found, creating a simple test image...") + test_image = create_test_image() + else: + print(f"βœ“ Loaded image: {test_image.size} ({test_image.mode})") + + # Convert to RGB if needed + if test_image.mode != "RGB": + test_image = test_image.convert("RGB") + print(f"βœ“ Converted to RGB: {test_image.size}") + + # Step 2: Load model + print("\n[Step 2] Loading ColQwen2 model...") + try: + model_name, model, processor, device_str, device, dtype = _load_colvision("colqwen2") + print(f"βœ“ Model loaded: {model_name}") + print(f"βœ“ Device: {device_str}, dtype: {dtype}") + + # Print model info + if hasattr(model, "device"): + print(f"βœ“ Model device: {model.device}") + if hasattr(model, "dtype"): + print(f"βœ“ Model dtype: {model.dtype}") + + except Exception as e: + print(f"βœ— Error loading model: {e}") + import traceback + + traceback.print_exc() + return + + # Step 3: Test forward pass + print("\n[Step 3] Running forward pass...") + try: + # Use the _embed_images function which handles batching and forward pass + images = [test_image] + print(f"Processing {len(images)} image(s)...") + + doc_vecs = _embed_images(model, processor, images) + + print("βœ“ Forward pass completed!") + print(f"βœ“ Number of embeddings: {len(doc_vecs)}") + + if len(doc_vecs) > 0: + emb = doc_vecs[0] + print(f"βœ“ Embedding shape: {emb.shape}") + print(f"βœ“ Embedding dtype: {emb.dtype}") + print("βœ“ Embedding stats:") + print(f" - Min: {emb.min().item():.4f}") + print(f" - Max: {emb.max().item():.4f}") + print(f" - Mean: {emb.mean().item():.4f}") + print(f" - Std: {emb.std().item():.4f}") + + # Check for NaN or Inf + if torch.isnan(emb).any(): + print("⚠ Warning: Embedding contains NaN values!") + if torch.isinf(emb).any(): + print("⚠ Warning: Embedding contains Inf values!") + + except Exception as e: + print(f"βœ— Error during forward pass: {e}") + import traceback + + traceback.print_exc() + return + + print("\n" + "=" * 60) + print("Test completed successfully!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/apps/multimodal/vision-based-pdf-multi-vector/fig/image.png b/apps/multimodal/vision-based-pdf-multi-vector/fig/image.png new file mode 100644 index 0000000..e6d1a70 Binary files /dev/null and b/apps/multimodal/vision-based-pdf-multi-vector/fig/image.png differ diff --git a/apps/multimodal/vision-based-pdf-multi-vector/leann_multi_vector.py b/apps/multimodal/vision-based-pdf-multi-vector/leann_multi_vector.py new file mode 100644 index 0000000..bd0a8f3 --- /dev/null +++ b/apps/multimodal/vision-based-pdf-multi-vector/leann_multi_vector.py @@ -0,0 +1,1456 @@ +import concurrent.futures +import glob +import json +import logging +import os +import re +import sys +import time +from pathlib import Path +from typing import Any, Optional, cast + +import numpy as np +from PIL import Image +from tqdm import tqdm + +logger = logging.getLogger(__name__) + + +def _ensure_repo_paths_importable(current_file: str) -> None: + """Make local leann packages importable without installing (mirrors multi-vector-leann.py).""" + _repo_root = Path(current_file).resolve().parents[3] + _leann_core_src = _repo_root / "packages" / "leann-core" / "src" + _leann_hnsw_pkg = _repo_root / "packages" / "leann-backend-hnsw" + if str(_leann_core_src) not in sys.path: + sys.path.append(str(_leann_core_src)) + if str(_leann_hnsw_pkg) not in sys.path: + sys.path.append(str(_leann_hnsw_pkg)) + + +def _find_backend_module_file() -> Optional[Path]: + """Best-effort locate the backend leann_multi_vector.py file, avoiding this file.""" + this_file = Path(__file__).resolve() + candidates: list[Path] = [] + + # Common in-repo location + repo_root = this_file.parents[3] + candidates.append(repo_root / "packages" / "leann-backend-hnsw" / "leann_multi_vector.py") + candidates.append( + repo_root / "packages" / "leann-backend-hnsw" / "src" / "leann_multi_vector.py" + ) + + for cand in candidates: + try: + if cand.exists() and cand.resolve() != this_file: + return cand.resolve() + except Exception: + pass + + # Fallback: scan sys.path for another leann_multi_vector.py different from this file + for p in list(sys.path): + try: + cand = Path(p) / "leann_multi_vector.py" + if cand.exists() and cand.resolve() != this_file: + return cand.resolve() + except Exception: + continue + return None + + +_BACKEND_LEANN_CLASS: Optional[type] = None + + +def _get_backend_leann_multi_vector() -> type: + """Load backend LeannMultiVector class even if this file shadows its module name.""" + global _BACKEND_LEANN_CLASS + if _BACKEND_LEANN_CLASS is not None: + return _BACKEND_LEANN_CLASS + + backend_path = _find_backend_module_file() + if backend_path is None: + # Fallback to local implementation in this module + try: + cls = LeannMultiVector + _BACKEND_LEANN_CLASS = cls + return cls + except Exception as e: + raise ImportError( + "Could not locate backend 'leann_multi_vector.py' and no local implementation found. " + "Ensure the leann backend is available under packages/leann-backend-hnsw or installed." + ) from e + + import importlib.util + + module_name = "leann_hnsw_backend_module" + spec = importlib.util.spec_from_file_location(module_name, str(backend_path)) + if spec is None or spec.loader is None: + raise ImportError(f"Failed to create spec for backend module at {backend_path}") + backend_module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = backend_module + spec.loader.exec_module(backend_module) + + if not hasattr(backend_module, "LeannMultiVector"): + raise ImportError(f"'LeannMultiVector' not found in backend module at {backend_path}") + _BACKEND_LEANN_CLASS = backend_module.LeannMultiVector + return _BACKEND_LEANN_CLASS + + +def _natural_sort_key(name: str) -> int: + m = re.search(r"\d+", name) + return int(m.group()) if m else 0 + + +def _load_images_from_dir( + pages_dir: str, recursive: bool = False +) -> tuple[list[str], list[Image.Image]]: + """ + Load images from a directory. + + Args: + pages_dir: Directory path containing images + recursive: If True, recursively search subdirectories (default: False) + + Returns: + Tuple of (filepaths, images) + """ + + # Supported image extensions + extensions = ("*.png", "*.jpg", "*.jpeg", "*.PNG", "*.JPG", "*.JPEG", "*.webp", "*.WEBP") + + if recursive: + # Recursive search + filepaths = [] + for ext in extensions: + pattern = os.path.join(pages_dir, "**", ext) + filepaths.extend(glob.glob(pattern, recursive=True)) + else: + # Non-recursive search (only top-level directory) + filepaths = [] + for ext in extensions: + pattern = os.path.join(pages_dir, ext) + filepaths.extend(glob.glob(pattern)) + + # Sort files naturally + filepaths = sorted(filepaths, key=lambda x: _natural_sort_key(os.path.basename(x))) + + # Load images with error handling + images = [] + valid_filepaths = [] + failed_count = 0 + + for filepath in filepaths: + try: + img = Image.open(filepath) + # Convert to RGB if necessary (handles RGBA, P, etc.) + if img.mode != "RGB": + img = img.convert("RGB") + images.append(img) + valid_filepaths.append(filepath) + except Exception as e: + failed_count += 1 + print(f"Warning: Failed to load image {filepath}: {e}") + continue + + if failed_count > 0: + print( + f"Warning: Failed to load {failed_count} image(s) out of {len(filepaths)} total files" + ) + + return valid_filepaths, images + + +def _maybe_convert_pdf_to_images(pdf_path: Optional[str], pages_dir: str, dpi: int = 200) -> None: + if not pdf_path: + return + os.makedirs(pages_dir, exist_ok=True) + try: + from pdf2image import convert_from_path + except Exception as e: + raise RuntimeError( + "pdf2image is required to convert PDF to images. Install via pip install pdf2image" + ) from e + images = convert_from_path(pdf_path, dpi=dpi) + for i, image in enumerate(images): + image.save(os.path.join(pages_dir, f"page_{i + 1}.png"), "PNG") + + +def _select_device_and_dtype(): + import torch + from colpali_engine.utils.torch_utils import get_torch_device + + device_str = ( + "cuda" + if torch.cuda.is_available() + else ( + "mps" + if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available() + else "cpu" + ) + ) + device = get_torch_device(device_str) + # Stable dtype selection to avoid NaNs: + # - CUDA: prefer bfloat16 if supported, else float16 + # - MPS: use float32 (fp16 on MPS can produce NaNs in some ops) + # - CPU: float32 + if device_str == "cuda": + dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16 + try: + torch.backends.cuda.matmul.allow_tf32 = True # Better stability/perf on Ampere+ + except Exception: + pass + elif device_str == "mps": + dtype = torch.float32 + else: + dtype = torch.float32 + return device_str, device, dtype + + +def _load_colvision(model_choice: str): + import os + + import torch + from colpali_engine.models import ( + ColPali, + ColQwen2, + ColQwen2_5, + ColQwen2_5_Processor, + ColQwen2Processor, + ) + from colpali_engine.models.paligemma.colpali.processing_colpali import ColPaliProcessor + from transformers.utils.import_utils import is_flash_attn_2_available + + # Force HuggingFace Hub to use HF endpoint, avoid Google Drive + # Set environment variables to ensure models are downloaded from HuggingFace + os.environ.setdefault("HF_ENDPOINT", "https://huggingface.co") + os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1") + + # Log model loading info + logger.info(f"Loading ColVision model: {model_choice}") + logger.info(f"HF_ENDPOINT: {os.environ.get('HF_ENDPOINT', 'not set')}") + logger.info("Models will be downloaded from HuggingFace Hub, not Google Drive") + + device_str, device, dtype = _select_device_and_dtype() + + # Determine model name and type + # IMPORTANT: Check colqwen2.5 BEFORE colqwen2 to avoid false matches + model_choice_lower = model_choice.lower() + if model_choice == "colqwen2": + model_name = "vidore/colqwen2-v1.0" + model_type = "colqwen2" + elif model_choice == "colqwen2.5" or model_choice == "colqwen25": + model_name = "vidore/colqwen2.5-v0.2" + model_type = "colqwen2.5" + elif model_choice == "colpali": + model_name = "vidore/colpali-v1.2" + model_type = "colpali" + elif ( + "colqwen2.5" in model_choice_lower + or "colqwen25" in model_choice_lower + or "colqwen2_5" in model_choice_lower + ): + # Handle HuggingFace model names like "vidore/colqwen2.5-v0.2" + model_name = model_choice + model_type = "colqwen2.5" + elif "colqwen2" in model_choice_lower and "colqwen2-v1.0" in model_choice_lower: + # Handle HuggingFace model names like "vidore/colqwen2-v1.0" (but not colqwen2.5) + model_name = model_choice + model_type = "colqwen2" + elif "colpali" in model_choice_lower: + # Handle HuggingFace model names like "vidore/colpali-v1.2" + model_name = model_choice + model_type = "colpali" + else: + # Default to colpali for backward compatibility + model_name = "vidore/colpali-v1.2" + model_type = "colpali" + + # Load model based on type + attn_implementation = ( + "flash_attention_2" if (device_str == "cuda" and is_flash_attn_2_available()) else "eager" + ) + + # Load model from HuggingFace Hub (not Google Drive) + # Use local_files_only=False to ensure download from HF if not cached + if model_type == "colqwen2.5": + model = ColQwen2_5.from_pretrained( + model_name, + torch_dtype=torch.bfloat16, + device_map=device, + attn_implementation=attn_implementation, + local_files_only=False, # Ensure download from HuggingFace Hub + ).eval() + processor = ColQwen2_5_Processor.from_pretrained(model_name, local_files_only=False) + elif model_type == "colqwen2": + model = ColQwen2.from_pretrained( + model_name, + torch_dtype=torch.bfloat16, + device_map=device, + attn_implementation=attn_implementation, + local_files_only=False, # Ensure download from HuggingFace Hub + ).eval() + processor = ColQwen2Processor.from_pretrained(model_name, local_files_only=False) + else: # colpali + model = ColPali.from_pretrained( + model_name, + torch_dtype=torch.bfloat16, + device_map=device, + local_files_only=False, # Ensure download from HuggingFace Hub + ).eval() + processor = cast( + ColPaliProcessor, ColPaliProcessor.from_pretrained(model_name, local_files_only=False) + ) + + return model_name, model, processor, device_str, device, dtype + + +def _embed_images(model, processor, images: list[Image.Image]) -> list[Any]: + import torch + from colpali_engine.utils.torch_utils import ListDataset + from torch.utils.data import DataLoader + + # Ensure deterministic eval and autocast for stability + model.eval() + + dataloader = DataLoader( + dataset=ListDataset[Image.Image](images), + batch_size=32, + shuffle=False, + collate_fn=lambda x: processor.process_images(x), + ) + + doc_vecs: list[Any] = [] + for batch_doc in tqdm(dataloader, desc="Embedding images"): + with torch.no_grad(): + batch_doc = {k: v.to(model.device) for k, v in batch_doc.items()} + # autocast on CUDA for bf16/fp16; on CPU/MPS stay in fp32 + if model.device.type == "cuda": + with torch.autocast( + device_type="cuda", + dtype=model.dtype if model.dtype.is_floating_point else torch.bfloat16, + ): + embeddings_doc = model(**batch_doc) + else: + embeddings_doc = model(**batch_doc) + doc_vecs.extend(list(torch.unbind(embeddings_doc.to("cpu")))) + return doc_vecs + + +def _embed_queries(model, processor, queries: list[str]) -> list[Any]: + import torch + + model.eval() + + # Match MTEB's exact query processing from ColPaliEngineWrapper.get_text_embeddings: + # 1. MTEB receives batch["text"] which already includes instruction/prompt (from _combine_queries_with_instruction_text) + # 2. Manually adds: query_prefix + text + query_augmentation_token * 10 + # 3. Calls processor.process_queries(batch) where batch is now a list of strings + # 4. process_queries adds: query_prefix + text + suffix (suffix = query_augmentation_token * 10) + # + # This results in duplicate addition: query_prefix is added twice, query_augmentation_token * 20 total + # We need to match this exactly to reproduce MTEB results + + all_embeds = [] + batch_size = 32 # Match MTEB's default batch_size + + with torch.no_grad(): + for i in tqdm(range(0, len(queries), batch_size), desc="Embedding queries"): + batch_queries = queries[i : i + batch_size] + + # Match MTEB: manually add query_prefix + text + query_augmentation_token * 10 + # Then process_queries will add them again (resulting in 20 augmentation tokens total) + batch = [ + processor.query_prefix + t + processor.query_augmentation_token * 10 + for t in batch_queries + ] + inputs = processor.process_queries(batch) + inputs = {k: v.to(model.device) for k, v in inputs.items()} + + if model.device.type == "cuda": + with torch.autocast( + device_type="cuda", + dtype=model.dtype if model.dtype.is_floating_point else torch.bfloat16, + ): + outs = model(**inputs) + else: + outs = model(**inputs) + + # Match MTEB: convert to float32 on CPU + all_embeds.extend(list(torch.unbind(outs.cpu().to(torch.float32)))) + + return all_embeds + + +def _build_index( + index_path: str, doc_vecs: list[Any], filepaths: list[str], images: list[Image.Image] +) -> Any: + LeannMultiVector = _get_backend_leann_multi_vector() + dim = int(doc_vecs[0].shape[-1]) + retriever = LeannMultiVector(index_path=index_path, dim=dim) + retriever.create_collection() + for i, vec in enumerate(doc_vecs): + data = { + "colbert_vecs": vec.float().numpy(), + "doc_id": i, + "filepath": filepaths[i], + "image": images[i], # Include the original image + } + retriever.insert(data) + retriever.create_index() + return retriever + + +def _load_retriever_if_index_exists(index_path: str) -> Optional[Any]: + LeannMultiVector = _get_backend_leann_multi_vector() + index_base = Path(index_path) + # Check for the actual HNSW index file written by the backend + our sidecar files + index_file = index_base.parent / f"{index_base.stem}.index" + meta = index_base.parent / f"{index_base.name}.meta.json" + labels = index_base.parent / f"{index_base.name}.labels.json" + if index_file.exists() and meta.exists() and labels.exists(): + try: + with open(meta, encoding="utf-8") as f: + meta_json = json.load(f) + dim = int(meta_json.get("dimensions", 128)) + except Exception: + dim = 128 + return LeannMultiVector(index_path=index_path, dim=dim) + return None + + +def _build_fast_plaid_index( + index_path: str, + doc_vecs: list[Any], + filepaths: list[str], + images: list[Image.Image], +) -> tuple[Any, float]: + """ + Build a Fast-Plaid index from document embeddings. + + Args: + index_path: Path to save the Fast-Plaid index + doc_vecs: List of document embeddings (each is a tensor with shape [num_tokens, embedding_dim]) + filepaths: List of filepath identifiers for each document + images: List of PIL Images corresponding to each document + + Returns: + Tuple of (FastPlaid index object, build_time_in_seconds) + """ + import torch + from fast_plaid import search as fast_plaid_search + + print(f" Preparing {len(doc_vecs)} document embeddings for Fast-Plaid...") + _t0 = time.perf_counter() + + # Convert doc_vecs to list of tensors + documents_embeddings = [] + for i, vec in enumerate(doc_vecs): + if i % 1000 == 0: + print(f" Converting embedding {i}/{len(doc_vecs)}...") + if not isinstance(vec, torch.Tensor): + vec = ( + torch.tensor(vec) + if isinstance(vec, np.ndarray) + else torch.from_numpy(np.array(vec)) + ) + # Ensure float32 for Fast-Plaid + if vec.dtype != torch.float32: + vec = vec.float() + documents_embeddings.append(vec) + + print(f" Converted {len(documents_embeddings)} embeddings") + if len(documents_embeddings) > 0: + print(f" First embedding shape: {documents_embeddings[0].shape}") + print(f" First embedding dtype: {documents_embeddings[0].dtype}") + + # Prepare metadata for Fast-Plaid + print(f" Preparing metadata for {len(filepaths)} documents...") + metadata_list = [] + for i, filepath in enumerate(filepaths): + metadata_list.append( + { + "filepath": filepath, + "index": i, + } + ) + + # Create Fast-Plaid index + print(f" Creating FastPlaid object with index path: {index_path}") + try: + fast_plaid_index = fast_plaid_search.FastPlaid(index=index_path) + print(" FastPlaid object created successfully") + except Exception as e: + print(f" Error creating FastPlaid object: {type(e).__name__}: {e}") + import traceback + + traceback.print_exc() + raise + + print(f" Calling fast_plaid_index.create() with {len(documents_embeddings)} documents...") + try: + fast_plaid_index.create( + documents_embeddings=documents_embeddings, + metadata=metadata_list, + ) + print(" Fast-Plaid index created successfully") + except Exception as e: + print(f" Error creating Fast-Plaid index: {type(e).__name__}: {e}") + import traceback + + traceback.print_exc() + raise + + build_secs = time.perf_counter() - _t0 + + # Save images separately (Fast-Plaid doesn't store images) + print(f" Saving {len(images)} images...") + images_dir = Path(index_path) / "images" + images_dir.mkdir(parents=True, exist_ok=True) + for i, img in enumerate(tqdm(images, desc="Saving images")): + img_path = images_dir / f"doc_{i}.png" + img.save(str(img_path)) + + return fast_plaid_index, build_secs + + +def _fast_plaid_index_exists(index_path: str) -> bool: + """ + Check if Fast-Plaid index exists by checking for key files. + This avoids creating the FastPlaid object which may trigger memory allocation. + + Args: + index_path: Path to the Fast-Plaid index + + Returns: + True if index appears to exist, False otherwise + """ + index_path_obj = Path(index_path) + if not index_path_obj.exists() or not index_path_obj.is_dir(): + return False + + # Fast-Plaid creates a SQLite database file for metadata + # Check for metadata.db as the most reliable indicator + metadata_db = index_path_obj / "metadata.db" + if metadata_db.exists() and metadata_db.stat().st_size > 0: + return True + + # Also check if directory has any files (might be incomplete index) + try: + if any(index_path_obj.iterdir()): + return True + except Exception: + pass + + return False + + +def _load_fast_plaid_index_if_exists(index_path: str) -> Optional[Any]: + """ + Load Fast-Plaid index if it exists. + First checks if index files exist, then creates the FastPlaid object. + The actual index data loading happens lazily when search is called. + + Args: + index_path: Path to the Fast-Plaid index + + Returns: + FastPlaid index object if exists, None otherwise + """ + try: + from fast_plaid import search as fast_plaid_search + + # First check if index files exist without creating the object + if not _fast_plaid_index_exists(index_path): + return None + + # Now try to create FastPlaid object + # This may trigger some memory allocation, but the full index loading is deferred + fast_plaid_index = fast_plaid_search.FastPlaid(index=index_path) + return fast_plaid_index + except ImportError: + # fast-plaid not installed + return None + except Exception as e: + # Any error (including memory errors from Rust backend) - return None + # The error will be caught and index will be rebuilt + print(f"Warning: Could not load Fast-Plaid index: {type(e).__name__}: {e}") + return None + + +def _search_fast_plaid( + fast_plaid_index: Any, + query_vec: Any, + top_k: int, +) -> tuple[list[tuple[float, int]], float]: + """ + Search Fast-Plaid index with a query embedding. + + Args: + fast_plaid_index: FastPlaid index object + query_vec: Query embedding tensor with shape [num_tokens, embedding_dim] + top_k: Number of top results to return + + Returns: + Tuple of (results_list, search_time_in_seconds) + results_list: List of (score, doc_id) tuples + """ + import torch + + _t0 = time.perf_counter() + + # Ensure query is a torch tensor + if not isinstance(query_vec, torch.Tensor): + q_vec_tensor = ( + torch.tensor(query_vec) + if isinstance(query_vec, np.ndarray) + else torch.from_numpy(np.array(query_vec)) + ) + else: + q_vec_tensor = query_vec + + # Fast-Plaid expects shape [num_queries, num_tokens, embedding_dim] + if q_vec_tensor.dim() == 2: + q_vec_tensor = q_vec_tensor.unsqueeze(0) # [1, num_tokens, embedding_dim] + + # Perform search + scores = fast_plaid_index.search( + queries_embeddings=q_vec_tensor, + top_k=top_k, + show_progress=True, + ) + + search_secs = time.perf_counter() - _t0 + + # Convert Fast-Plaid results to same format as LEANN: list of (score, doc_id) tuples + results = [] + if scores and len(scores) > 0: + query_results = scores[0] + # Fast-Plaid returns (doc_id, score), convert to (score, doc_id) to match LEANN format + results = [(float(score), int(doc_id)) for doc_id, score in query_results] + + return results, search_secs + + +def _get_fast_plaid_image(index_path: str, doc_id: int) -> Optional[Image.Image]: + """ + Retrieve image for a document from Fast-Plaid index. + + Args: + index_path: Path to the Fast-Plaid index + doc_id: Document ID returned by Fast-Plaid search + + Returns: + PIL Image if found, None otherwise + + Note: Uses metadata['index'] to get the actual file index, as Fast-Plaid + doc_id may differ from the file naming index. + """ + # First get metadata to find the actual index used for file naming + metadata = _get_fast_plaid_metadata(index_path, doc_id) + if metadata is None: + # Fallback: try using doc_id directly + file_index = doc_id + else: + # Use the 'index' field from metadata, which matches the file naming + file_index = metadata.get("index", doc_id) + + images_dir = Path(index_path) / "images" + image_path = images_dir / f"doc_{file_index}.png" + + if image_path.exists(): + return Image.open(image_path) + + # If not found with index, try doc_id as fallback + if file_index != doc_id: + fallback_path = images_dir / f"doc_{doc_id}.png" + if fallback_path.exists(): + return Image.open(fallback_path) + + return None + + +def _get_fast_plaid_metadata(index_path: str, doc_id: int) -> Optional[dict]: + """ + Retrieve metadata for a document from Fast-Plaid index. + + Args: + index_path: Path to the Fast-Plaid index + doc_id: Document ID + + Returns: + Dictionary with metadata if found, None otherwise + """ + try: + from fast_plaid import filtering + + metadata_list = filtering.get(index=index_path, subset=[doc_id]) + if metadata_list and len(metadata_list) > 0: + return metadata_list[0] + except Exception: + pass + return None + + +def _generate_similarity_map( + model, + processor, + image: Image.Image, + query: str, + token_idx: Optional[int] = None, + output_path: Optional[str] = None, +) -> tuple[int, float]: + import torch + from colpali_engine.interpretability import ( + get_similarity_maps_from_embeddings, + plot_similarity_map, + ) + + batch_images = processor.process_images([image]).to(model.device) + batch_queries = processor.process_queries([query]).to(model.device) + + with torch.no_grad(): + image_embeddings = model.forward(**batch_images) + query_embeddings = model.forward(**batch_queries) + + n_patches = processor.get_n_patches( + image_size=image.size, + spatial_merge_size=getattr(model, "spatial_merge_size", None), + ) + image_mask = processor.get_image_mask(batch_images) + + batched_similarity_maps = get_similarity_maps_from_embeddings( + image_embeddings=image_embeddings, + query_embeddings=query_embeddings, + n_patches=n_patches, + image_mask=image_mask, + ) + + similarity_maps = batched_similarity_maps[0] + + # Determine token index if not provided: choose the token with highest max score + if token_idx is None: + per_token_max = similarity_maps.view(similarity_maps.shape[0], -1).max(dim=1).values + token_idx = int(per_token_max.argmax().item()) + + max_sim_score = similarity_maps[token_idx, :, :].max().item() + + if output_path: + import matplotlib.pyplot as plt + + fig, ax = plot_similarity_map( + image=image, + similarity_map=similarity_maps[token_idx], + figsize=(14, 14), + show_colorbar=False, + ) + ax.set_title(f"Token #{token_idx}. MaxSim score: {max_sim_score:.2f}", fontsize=12) + os.makedirs(os.path.dirname(output_path), exist_ok=True) + plt.savefig(output_path, bbox_inches="tight") + plt.close(fig) + + return token_idx, float(max_sim_score) + + +class QwenVL: + def __init__(self, device: str): + from transformers import AutoProcessor, Qwen2_5_VLForConditionalGeneration + from transformers.utils.import_utils import is_flash_attn_2_available + + attn_implementation = "flash_attention_2" if is_flash_attn_2_available() else "eager" + self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained( + "Qwen/Qwen2.5-VL-3B-Instruct", + torch_dtype="auto", + device_map=device, + attn_implementation=attn_implementation, + ) + + min_pixels = 256 * 28 * 28 + max_pixels = 1280 * 28 * 28 + self.processor = AutoProcessor.from_pretrained( + "Qwen/Qwen2.5-VL-3B-Instruct", min_pixels=min_pixels, max_pixels=max_pixels + ) + + def answer(self, query: str, images: list[Image.Image], max_new_tokens: int = 128) -> str: + import base64 + from io import BytesIO + + from qwen_vl_utils import process_vision_info + + content = [] + for img in images: + buffer = BytesIO() + img.save(buffer, format="jpeg") + img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") + content.append({"type": "image", "image": f"data:image;base64,{img_base64}"}) + content.append({"type": "text", "text": query}) + messages = [{"role": "user", "content": content}] + + text = self.processor.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True + ) + image_inputs, video_inputs = process_vision_info(messages) + inputs = self.processor( + text=[text], images=image_inputs, videos=video_inputs, padding=True, return_tensors="pt" + ) + inputs = inputs.to(self.model.device) + + generated_ids = self.model.generate(**inputs, max_new_tokens=max_new_tokens) + generated_ids_trimmed = [ + out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) + ] + return self.processor.batch_decode( + generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False + )[0] + + +# Ensure repo paths are importable for dynamic backend loading +_ensure_repo_paths_importable(__file__) + +from leann_backend_hnsw.hnsw_backend import HNSWBuilder, HNSWSearcher # noqa: E402 + + +class LeannMultiVector: + def __init__( + self, + index_path: str, + dim: int = 128, + distance_metric: str = "mips", + m: int = 16, + ef_construction: int = 500, + is_compact: bool = False, + is_recompute: bool = False, + embedding_model_name: str = "colvision", + ) -> None: + self.index_path = index_path + self.dim = dim + self.embedding_model_name = embedding_model_name + self._pending_items: list[dict] = [] + self._backend_kwargs = { + "distance_metric": distance_metric, + "M": m, + "efConstruction": ef_construction, + "is_compact": is_compact, + "is_recompute": is_recompute, + } + self._labels_meta: list[dict] = [] + self._docid_to_indices: dict[int, list[int]] | None = None + + def _meta_dict(self) -> dict: + return { + "version": "1.0", + "backend_name": "hnsw", + "embedding_model": self.embedding_model_name, + "embedding_mode": "custom", + "dimensions": self.dim, + "backend_kwargs": self._backend_kwargs, + "is_compact": self._backend_kwargs.get("is_compact", True), + "is_pruned": self._backend_kwargs.get("is_compact", True) + and self._backend_kwargs.get("is_recompute", True), + } + + def create_collection(self) -> None: + path = Path(self.index_path) + path.parent.mkdir(parents=True, exist_ok=True) + + def insert(self, data: dict) -> None: + self._pending_items.append( + { + "doc_id": int(data["doc_id"]), + "filepath": data.get("filepath", ""), + "colbert_vecs": [np.asarray(v, dtype=np.float32) for v in data["colbert_vecs"]], + "image": data.get("image"), # PIL Image object (optional) + } + ) + + def _labels_path(self) -> Path: + index_path_obj = Path(self.index_path) + return index_path_obj.parent / f"{index_path_obj.name}.labels.json" + + def _meta_path(self) -> Path: + index_path_obj = Path(self.index_path) + return index_path_obj.parent / f"{index_path_obj.name}.meta.json" + + def _embeddings_path(self) -> Path: + index_path_obj = Path(self.index_path) + return index_path_obj.parent / f"{index_path_obj.name}.emb.npy" + + def _images_dir_path(self) -> Path: + """Directory where original images are stored.""" + index_path_obj = Path(self.index_path) + return index_path_obj.parent / f"{index_path_obj.name}.images" + + def create_index(self) -> None: + if not self._pending_items: + return + + embeddings: list[np.ndarray] = [] + labels_meta: list[dict] = [] + + # Create images directory if needed + images_dir = self._images_dir_path() + images_dir.mkdir(parents=True, exist_ok=True) + + for item in self._pending_items: + doc_id = int(item["doc_id"]) + filepath = item.get("filepath", "") + colbert_vecs = item["colbert_vecs"] + image = item.get("image") + + # Save image if provided + image_path = "" + if image is not None and isinstance(image, Image.Image): + image_filename = f"doc_{doc_id}.png" + image_path = str(images_dir / image_filename) + image.save(image_path, "PNG") + + for seq_id, vec in enumerate(colbert_vecs): + vec_np = np.asarray(vec, dtype=np.float32) + embeddings.append(vec_np) + labels_meta.append( + { + "id": f"{doc_id}:{seq_id}", + "doc_id": doc_id, + "seq_id": int(seq_id), + "filepath": filepath, + "image_path": image_path, # Store the path to the saved image + } + ) + + if not embeddings: + return + + embeddings_np = np.vstack(embeddings).astype(np.float32) + print(embeddings_np.shape) + + builder = HNSWBuilder(**{**self._backend_kwargs, "dimensions": self.dim}) + ids = [str(i) for i in range(embeddings_np.shape[0])] + builder.build(embeddings_np, ids, self.index_path) + + import json as _json + + with open(self._meta_path(), "w", encoding="utf-8") as f: + _json.dump(self._meta_dict(), f, indent=2) + with open(self._labels_path(), "w", encoding="utf-8") as f: + _json.dump(labels_meta, f) + + # Persist embeddings for exact reranking + np.save(self._embeddings_path(), embeddings_np) + + self._labels_meta = labels_meta + + def _load_labels_meta_if_needed(self) -> None: + if self._labels_meta: + return + labels_path = self._labels_path() + if labels_path.exists(): + import json as _json + + with open(labels_path, encoding="utf-8") as f: + self._labels_meta = _json.load(f) + + def _build_docid_to_indices_if_needed(self) -> None: + if self._docid_to_indices is not None: + return + self._load_labels_meta_if_needed() + mapping: dict[int, list[int]] = {} + for idx, meta in enumerate(self._labels_meta): + try: + doc_id = int(meta["doc_id"]) + except Exception: + continue + mapping.setdefault(doc_id, []).append(idx) + self._docid_to_indices = mapping + + def search( + self, data: np.ndarray, topk: int, first_stage_k: int = 50 + ) -> list[tuple[float, int]]: + if data.ndim == 1: + data = data.reshape(1, -1) + if data.dtype != np.float32: + data = data.astype(np.float32) + + self._load_labels_meta_if_needed() + + searcher = HNSWSearcher(self.index_path, meta=self._meta_dict()) + raw = searcher.search( + data, + first_stage_k, + recompute_embeddings=False, + complexity=128, + beam_width=1, + prune_ratio=0.0, + batch_size=0, + ) + + labels = raw.get("labels") + distances = raw.get("distances") + if labels is None or distances is None: + return [] + + doc_scores: dict[int, float] = {} + B = len(labels) + for b in range(B): + per_doc_best: dict[int, float] = {} + for k, sid in enumerate(labels[b]): + try: + idx = int(sid) + except Exception: + continue + if 0 <= idx < len(self._labels_meta): + doc_id = int(self._labels_meta[idx]["doc_id"]) + else: + continue + score = float(distances[b][k]) + if (doc_id not in per_doc_best) or (score > per_doc_best[doc_id]): + per_doc_best[doc_id] = score + for doc_id, best_score in per_doc_best.items(): + doc_scores[doc_id] = doc_scores.get(doc_id, 0.0) + best_score + + scores = sorted(((v, k) for k, v in doc_scores.items()), key=lambda x: x[0], reverse=True) + return scores[:topk] if len(scores) >= topk else scores + + def search_exact( + self, + data: np.ndarray, + topk: int, + *, + first_stage_k: int = 200, + max_workers: int = 32, + ) -> list[tuple[float, int]]: + """ + High-precision MaxSim reranking over candidate documents. + + Steps: + 1) Run a first-stage ANN to collect candidate doc_ids (using seq-level neighbors). + 2) For each candidate doc, load all its token embeddings and compute + MaxSim(query_tokens, doc_tokens) exactly: sum(max(dot(q_i, d_j))). + + Returns top-k list of (score, doc_id). + """ + # Normalize inputs + if data.ndim == 1: + data = data.reshape(1, -1) + if data.dtype != np.float32: + data = data.astype(np.float32) + + self._load_labels_meta_if_needed() + self._build_docid_to_indices_if_needed() + + emb_path = self._embeddings_path() + if not emb_path.exists(): + # Fallback to approximate if we don't have persisted embeddings + return self.search(data, topk, first_stage_k=first_stage_k) + + # Memory-map embeddings to avoid loading all into RAM + all_embeddings = np.load(emb_path, mmap_mode="r") + if all_embeddings.dtype != np.float32: + all_embeddings = all_embeddings.astype(np.float32) + + # First-stage ANN to collect candidate doc_ids + searcher = HNSWSearcher(self.index_path, meta=self._meta_dict()) + raw = searcher.search( + data, + first_stage_k, + recompute_embeddings=False, + complexity=128, + beam_width=1, + prune_ratio=0.0, + batch_size=0, + ) + labels = raw.get("labels") + if labels is None: + return [] + candidate_doc_ids: set[int] = set() + for batch in labels: + for sid in batch: + try: + idx = int(sid) + except Exception: + continue + if 0 <= idx < len(self._labels_meta): + candidate_doc_ids.add(int(self._labels_meta[idx]["doc_id"])) + + # Exact scoring per doc (parallelized) + docid_to_indices = self._docid_to_indices + if docid_to_indices is None: + return [] + + def _score_one(doc_id: int) -> tuple[float, int]: + token_indices = docid_to_indices.get(doc_id, []) + if not token_indices: + return (0.0, doc_id) + doc_vecs = np.asarray(all_embeddings[token_indices], dtype=np.float32) + # (Q, D) x (P, D)^T -> (Q, P) then MaxSim over P, sum over Q + sim = np.dot(data, doc_vecs.T) + # nan-safe + sim = np.nan_to_num(sim, nan=-1e30, posinf=1e30, neginf=-1e30) + score = sim.max(axis=2).sum(axis=1) if sim.ndim == 3 else sim.max(axis=1).sum() + return (float(score), doc_id) + + scores: list[tuple[float, int]] = [] + # load and core time + start_time = time.time() + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as ex: + futures = [ex.submit(_score_one, doc_id) for doc_id in candidate_doc_ids] + for fut in concurrent.futures.as_completed(futures): + scores.append(fut.result()) + end_time = time.time() + print(f"Number of candidate doc ids: {len(candidate_doc_ids)}") + print(f"Time taken in load and core time: {end_time - start_time} seconds") + scores.sort(key=lambda x: x[0], reverse=True) + return scores[:topk] if len(scores) >= topk else scores + + def search_exact_all( + self, + data: np.ndarray, + topk: int, + *, + max_workers: int = 32, + ) -> list[tuple[float, int]]: + """ + Exact MaxSim over ALL documents (no ANN pre-filtering). + + This computes, for each document, sum_i max_j dot(q_i, d_j). + It memory-maps the persisted token-embedding matrix for scalability. + """ + if data.ndim == 1: + data = data.reshape(1, -1) + if data.dtype != np.float32: + data = data.astype(np.float32) + + self._load_labels_meta_if_needed() + self._build_docid_to_indices_if_needed() + + emb_path = self._embeddings_path() + if not emb_path.exists(): + return self.search(data, topk) + all_embeddings = np.load(emb_path, mmap_mode="r") + if all_embeddings.dtype != np.float32: + all_embeddings = all_embeddings.astype(np.float32) + + docid_to_indices = self._docid_to_indices + if docid_to_indices is None: + return [] + candidate_doc_ids = list(docid_to_indices.keys()) + + def _score_one(doc_id: int, _all_embeddings=all_embeddings) -> tuple[float, int]: + token_indices = docid_to_indices.get(doc_id, []) + if not token_indices: + return (0.0, doc_id) + doc_vecs = np.asarray(_all_embeddings[token_indices], dtype=np.float32) + sim = np.dot(data, doc_vecs.T) + sim = np.nan_to_num(sim, nan=-1e30, posinf=1e30, neginf=-1e30) + score = sim.max(axis=2).sum(axis=1) if sim.ndim == 3 else sim.max(axis=1).sum() + return (float(score), doc_id) + + scores: list[tuple[float, int]] = [] + # load and core time + start_time = time.time() + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as ex: + futures = [ex.submit(_score_one, d) for d in candidate_doc_ids] + for fut in concurrent.futures.as_completed(futures): + scores.append(fut.result()) + end_time = time.time() + # print number of candidate doc ids + print(f"Number of candidate doc ids: {len(candidate_doc_ids)}") + print(f"Time taken in load and core time: {end_time - start_time} seconds") + scores.sort(key=lambda x: x[0], reverse=True) + del all_embeddings + return scores[:topk] if len(scores) >= topk else scores + + def get_image(self, doc_id: int) -> Optional[Image.Image]: + """ + Retrieve the original image for a given doc_id from the index. + + Args: + doc_id: The document ID + + Returns: + PIL Image object if found, None otherwise + """ + self._load_labels_meta_if_needed() + + # Find the image_path for this doc_id (all seq_ids for same doc share the same image_path) + for meta in self._labels_meta: + if meta.get("doc_id") == doc_id: + image_path = meta.get("image_path", "") + if image_path and Path(image_path).exists(): + return Image.open(image_path) + break + return None + + def get_metadata(self, doc_id: int) -> Optional[dict]: + """ + Retrieve metadata for a given doc_id. + + Args: + doc_id: The document ID + + Returns: + Dictionary with metadata (filepath, image_path, etc.) if found, None otherwise + """ + self._load_labels_meta_if_needed() + + for meta in self._labels_meta: + if meta.get("doc_id") == doc_id: + return { + "doc_id": doc_id, + "filepath": meta.get("filepath", ""), + "image_path": meta.get("image_path", ""), + } + return None + + +class ViDoReBenchmarkEvaluator: + """ + A reusable class for evaluating ViDoRe benchmarks (v1 and v2). + This class encapsulates common functionality for building indexes, searching, and evaluating. + """ + + def __init__( + self, + model_name: str, + use_fast_plaid: bool = False, + top_k: int = 100, + first_stage_k: int = 500, + k_values: Optional[list[int]] = None, + ): + """ + Initialize the evaluator. + + Args: + model_name: Model name ("colqwen2" or "colpali") + use_fast_plaid: Whether to use Fast-Plaid instead of LEANN + top_k: Top-k results to retrieve + first_stage_k: First stage k for LEANN search + k_values: List of k values for evaluation metrics + """ + self.model_name = model_name + self.use_fast_plaid = use_fast_plaid + self.top_k = top_k + self.first_stage_k = first_stage_k + self.k_values = k_values if k_values is not None else [1, 3, 5, 10, 100] + + # Load model once (can be reused across tasks) + self._model = None + self._processor = None + self._model_name_actual = None + + def _load_model_if_needed(self): + """Lazy load the model.""" + if self._model is None: + print(f"\nLoading model: {self.model_name}") + self._model_name_actual, self._model, self._processor, _, _, _ = _load_colvision( + self.model_name + ) + print(f"Model loaded: {self._model_name_actual}") + + def build_index_from_corpus( + self, + corpus: dict[str, Image.Image], + index_path: str, + rebuild: bool = False, + ) -> tuple[Any, list[str]]: + """ + Build index from corpus images. + + Args: + corpus: dict mapping corpus_id to PIL Image + index_path: Path to save/load the index + rebuild: Whether to rebuild even if index exists + + Returns: + tuple: (retriever or fast_plaid_index object, list of corpus_ids in order) + """ + self._load_model_if_needed() + + # Ensure consistent ordering + corpus_ids = sorted(corpus.keys()) + images = [corpus[cid] for cid in corpus_ids] + + if self.use_fast_plaid: + # Check if Fast-Plaid index exists + if not rebuild and _load_fast_plaid_index_if_exists(index_path) is not None: + print(f"Fast-Plaid index already exists at {index_path}") + return _load_fast_plaid_index_if_exists(index_path), corpus_ids + + print(f"Building Fast-Plaid index at {index_path}...") + print("Embedding images...") + doc_vecs = _embed_images(self._model, self._processor, images) + + fast_plaid_index, build_time = _build_fast_plaid_index( + index_path, doc_vecs, corpus_ids, images + ) + print(f"Fast-Plaid index built in {build_time:.2f}s") + return fast_plaid_index, corpus_ids + else: + # Check if LEANN index exists + if not rebuild: + retriever = _load_retriever_if_index_exists(index_path) + if retriever is not None: + print(f"LEANN index already exists at {index_path}") + return retriever, corpus_ids + + print(f"Building LEANN index at {index_path}...") + print("Embedding images...") + doc_vecs = _embed_images(self._model, self._processor, images) + + retriever = _build_index(index_path, doc_vecs, corpus_ids, images) + print("LEANN index built") + return retriever, corpus_ids + + def search_queries( + self, + queries: dict[str, str], + corpus_ids: list[str], + index_or_retriever: Any, + fast_plaid_index_path: Optional[str] = None, + task_prompt: Optional[dict[str, str]] = None, + ) -> dict[str, dict[str, float]]: + """ + Search queries against the index. + + Args: + queries: dict mapping query_id to query text + corpus_ids: list of corpus_ids in the same order as the index + index_or_retriever: index or retriever object + fast_plaid_index_path: path to Fast-Plaid index (for metadata) + task_prompt: Optional dict with prompt for query (e.g., {"query": "..."}) + + Returns: + results: dict mapping query_id to dict of {corpus_id: score} + """ + self._load_model_if_needed() + + print(f"Searching {len(queries)} queries (top_k={self.top_k})...") + + query_ids = list(queries.keys()) + query_texts = [queries[qid] for qid in query_ids] + + # Note: ColPaliEngineWrapper does NOT use task prompt from metadata + # It uses query_prefix + text + query_augmentation_token (handled in _embed_queries) + # So we don't append task_prompt here to match MTEB behavior + + # Embed queries + print("Embedding queries...") + query_vecs = _embed_queries(self._model, self._processor, query_texts) + + results = {} + + for query_id, query_vec in zip(tqdm(query_ids, desc="Searching"), query_vecs): + if self.use_fast_plaid: + # Fast-Plaid search + search_results, _ = _search_fast_plaid(index_or_retriever, query_vec, self.top_k) + query_results = {} + for score, doc_id in search_results: + if doc_id < len(corpus_ids): + corpus_id = corpus_ids[doc_id] + query_results[corpus_id] = float(score) + else: + # LEANN search + import torch + + query_np = ( + query_vec.float().numpy() if isinstance(query_vec, torch.Tensor) else query_vec + ) + search_results = index_or_retriever.search_exact(query_np, topk=self.top_k) + query_results = {} + for score, doc_id in search_results: + if doc_id < len(corpus_ids): + corpus_id = corpus_ids[doc_id] + query_results[corpus_id] = float(score) + + results[query_id] = query_results + + return results + + @staticmethod + def evaluate_results( + results: dict[str, dict[str, float]], + qrels: dict[str, dict[str, int]], + k_values: Optional[list[int]] = None, + ) -> dict[str, float]: + """ + Evaluate retrieval results using NDCG and other metrics. + + Args: + results: dict mapping query_id to dict of {corpus_id: score} + qrels: dict mapping query_id to dict of {corpus_id: relevance_score} + k_values: List of k values for evaluation metrics + + Returns: + Dictionary of metric scores + """ + try: + from mteb._evaluators.retrieval_metrics import ( + calculate_retrieval_scores, + make_score_dict, + ) + except ImportError: + raise ImportError( + "pytrec_eval is required for evaluation. Install with: pip install pytrec-eval" + ) + + if k_values is None: + k_values = [1, 3, 5, 10, 100] + + # Check if we have any queries to evaluate + if len(results) == 0: + print("Warning: No queries to evaluate. Returning zero scores.") + scores = {} + for k in k_values: + scores[f"ndcg_at_{k}"] = 0.0 + scores[f"map_at_{k}"] = 0.0 + scores[f"recall_at_{k}"] = 0.0 + scores[f"precision_at_{k}"] = 0.0 + scores[f"mrr_at_{k}"] = 0.0 + return scores + + print(f"Evaluating results with k_values={k_values}...") + print(f"Before filtering: {len(results)} results, {len(qrels)} qrels") + + # Filter to ensure qrels and results have the same query set + # This matches MTEB behavior: only evaluate queries that exist in both + # pytrec_eval only evaluates queries in qrels, so we need to ensure + # results contains all queries in qrels, and filter out queries not in qrels + results_filtered = {qid: res for qid, res in results.items() if qid in qrels} + qrels_filtered = { + qid: rel_docs for qid, rel_docs in qrels.items() if qid in results_filtered + } + + print(f"After filtering: {len(results_filtered)} results, {len(qrels_filtered)} qrels") + + if len(results_filtered) != len(qrels_filtered): + print( + f"Warning: Mismatch between results ({len(results_filtered)}) and qrels ({len(qrels_filtered)}) queries" + ) + missing_in_results = set(qrels.keys()) - set(results.keys()) + if missing_in_results: + print(f"Queries in qrels but not in results: {len(missing_in_results)} queries") + print(f"First 5 missing queries: {list(missing_in_results)[:5]}") + + # Convert qrels to pytrec_eval format + qrels_pytrec = {} + for qid, rel_docs in qrels_filtered.items(): + qrels_pytrec[qid] = dict(rel_docs.items()) + + # Evaluate + eval_result = calculate_retrieval_scores( + results=results_filtered, + qrels=qrels_pytrec, + k_values=k_values, + ) + + # Format scores + scores = make_score_dict( + ndcg=eval_result.ndcg, + _map=eval_result.map, + recall=eval_result.recall, + precision=eval_result.precision, + mrr=eval_result.mrr, + naucs=eval_result.naucs, + naucs_mrr=eval_result.naucs_mrr, + cv_recall=eval_result.cv_recall, + task_scores={}, + ) + + return scores diff --git a/apps/multimodal/vision-based-pdf-multi-vector/multi-vector-leann-paper-example.py b/apps/multimodal/vision-based-pdf-multi-vector/multi-vector-leann-paper-example.py new file mode 100644 index 0000000..3fc61e6 --- /dev/null +++ b/apps/multimodal/vision-based-pdf-multi-vector/multi-vector-leann-paper-example.py @@ -0,0 +1,118 @@ +# pip install pdf2image +# pip install pymilvus +# pip install colpali_engine +# pip install tqdm +# pip install pillow + +import os +import re +import sys +from pathlib import Path +from typing import cast + +from PIL import Image +from tqdm import tqdm + +# Ensure local leann packages are importable before importing them +_repo_root = Path(__file__).resolve().parents[3] +_leann_core_src = _repo_root / "packages" / "leann-core" / "src" +_leann_hnsw_pkg = _repo_root / "packages" / "leann-backend-hnsw" +if str(_leann_core_src) not in sys.path: + sys.path.insert(0, str(_leann_core_src)) +if str(_leann_hnsw_pkg) not in sys.path: + sys.path.insert(0, str(_leann_hnsw_pkg)) + +from leann_multi_vector import LeannMultiVector + +import torch +from colpali_engine.models import ColPali +from colpali_engine.models.paligemma.colpali.processing_colpali import ColPaliProcessor +from colpali_engine.utils.torch_utils import ListDataset, get_torch_device +from torch.utils.data import DataLoader + +# Auto-select device: CUDA > MPS (mac) > CPU +_device_str = ( + "cuda" + if torch.cuda.is_available() + else ( + "mps" + if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available() + else "cpu" + ) +) +device = get_torch_device(_device_str) +# Prefer fp16 on GPU/MPS, bfloat16 on CPU +_dtype = torch.float16 if _device_str in ("cuda", "mps") else torch.bfloat16 +model_name = "vidore/colpali-v1.2" + +model = ColPali.from_pretrained( + model_name, + torch_dtype=_dtype, + device_map=device, +).eval() +print(f"Using device={_device_str}, dtype={_dtype}") + +queries = [ + "How to end-to-end retrieval with ColBert", + "Where is ColBERT performance Table, including text representation results?", +] + +processor = cast(ColPaliProcessor, ColPaliProcessor.from_pretrained(model_name)) + +dataloader = DataLoader( + dataset=ListDataset[str](queries), + batch_size=1, + shuffle=False, + collate_fn=lambda x: processor.process_queries(x), +) + +qs: list[torch.Tensor] = [] +for batch_query in dataloader: + with torch.no_grad(): + batch_query = {k: v.to(model.device) for k, v in batch_query.items()} + embeddings_query = model(**batch_query) + qs.extend(list(torch.unbind(embeddings_query.to("cpu")))) +print(qs[0].shape) +# %% +def _page_sort_key(name: str) -> int: + match = re.search(r"\d+", name) + return int(match.group()) if match else -1 + + +page_filenames = sorted(os.listdir("./pages"), key=_page_sort_key) +images = [Image.open(os.path.join("./pages", name)) for name in page_filenames] + +dataloader = DataLoader( + dataset=ListDataset[str](images), + batch_size=1, + shuffle=False, + collate_fn=lambda x: processor.process_images(x), +) + +ds: list[torch.Tensor] = [] +for batch_doc in tqdm(dataloader): + with torch.no_grad(): + batch_doc = {k: v.to(model.device) for k, v in batch_doc.items()} + embeddings_doc = model(**batch_doc) + ds.extend(list(torch.unbind(embeddings_doc.to("cpu")))) + +print(ds[0].shape) + +# %% +# Build HNSW index via LeannMultiVector primitives and run search +index_path = "./indexes/colpali.leann" +retriever = LeannMultiVector(index_path=index_path, dim=int(ds[0].shape[-1])) +retriever.create_collection() +filepaths = [os.path.join("./pages", name) for name in page_filenames] +for i in range(len(filepaths)): + data = { + "colbert_vecs": ds[i].float().numpy(), + "doc_id": i, + "filepath": filepaths[i], + } + retriever.insert(data) +retriever.create_index() +for query in qs: + query_np = query.float().numpy() + result = retriever.search(query_np, topk=1) + print(filepaths[result[0][1]]) diff --git a/apps/multimodal/vision-based-pdf-multi-vector/multi-vector-leann-similarity-map.py b/apps/multimodal/vision-based-pdf-multi-vector/multi-vector-leann-similarity-map.py new file mode 100644 index 0000000..5298053 --- /dev/null +++ b/apps/multimodal/vision-based-pdf-multi-vector/multi-vector-leann-similarity-map.py @@ -0,0 +1,728 @@ +## Jupyter-style notebook script +# %% +# uv pip install matplotlib qwen_vl_utils +import argparse +import faulthandler +import os +import time +from typing import Any, Optional, cast + +import numpy as np +from PIL import Image +from tqdm import tqdm + +# Enable faulthandler to get stack trace on segfault +faulthandler.enable() + + +from leann_multi_vector import ( # utility functions/classes + _ensure_repo_paths_importable, + _load_images_from_dir, + _maybe_convert_pdf_to_images, + _load_colvision, + _embed_images, + _embed_queries, + _build_index, + _load_retriever_if_index_exists, + _generate_similarity_map, + _build_fast_plaid_index, + _load_fast_plaid_index_if_exists, + _search_fast_plaid, + _get_fast_plaid_image, + _get_fast_plaid_metadata, + QwenVL, +) + +_ensure_repo_paths_importable(__file__) + +# %% +# Config +os.environ["TOKENIZERS_PARALLELISM"] = "false" +QUERY = "The paper talk about the latent video generative model and data curation in the related work part?" +MODEL: str = "colqwen2" # "colpali" or "colqwen2" + +# Data source: set to True to use the Hugging Face dataset example (recommended) +USE_HF_DATASET: bool = True +# Single dataset name (used when DATASET_NAMES is None) +DATASET_NAME: str = "weaviate/arXiv-AI-papers-multi-vector" +# Multiple datasets to combine (if provided, DATASET_NAME is ignored) +# Can be: +# - List of strings: ["dataset1", "dataset2"] +# - List of tuples: [("dataset1", "config1"), ("dataset2", None)] # None = no config needed +# - Mixed: ["dataset1", ("dataset2", "config2")] +# +# Some potential datasets with images (may need IMAGE_FIELD_NAME adjustment): +# - "weaviate/arXiv-AI-papers-multi-vector" (current, has "page_image" field) +# - ("lmms-lab/DocVQA", "DocVQA") (has "image" field, document images, needs config) +# - ("lmms-lab/DocVQA", "InfographicVQA") (has "image" field, infographic images) +# - "pixparse/arxiv-papers" (if available, arXiv papers) +# - "allenai/ai2d" (AI2D diagram dataset, has "image" field) +# - "huggingface/document-images" (if available) +# Note: Check dataset structure first - some may need IMAGE_FIELD_NAME specified +# DATASET_NAMES: Optional[list[str | tuple[str, Optional[str]]]] = None +DATASET_NAMES = [ + "weaviate/arXiv-AI-papers-multi-vector", + # ("lmms-lab/DocVQA", "DocVQA"), # Specify config name for datasets with multiple configs +] +# Load multiple splits to get more data (e.g., ["train", "test", "validation"]) +# Set to None to try loading all available splits automatically +DATASET_SPLITS: Optional[list[str]] = ["train", "test"] # None = auto-detect all splits +# Image field name in the dataset (auto-detect if None) +# Common names: "page_image", "image", "images", "img" +IMAGE_FIELD_NAME: Optional[str] = None # None = auto-detect +MAX_DOCS: Optional[int] = None # limit number of pages to index; None = all + +# Local pages (used when USE_HF_DATASET == False) +PDF: Optional[str] = None # e.g., "./pdfs/2004.12832v2.pdf" +PAGES_DIR: str = "./pages" +# Custom folder path (takes precedence over USE_HF_DATASET and PAGES_DIR) +# If set, images will be loaded directly from this folder +CUSTOM_FOLDER_PATH: Optional[str] = None # e.g., "/home/ubuntu/dr-tulu/agent/screenshots" +# Whether to recursively search subdirectories when loading from custom folder +CUSTOM_FOLDER_RECURSIVE: bool = False # Set to True to search subdirectories + +# Index + retrieval settings +# Use a different index path for larger dataset to avoid overwriting existing index +INDEX_PATH: str = "./indexes/colvision_large.leann" +# Fast-Plaid index settings (alternative to LEANN index) +# These are now command-line arguments (see CLI overrides section) +TOPK: int = 3 +FIRST_STAGE_K: int = 500 +REBUILD_INDEX: bool = False # Set to True to force rebuild even if index exists + +# Artifacts +SAVE_TOP_IMAGE: Optional[str] = "./figures/retrieved_page.png" +SIMILARITY_MAP: bool = True +SIM_TOKEN_IDX: int = 13 # -1 means auto-select the most salient token +SIM_OUTPUT: str = "./figures/similarity_map.png" +ANSWER: bool = True +MAX_NEW_TOKENS: int = 1024 + + +# %% +# CLI overrides +parser = argparse.ArgumentParser(description="Multi-vector LEANN similarity map demo") +parser.add_argument( + "--search-method", + type=str, + choices=["ann", "exact", "exact-all"], + default="ann", + help="Which search method to use: 'ann' (fast ANN), 'exact' (ANN + exact rerank), or 'exact-all' (exact over all docs).", +) +parser.add_argument( + "--query", + type=str, + default=QUERY, + help=f"Query string to search for. Default: '{QUERY}'", +) +parser.add_argument( + "--use-fast-plaid", + action="store_true", + default=False, + help="Set to True to use fast-plaid instead of LEANN. Default: False", +) +parser.add_argument( + "--fast-plaid-index-path", + type=str, + default="./indexes/colvision_fastplaid", + help="Path to the Fast-Plaid index. Default: './indexes/colvision_fastplaid'", +) +parser.add_argument( + "--topk", + type=int, + default=TOPK, + help=f"Number of top results to retrieve. Default: {TOPK}", +) +parser.add_argument( + "--custom-folder", + type=str, + default=None, + help="Path to a custom folder containing images to search. Takes precedence over dataset loading. Default: None", +) +parser.add_argument( + "--recursive", + action="store_true", + default=False, + help="Recursively search subdirectories when loading images from custom folder. Default: False", +) +parser.add_argument( + "--rebuild-index", + action="store_true", + default=False, + help="Force rebuild the index even if it already exists. Default: False (reuse existing index if available)", +) +cli_args, _unknown = parser.parse_known_args() +SEARCH_METHOD: str = cli_args.search_method +QUERY = cli_args.query # Override QUERY with CLI argument if provided +USE_FAST_PLAID: bool = cli_args.use_fast_plaid +FAST_PLAID_INDEX_PATH: str = cli_args.fast_plaid_index_path +TOPK: int = cli_args.topk # Override TOPK with CLI argument if provided +CUSTOM_FOLDER_PATH = cli_args.custom_folder if cli_args.custom_folder else CUSTOM_FOLDER_PATH # Override with CLI argument if provided +CUSTOM_FOLDER_RECURSIVE = cli_args.recursive if cli_args.recursive else CUSTOM_FOLDER_RECURSIVE # Override with CLI argument if provided +REBUILD_INDEX = cli_args.rebuild_index # Override REBUILD_INDEX with CLI argument + +# %% + +# Step 1: Check if we can skip data loading (index already exists) +retriever: Optional[Any] = None +fast_plaid_index: Optional[Any] = None +need_to_build_index = REBUILD_INDEX + +if USE_FAST_PLAID: + # Fast-Plaid index handling + if not REBUILD_INDEX: + try: + fast_plaid_index = _load_fast_plaid_index_if_exists(FAST_PLAID_INDEX_PATH) + if fast_plaid_index is not None: + print(f"βœ“ Fast-Plaid index found at {FAST_PLAID_INDEX_PATH}") + need_to_build_index = False + else: + print(f"Fast-Plaid index not found, will build new index") + need_to_build_index = True + except Exception as e: + # If loading fails (e.g., memory error, corrupted index), rebuild + print(f"Warning: Failed to load Fast-Plaid index: {e}") + print("Will rebuild the index...") + need_to_build_index = True + fast_plaid_index = None + else: + print(f"REBUILD_INDEX=True, will rebuild Fast-Plaid index") + need_to_build_index = True +else: + # Original LEANN index handling + if not REBUILD_INDEX: + retriever = _load_retriever_if_index_exists(INDEX_PATH) + if retriever is not None: + retriever_any = cast(Any, retriever) + print(f"βœ“ Index loaded from {INDEX_PATH}") + print(f"βœ“ Images available at: {retriever_any._images_dir_path()}") + need_to_build_index = False + else: + print(f"Index not found, will build new index") + need_to_build_index = True + else: + print(f"REBUILD_INDEX=True, will rebuild index") + need_to_build_index = True + +# Step 2: Load data only if we need to build the index +if need_to_build_index: + print("Loading dataset...") + # Check for custom folder path first (takes precedence) + if CUSTOM_FOLDER_PATH: + if not os.path.isdir(CUSTOM_FOLDER_PATH): + raise RuntimeError(f"Custom folder path does not exist: {CUSTOM_FOLDER_PATH}") + print(f"Loading images from custom folder: {CUSTOM_FOLDER_PATH}") + if CUSTOM_FOLDER_RECURSIVE: + print(" (recursive mode: searching subdirectories)") + filepaths, images = _load_images_from_dir(CUSTOM_FOLDER_PATH, recursive=CUSTOM_FOLDER_RECURSIVE) + print(f" Found {len(filepaths)} image files") + if not images: + raise RuntimeError( + f"No images found in {CUSTOM_FOLDER_PATH}. Ensure the folder contains image files (.png, .jpg, .jpeg, .webp)." + ) + print(f" Successfully loaded {len(images)} images") + # Use filenames as identifiers instead of full paths for cleaner metadata + filepaths = [os.path.basename(fp) for fp in filepaths] + elif USE_HF_DATASET: + from datasets import Dataset, concatenate_datasets, load_dataset + + # Determine which datasets to load + if DATASET_NAMES is not None: + dataset_names_to_load = DATASET_NAMES + print(f"Loading {len(dataset_names_to_load)} datasets: {dataset_names_to_load}") + else: + dataset_names_to_load = [DATASET_NAME] + print(f"Loading single dataset: {DATASET_NAME}") + + # Load and combine datasets + all_datasets_to_concat = [] + + for dataset_entry in dataset_names_to_load: + # Handle both string and tuple formats + if isinstance(dataset_entry, tuple): + dataset_name, config_name = dataset_entry + else: + dataset_name = dataset_entry + config_name = None + + print(f"\nProcessing dataset: {dataset_name}" + (f" (config: {config_name})" if config_name else "")) + + # Load dataset to check available splits + # If config_name is provided, use it; otherwise try without config + try: + if config_name: + dataset_dict = load_dataset(dataset_name, config_name) + else: + dataset_dict = load_dataset(dataset_name) + except ValueError as e: + if "Config name is missing" in str(e): + # Try to get available configs and suggest + from datasets import get_dataset_config_names + try: + available_configs = get_dataset_config_names(dataset_name) + raise ValueError( + f"Dataset '{dataset_name}' requires a config name. " + f"Available configs: {available_configs}. " + f"Please specify as: ('{dataset_name}', 'config_name')" + ) from e + except Exception: + raise ValueError( + f"Dataset '{dataset_name}' requires a config name. " + f"Please specify as: ('{dataset_name}', 'config_name')" + ) from e + raise + + if not isinstance(dataset_dict, dict): + dataset = cast(Dataset, dataset_dict) + all_datasets_to_concat.append(dataset) + continue + + # Determine which splits to load + if DATASET_SPLITS is None: + # Auto-detect: try to load all available splits + available_splits = list(dataset_dict.keys()) + print(f" Auto-detected splits: {available_splits}") + splits_to_load = available_splits + else: + splits_to_load = DATASET_SPLITS + + # Load and concatenate multiple splits for this dataset + datasets_to_concat: list[Dataset] = [] + for split in splits_to_load: + if split not in dataset_dict: + print( + f" Warning: Split '{split}' not found in dataset. Available splits: {list(dataset_dict.keys())}" + ) + continue + split_dataset = cast(Dataset, dataset_dict[split]) + print(f" Loaded split '{split}': {len(split_dataset)} pages") + datasets_to_concat.append(split_dataset) + + if not datasets_to_concat: + print(f" Warning: No valid splits found for {dataset_name}. Skipping.") + continue + + # Concatenate splits for this dataset + if len(datasets_to_concat) > 1: + combined_dataset = concatenate_datasets(datasets_to_concat) + print(f" Concatenated {len(datasets_to_concat)} splits into {len(combined_dataset)} pages") + else: + combined_dataset = datasets_to_concat[0] + + all_datasets_to_concat.append(combined_dataset) + + if not all_datasets_to_concat: + raise RuntimeError("No valid datasets or splits found.") + + # Concatenate all datasets + if len(all_datasets_to_concat) > 1: + dataset = concatenate_datasets(all_datasets_to_concat) + print(f"\nConcatenated {len(all_datasets_to_concat)} datasets into {len(dataset)} total pages") + else: + dataset = all_datasets_to_concat[0] + + # Apply MAX_DOCS limit if specified + N = len(dataset) if MAX_DOCS is None else min(MAX_DOCS, len(dataset)) + if N < len(dataset): + print(f"Limiting to {N} pages (from {len(dataset)} total)") + dataset = dataset.select(range(N)) + + # Auto-detect image field name if not specified + if IMAGE_FIELD_NAME is None: + # Check multiple samples to find the most common image field + # (useful when datasets are merged and may have different field names) + possible_image_fields = ["page_image", "image", "images", "img", "page", "document_image"] + field_counts = {} + + # Check first few samples to find image fields + num_samples_to_check = min(10, len(dataset)) + for sample_idx in range(num_samples_to_check): + sample = dataset[sample_idx] + for field in possible_image_fields: + if field in sample and sample[field] is not None: + value = sample[field] + if isinstance(value, Image.Image) or (hasattr(value, 'size') and hasattr(value, 'mode')): + field_counts[field] = field_counts.get(field, 0) + 1 + + # Choose the most common field, or first found if tied + if field_counts: + image_field = max(field_counts.items(), key=lambda x: x[1])[0] + print(f"Auto-detected image field: '{image_field}' (found in {field_counts[image_field]}/{num_samples_to_check} samples)") + else: + # Fallback: check first sample only + sample = dataset[0] + image_field = None + for field in possible_image_fields: + if field in sample: + value = sample[field] + if isinstance(value, Image.Image) or (hasattr(value, 'size') and hasattr(value, 'mode')): + image_field = field + break + if image_field is None: + raise RuntimeError( + f"Could not auto-detect image field. Available fields: {list(sample.keys())}. " + f"Please specify IMAGE_FIELD_NAME manually." + ) + print(f"Auto-detected image field: '{image_field}'") + else: + image_field = IMAGE_FIELD_NAME + if image_field not in dataset[0]: + raise RuntimeError( + f"Image field '{image_field}' not found. Available fields: {list(dataset[0].keys())}" + ) + + filepaths: list[str] = [] + images: list[Image.Image] = [] + for i in tqdm(range(len(dataset)), desc="Loading dataset", total=len(dataset)): + p = dataset[i] + # Try to compose a descriptive identifier + # Handle different dataset structures + identifier_parts = [] + + # Helper function to safely get field value + def safe_get(field_name, default=None): + if field_name in p and p[field_name] is not None: + return p[field_name] + return default + + # Try to get various identifier fields + if safe_get("paper_arxiv_id"): + identifier_parts.append(f"arXiv:{p['paper_arxiv_id']}") + if safe_get("paper_title"): + identifier_parts.append(f"title:{p['paper_title']}") + if safe_get("page_number") is not None: + try: + identifier_parts.append(f"page:{int(p['page_number'])}") + except (ValueError, TypeError): + # If conversion fails, use the raw value or skip + if p['page_number']: + identifier_parts.append(f"page:{p['page_number']}") + if safe_get("page_id"): + identifier_parts.append(f"id:{p['page_id']}") + elif safe_get("questionId"): + identifier_parts.append(f"qid:{p['questionId']}") + elif safe_get("docId"): + identifier_parts.append(f"docId:{p['docId']}") + elif safe_get("id"): + identifier_parts.append(f"id:{p['id']}") + + # If no identifier parts found, create one from index + if identifier_parts: + identifier = "|".join(identifier_parts) + else: + # Create identifier from available fields or index + fallback_parts = [] + # Try common fields that might exist + for field in ["ucsf_document_id", "docId", "questionId", "id"]: + if safe_get(field): + fallback_parts.append(f"{field}:{p[field]}") + break + if fallback_parts: + identifier = "|".join(fallback_parts) + f"|idx:{i}" + else: + identifier = f"doc_{i}" + + filepaths.append(identifier) + + # Get image - try detected field first, then fallback to other common fields + img = None + if image_field in p and p[image_field] is not None: + img = p[image_field] + else: + # Fallback: try other common image field names + for fallback_field in ["image", "page_image", "images", "img"]: + if fallback_field in p and p[fallback_field] is not None: + img = p[fallback_field] + break + + if img is None: + raise RuntimeError( + f"No image found for sample {i}. Available fields: {list(p.keys())}. " + f"Expected field: {image_field}" + ) + + # Ensure it's a PIL Image + if not isinstance(img, Image.Image): + if hasattr(img, 'convert'): + img = img.convert('RGB') + else: + img = Image.fromarray(img) if hasattr(img, '__array__') else Image.open(img) + images.append(img) + else: + _maybe_convert_pdf_to_images(PDF, PAGES_DIR) + filepaths, images = _load_images_from_dir(PAGES_DIR) + if not images: + raise RuntimeError( + f"No images found in {PAGES_DIR}. Provide PDF path in PDF variable or ensure images exist." + ) + print(f"Loaded {len(images)} images") + + # Memory check before loading model + try: + import psutil + import torch + process = psutil.Process(os.getpid()) + mem_info = process.memory_info() + print(f"Memory usage after loading images: {mem_info.rss / 1024 / 1024 / 1024:.2f} GB") + if torch.cuda.is_available(): + print(f"GPU memory allocated: {torch.cuda.memory_allocated() / 1024**3:.2f} GB") + print(f"GPU memory reserved: {torch.cuda.memory_reserved() / 1024**3:.2f} GB") + except ImportError: + pass +else: + print("Skipping dataset loading (using existing index)") + filepaths = [] # Not needed when using existing index + images = [] # Not needed when using existing index + + +# %% +# Step 3: Load model and processor (only if we need to build index or perform search) +print("Step 3: Loading model and processor...") +print(f" Model: {MODEL}") +try: + import sys + print(f" Python version: {sys.version}") + print(f" Python executable: {sys.executable}") + + model_name, model, processor, device_str, device, dtype = _load_colvision(MODEL) + print(f"βœ“ Using model={model_name}, device={device_str}, dtype={dtype}") + + # Memory check after loading model + try: + import psutil + import torch + process = psutil.Process(os.getpid()) + mem_info = process.memory_info() + print(f" Memory usage after loading model: {mem_info.rss / 1024 / 1024 / 1024:.2f} GB") + if torch.cuda.is_available(): + print(f" GPU memory allocated: {torch.cuda.memory_allocated() / 1024**3:.2f} GB") + print(f" GPU memory reserved: {torch.cuda.memory_reserved() / 1024**3:.2f} GB") + except ImportError: + pass +except Exception as e: + print(f"βœ— Error loading model: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + raise + + +# %% + +# %% +# Step 4: Build index if needed +if need_to_build_index: + print("Step 4: Building index...") + print(f" Number of images: {len(images)}") + print(f" Number of filepaths: {len(filepaths)}") + + try: + print(" Embedding images...") + doc_vecs = _embed_images(model, processor, images) + print(f" Embedded {len(doc_vecs)} documents") + print(f" First doc vec shape: {doc_vecs[0].shape if len(doc_vecs) > 0 else 'N/A'}") + except Exception as e: + print(f"Error embedding images: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + raise + + if USE_FAST_PLAID: + # Build Fast-Plaid index + print(" Building Fast-Plaid index...") + try: + fast_plaid_index, build_secs = _build_fast_plaid_index( + FAST_PLAID_INDEX_PATH, doc_vecs, filepaths, images + ) + from pathlib import Path + print(f"βœ“ Fast-Plaid index built in {build_secs:.3f}s") + print(f"βœ“ Index saved to: {FAST_PLAID_INDEX_PATH}") + print(f"βœ“ Images saved to: {Path(FAST_PLAID_INDEX_PATH) / 'images'}") + except Exception as e: + print(f"Error building Fast-Plaid index: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + raise + finally: + # Clear memory + print(" Clearing memory...") + del images, filepaths, doc_vecs + else: + # Build original LEANN index + try: + retriever = _build_index(INDEX_PATH, doc_vecs, filepaths, images) + print(f"βœ“ Index built and images saved to: {retriever._images_dir_path()}") + except Exception as e: + print(f"Error building LEANN index: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + raise + finally: + # Clear memory + print(" Clearing memory...") + del images, filepaths, doc_vecs + +# Note: Images are now stored separately, retriever/fast_plaid_index will reference them + + +# %% +# Step 5: Embed query and search +_t0 = time.perf_counter() +q_vec = _embed_queries(model, processor, [QUERY])[0] +query_embed_secs = time.perf_counter() - _t0 + +print(f"[Search] Method: {SEARCH_METHOD}") +print(f"[Timing] Query embedding: {query_embed_secs:.3f}s") + +# Run the selected search method and time it +if USE_FAST_PLAID: + # Fast-Plaid search + if fast_plaid_index is None: + fast_plaid_index = _load_fast_plaid_index_if_exists(FAST_PLAID_INDEX_PATH) + if fast_plaid_index is None: + raise RuntimeError(f"Fast-Plaid index not found at {FAST_PLAID_INDEX_PATH}") + + results, search_secs = _search_fast_plaid(fast_plaid_index, q_vec, TOPK) + print(f"[Timing] Fast-Plaid Search: {search_secs:.3f}s") +else: + # Original LEANN search + query_np = q_vec.float().numpy() + + if retriever is None: + raise RuntimeError("Retriever not initialized") + retriever_any = cast(Any, retriever) + + if SEARCH_METHOD == "ann": + results = retriever_any.search(query_np, topk=TOPK, first_stage_k=FIRST_STAGE_K) + search_secs = time.perf_counter() - _t0 + print(f"[Timing] Search (ANN): {search_secs:.3f}s (first_stage_k={FIRST_STAGE_K})") + elif SEARCH_METHOD == "exact": + results = retriever_any.search_exact(query_np, topk=TOPK, first_stage_k=FIRST_STAGE_K) + search_secs = time.perf_counter() - _t0 + print(f"[Timing] Search (Exact rerank): {search_secs:.3f}s (first_stage_k={FIRST_STAGE_K})") + elif SEARCH_METHOD == "exact-all": + results = retriever_any.search_exact_all(query_np, topk=TOPK) + search_secs = time.perf_counter() - _t0 + print(f"[Timing] Search (Exact all): {search_secs:.3f}s") + else: + results = [] +if not results: + print("No results found.") +else: + print(f'Top {len(results)} results for query: "{QUERY}"') + print("\n[DEBUG] Retrieval details:") + top_images: list[Image.Image] = [] + image_hashes = {} # Track image hashes to detect duplicates + + for rank, (score, doc_id) in enumerate(results, start=1): + # Retrieve image and metadata based on index type + if USE_FAST_PLAID: + # Fast-Plaid: load image and get metadata + image = _get_fast_plaid_image(FAST_PLAID_INDEX_PATH, doc_id) + if image is None: + print(f"Warning: Could not find image for doc_id {doc_id}") + continue + + metadata = _get_fast_plaid_metadata(FAST_PLAID_INDEX_PATH, doc_id) + path = metadata.get("filepath", f"doc_{doc_id}") if metadata else f"doc_{doc_id}" + top_images.append(image) + else: + # Original LEANN: retrieve from retriever + if retriever is None: + raise RuntimeError("Retriever not initialized") + retriever_any = cast(Any, retriever) + image = retriever_any.get_image(doc_id) + if image is None: + print(f"Warning: Could not retrieve image for doc_id {doc_id}") + continue + + metadata = retriever_any.get_metadata(doc_id) + path = metadata.get("filepath", "unknown") if metadata else "unknown" + top_images.append(image) + + # Calculate image hash to detect duplicates + import hashlib + import io + # Convert image to bytes for hashing + img_bytes = io.BytesIO() + image.save(img_bytes, format='PNG') + image_bytes = img_bytes.getvalue() + image_hash = hashlib.md5(image_bytes).hexdigest()[:8] + + # Check if this image was already seen + duplicate_info = "" + if image_hash in image_hashes: + duplicate_info = f" [DUPLICATE of rank {image_hashes[image_hash]}]" + else: + image_hashes[image_hash] = rank + + # Print detailed information + print(f"{rank}) doc_id={doc_id}, MaxSim={score:.4f}, Page={path}, ImageHash={image_hash}{duplicate_info}") + if metadata: + print(f" Metadata: {metadata}") + + if SAVE_TOP_IMAGE: + from pathlib import Path as _Path + + base = _Path(SAVE_TOP_IMAGE) + base.parent.mkdir(parents=True, exist_ok=True) + for rank, img in enumerate(top_images[:TOPK], start=1): + if base.suffix: + out_path = base.parent / f"{base.stem}_rank{rank}{base.suffix}" + else: + out_path = base / f"retrieved_page_rank{rank}.png" + img.save(str(out_path)) + # Print the retrieval score (document-level MaxSim) alongside the saved path + try: + score, _doc_id = results[rank - 1] + print(f"Saved retrieved page (rank {rank}) [MaxSim={score:.4f}] to: {out_path}") + except Exception: + print(f"Saved retrieved page (rank {rank}) to: {out_path}") + + +# %% +# Step 6: Similarity maps for top-K results +if results and SIMILARITY_MAP: + token_idx = None if SIM_TOKEN_IDX < 0 else int(SIM_TOKEN_IDX) + from pathlib import Path as _Path + + output_base = _Path(SIM_OUTPUT) if SIM_OUTPUT else None + for rank, img in enumerate(top_images[:TOPK], start=1): + if output_base: + if output_base.suffix: + out_dir = output_base.parent + out_name = f"{output_base.stem}_rank{rank}{output_base.suffix}" + out_path = str(out_dir / out_name) + else: + out_dir = output_base + out_dir.mkdir(parents=True, exist_ok=True) + out_path = str(out_dir / f"similarity_map_rank{rank}.png") + else: + out_path = None + chosen_idx, max_sim = _generate_similarity_map( + model=model, + processor=processor, + image=img, + query=QUERY, + token_idx=token_idx, + output_path=out_path, + ) + if out_path: + print( + f"Saved similarity map for rank {rank}, token #{chosen_idx} (max={max_sim:.2f}) to: {out_path}" + ) + else: + print( + f"Computed similarity map for rank {rank}, token #{chosen_idx} (max={max_sim:.2f})" + ) + + +# %% +# Step 7: Optional answer generation +if results and ANSWER: + qwen = QwenVL(device=device_str) + _t0 = time.perf_counter() + response = qwen.answer(QUERY, top_images[:TOPK], max_new_tokens=MAX_NEW_TOKENS) + gen_secs = time.perf_counter() - _t0 + print(f"[Timing] Generation: {gen_secs:.3f}s") + print("\nAnswer:") + print(response) diff --git a/apps/multimodal/vision-based-pdf-multi-vector/vidore_v1_benchmark.py b/apps/multimodal/vision-based-pdf-multi-vector/vidore_v1_benchmark.py new file mode 100644 index 0000000..3b2d7df --- /dev/null +++ b/apps/multimodal/vision-based-pdf-multi-vector/vidore_v1_benchmark.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +""" +Modular script to reproduce NDCG results for ViDoRe v1 benchmark. + +This script uses the interface from leann_multi_vector.py to: +1. Download ViDoRe v1 datasets +2. Build indexes (LEANN or Fast-Plaid) +3. Perform retrieval +4. Evaluate using NDCG metrics + +Usage: + # Evaluate all ViDoRe v1 tasks + python vidore_v1_benchmark.py --model colqwen2 --tasks all + + # Evaluate specific task + python vidore_v1_benchmark.py --model colqwen2 --task VidoreArxivQARetrieval + + # Use Fast-Plaid index + python vidore_v1_benchmark.py --model colqwen2 --use-fast-plaid --fast-plaid-index-path ./indexes/vidore_fastplaid + + # Rebuild index + python vidore_v1_benchmark.py --model colqwen2 --rebuild-index +""" + +import argparse +import json +import os +from typing import Any, Optional, cast + +from datasets import Dataset, load_dataset +from leann_multi_vector import ( + ViDoReBenchmarkEvaluator, + _ensure_repo_paths_importable, +) + +_ensure_repo_paths_importable(__file__) + +# ViDoRe v1 task configurations +# Prompts match MTEB task metadata prompts +VIDORE_V1_TASKS = { + "VidoreArxivQARetrieval": { + "dataset_path": "vidore/arxivqa_test_subsampled_beir", + "revision": "7d94d570960eac2408d3baa7a33f9de4822ae3e4", + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "VidoreDocVQARetrieval": { + "dataset_path": "vidore/docvqa_test_subsampled_beir", + "revision": "162ba2fc1a8437eda8b6c37b240bc1c0f0deb092", + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "VidoreInfoVQARetrieval": { + "dataset_path": "vidore/infovqa_test_subsampled_beir", + "revision": "b802cc5fd6c605df2d673a963667d74881d2c9a4", + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "VidoreTabfquadRetrieval": { + "dataset_path": "vidore/tabfquad_test_subsampled_beir", + "revision": "61a2224bcd29b7b261a4892ff4c8bea353527a31", + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "VidoreTatdqaRetrieval": { + "dataset_path": "vidore/tatdqa_test_beir", + "revision": "5feb5630fdff4d8d189ffedb2dba56862fdd45c0", + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "VidoreShiftProjectRetrieval": { + "dataset_path": "vidore/shiftproject_test_beir", + "revision": "84a382e05c4473fed9cff2bbae95fe2379416117", + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "VidoreSyntheticDocQAAIRetrieval": { + "dataset_path": "vidore/syntheticDocQA_artificial_intelligence_test_beir", + "revision": "2d9ebea5a1c6e9ef4a3b902a612f605dca11261c", + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "VidoreSyntheticDocQAEnergyRetrieval": { + "dataset_path": "vidore/syntheticDocQA_energy_test_beir", + "revision": "9935aadbad5c8deec30910489db1b2c7133ae7a7", + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "VidoreSyntheticDocQAGovernmentReportsRetrieval": { + "dataset_path": "vidore/syntheticDocQA_government_reports_test_beir", + "revision": "b4909afa930f81282fd20601e860668073ad02aa", + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "VidoreSyntheticDocQAHealthcareIndustryRetrieval": { + "dataset_path": "vidore/syntheticDocQA_healthcare_industry_test_beir", + "revision": "f9e25d5b6e13e1ad9f5c3cce202565031b3ab164", + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, +} + +# Task name aliases (short names -> full names) +TASK_ALIASES = { + "arxivqa": "VidoreArxivQARetrieval", + "docvqa": "VidoreDocVQARetrieval", + "infovqa": "VidoreInfoVQARetrieval", + "tabfquad": "VidoreTabfquadRetrieval", + "tatdqa": "VidoreTatdqaRetrieval", + "shiftproject": "VidoreShiftProjectRetrieval", + "syntheticdocqa_ai": "VidoreSyntheticDocQAAIRetrieval", + "syntheticdocqa_energy": "VidoreSyntheticDocQAEnergyRetrieval", + "syntheticdocqa_government": "VidoreSyntheticDocQAGovernmentReportsRetrieval", + "syntheticdocqa_healthcare": "VidoreSyntheticDocQAHealthcareIndustryRetrieval", +} + + +def normalize_task_name(task_name: str) -> str: + """Normalize task name (handle aliases).""" + task_name_lower = task_name.lower() + if task_name in VIDORE_V1_TASKS: + return task_name + if task_name_lower in TASK_ALIASES: + return TASK_ALIASES[task_name_lower] + # Try partial match + for alias, full_name in TASK_ALIASES.items(): + if alias in task_name_lower or task_name_lower in alias: + return full_name + return task_name + + +def get_safe_model_name(model_name: str) -> str: + """Get a safe model name for use in file paths.""" + import hashlib + import os + + # If it's a path, use basename or hash + if os.path.exists(model_name) and os.path.isdir(model_name): + # Use basename if it's reasonable, otherwise use hash + basename = os.path.basename(model_name.rstrip("/")) + if basename and len(basename) < 100 and not basename.startswith("."): + return basename + # Use hash for very long or problematic paths + return hashlib.md5(model_name.encode()).hexdigest()[:16] + # For HuggingFace model names, replace / with _ + return model_name.replace("/", "_").replace(":", "_") + + +def load_vidore_v1_data( + dataset_path: str, + revision: Optional[str] = None, + split: str = "test", +): + """ + Load ViDoRe v1 dataset. + + Returns: + corpus: dict mapping corpus_id to PIL Image + queries: dict mapping query_id to query text + qrels: dict mapping query_id to dict of {corpus_id: relevance_score} + """ + print(f"Loading dataset: {dataset_path} (split={split})") + + # Load queries - cast to Dataset since we know split returns Dataset not DatasetDict + query_ds = cast(Dataset, load_dataset(dataset_path, "queries", split=split, revision=revision)) + + queries: dict[str, str] = {} + for row in query_ds: + row_dict = cast(dict[str, Any], row) + query_id = f"query-{split}-{row_dict['query-id']}" + queries[query_id] = row_dict["query"] + + # Load corpus (images) - cast to Dataset + corpus_ds = cast(Dataset, load_dataset(dataset_path, "corpus", split=split, revision=revision)) + + corpus: dict[str, Any] = {} + for row in corpus_ds: + row_dict = cast(dict[str, Any], row) + corpus_id = f"corpus-{split}-{row_dict['corpus-id']}" + # Extract image from the dataset row + if "image" in row_dict: + corpus[corpus_id] = row_dict["image"] + elif "page_image" in row_dict: + corpus[corpus_id] = row_dict["page_image"] + else: + raise ValueError( + f"No image field found in corpus. Available fields: {list(row_dict.keys())}" + ) + + # Load qrels (relevance judgments) - cast to Dataset + qrels_ds = cast(Dataset, load_dataset(dataset_path, "qrels", split=split, revision=revision)) + + qrels: dict[str, dict[str, int]] = {} + for row in qrels_ds: + row_dict = cast(dict[str, Any], row) + query_id = f"query-{split}-{row_dict['query-id']}" + corpus_id = f"corpus-{split}-{row_dict['corpus-id']}" + if query_id not in qrels: + qrels[query_id] = {} + qrels[query_id][corpus_id] = int(row_dict["score"]) + + print( + f"Loaded {len(queries)} queries, {len(corpus)} corpus items, {len(qrels)} query-relevance mappings" + ) + + # Filter qrels to only include queries that exist + qrels = {qid: rel_docs for qid, rel_docs in qrels.items() if qid in queries} + + # Filter out queries without any relevant documents (matching MTEB behavior) + # This is important for correct NDCG calculation + qrels_filtered = {qid: rel_docs for qid, rel_docs in qrels.items() if len(rel_docs) > 0} + queries_filtered = { + qid: query_text for qid, query_text in queries.items() if qid in qrels_filtered + } + + print( + f"After filtering queries without positives: {len(queries_filtered)} queries, {len(qrels_filtered)} query-relevance mappings" + ) + + return corpus, queries_filtered, qrels_filtered + + +def evaluate_task( + task_name: str, + model_name: str, + index_path: str, + use_fast_plaid: bool = False, + fast_plaid_index_path: Optional[str] = None, + rebuild_index: bool = False, + top_k: int = 1000, + first_stage_k: int = 500, + k_values: Optional[list[int]] = None, + output_dir: Optional[str] = None, +): + """ + Evaluate a single ViDoRe v1 task. + """ + print(f"\n{'=' * 80}") + print(f"Evaluating task: {task_name}") + print(f"{'=' * 80}") + + # Normalize task name (handle aliases) + task_name = normalize_task_name(task_name) + + # Get task config + if task_name not in VIDORE_V1_TASKS: + raise ValueError(f"Unknown task: {task_name}. Available: {list(VIDORE_V1_TASKS.keys())}") + + task_config = VIDORE_V1_TASKS[task_name] + dataset_path = str(task_config["dataset_path"]) + revision = str(task_config["revision"]) + + # Load data + corpus, queries, qrels = load_vidore_v1_data( + dataset_path=dataset_path, + revision=revision, + split="test", + ) + + # Initialize k_values if not provided + if k_values is None: + k_values = [1, 3, 5, 10, 20, 100, 1000] + + # Check if we have any queries + if len(queries) == 0: + print(f"\nWarning: No queries found for task {task_name}. Skipping evaluation.") + # Return zero scores + scores = {} + for k in k_values: + scores[f"ndcg_at_{k}"] = 0.0 + scores[f"map_at_{k}"] = 0.0 + scores[f"recall_at_{k}"] = 0.0 + scores[f"precision_at_{k}"] = 0.0 + scores[f"mrr_at_{k}"] = 0.0 + return scores + + # Initialize evaluator + evaluator = ViDoReBenchmarkEvaluator( + model_name=model_name, + use_fast_plaid=use_fast_plaid, + top_k=top_k, + first_stage_k=first_stage_k, + k_values=k_values, + ) + + # Build or load index + # Use safe model name for index path (different models need different indexes) + safe_model_name = get_safe_model_name(model_name) + index_path_full = index_path if not use_fast_plaid else fast_plaid_index_path + if index_path_full is None: + index_path_full = f"./indexes/{task_name}_{safe_model_name}" + if use_fast_plaid: + index_path_full = f"./indexes/{task_name}_{safe_model_name}_fastplaid" + + index_or_retriever, corpus_ids_ordered = evaluator.build_index_from_corpus( + corpus=corpus, + index_path=index_path_full, + rebuild=rebuild_index, + ) + + # Search queries + task_prompt = cast(Optional[dict[str, str]], task_config.get("prompt")) + results = evaluator.search_queries( + queries=queries, + corpus_ids=corpus_ids_ordered, + index_or_retriever=index_or_retriever, + fast_plaid_index_path=fast_plaid_index_path, + task_prompt=task_prompt, + ) + + # Evaluate + scores = evaluator.evaluate_results(results, qrels, k_values=k_values) + + # Print results + print(f"\n{'=' * 80}") + print(f"Results for {task_name}:") + print(f"{'=' * 80}") + for metric, value in scores.items(): + if isinstance(value, (int, float)): + print(f" {metric}: {value:.5f}") + + # Save results + if output_dir: + os.makedirs(output_dir, exist_ok=True) + results_file = os.path.join(output_dir, f"{task_name}_results.json") + scores_file = os.path.join(output_dir, f"{task_name}_scores.json") + + with open(results_file, "w") as f: + json.dump(results, f, indent=2) + print(f"\nSaved results to: {results_file}") + + with open(scores_file, "w") as f: + json.dump(scores, f, indent=2) + print(f"Saved scores to: {scores_file}") + + return scores + + +def main(): + parser = argparse.ArgumentParser( + description="Evaluate ViDoRe v1 benchmark using LEANN/Fast-Plaid indexing" + ) + parser.add_argument( + "--model", + type=str, + default="colqwen2", + help="Model to use: 'colqwen2', 'colpali', or path to a model directory (supports LoRA adapters)", + ) + parser.add_argument( + "--task", + type=str, + default=None, + help="Specific task to evaluate (or 'all' for all tasks)", + ) + parser.add_argument( + "--tasks", + type=str, + default="all", + help="Tasks to evaluate: 'all' or comma-separated list", + ) + parser.add_argument( + "--index-path", + type=str, + default=None, + help="Path to LEANN index (auto-generated if not provided)", + ) + parser.add_argument( + "--use-fast-plaid", + action="store_true", + help="Use Fast-Plaid instead of LEANN", + ) + parser.add_argument( + "--fast-plaid-index-path", + type=str, + default=None, + help="Path to Fast-Plaid index (auto-generated if not provided)", + ) + parser.add_argument( + "--rebuild-index", + action="store_true", + help="Rebuild index even if it exists", + ) + parser.add_argument( + "--top-k", + type=int, + default=1000, + help="Top-k results to retrieve (MTEB default is max(k_values)=1000)", + ) + parser.add_argument( + "--first-stage-k", + type=int, + default=500, + help="First stage k for LEANN search", + ) + parser.add_argument( + "--k-values", + type=str, + default="1,3,5,10,20,100,1000", + help="Comma-separated k values for evaluation (e.g., '1,3,5,10,100')", + ) + parser.add_argument( + "--output-dir", + type=str, + default="./vidore_v1_results", + help="Output directory for results", + ) + + args = parser.parse_args() + + # Parse k_values + k_values = [int(k.strip()) for k in args.k_values.split(",")] + + # Determine tasks to evaluate + if args.task: + tasks_to_eval = [normalize_task_name(args.task)] + elif args.tasks.lower() == "all": + tasks_to_eval = list(VIDORE_V1_TASKS.keys()) + else: + tasks_to_eval = [normalize_task_name(t.strip()) for t in args.tasks.split(",")] + + print(f"Tasks to evaluate: {tasks_to_eval}") + + # Evaluate each task + all_scores = {} + for task_name in tasks_to_eval: + try: + scores = evaluate_task( + task_name=task_name, + model_name=args.model, + index_path=args.index_path, + use_fast_plaid=args.use_fast_plaid, + fast_plaid_index_path=args.fast_plaid_index_path, + rebuild_index=args.rebuild_index, + top_k=args.top_k, + first_stage_k=args.first_stage_k, + k_values=k_values, + output_dir=args.output_dir, + ) + all_scores[task_name] = scores + except Exception as e: + print(f"\nError evaluating {task_name}: {e}") + import traceback + + traceback.print_exc() + continue + + # Print summary + if all_scores: + print(f"\n{'=' * 80}") + print("SUMMARY") + print(f"{'=' * 80}") + for task_name, scores in all_scores.items(): + print(f"\n{task_name}:") + # Print main metrics + for metric in ["ndcg_at_5", "ndcg_at_10", "ndcg_at_100", "map_at_10", "recall_at_10"]: + if metric in scores: + print(f" {metric}: {scores[metric]:.5f}") + + +if __name__ == "__main__": + main() diff --git a/apps/multimodal/vision-based-pdf-multi-vector/vidore_v2_benchmark.py b/apps/multimodal/vision-based-pdf-multi-vector/vidore_v2_benchmark.py new file mode 100644 index 0000000..be4eb4f --- /dev/null +++ b/apps/multimodal/vision-based-pdf-multi-vector/vidore_v2_benchmark.py @@ -0,0 +1,443 @@ +#!/usr/bin/env python3 +""" +Modular script to reproduce NDCG results for ViDoRe v2 benchmark. + +This script uses the interface from leann_multi_vector.py to: +1. Download ViDoRe v2 datasets +2. Build indexes (LEANN or Fast-Plaid) +3. Perform retrieval +4. Evaluate using NDCG metrics + +Usage: + # Evaluate all ViDoRe v2 tasks + python vidore_v2_benchmark.py --model colqwen2 --tasks all + + # Evaluate specific task + python vidore_v2_benchmark.py --model colqwen2 --task Vidore2ESGReportsRetrieval + + # Use Fast-Plaid index + python vidore_v2_benchmark.py --model colqwen2 --use-fast-plaid --fast-plaid-index-path ./indexes/vidore_fastplaid + + # Rebuild index + python vidore_v2_benchmark.py --model colqwen2 --rebuild-index +""" + +import argparse +import json +import os +from typing import Any, Optional, cast + +from datasets import Dataset, load_dataset +from leann_multi_vector import ( + ViDoReBenchmarkEvaluator, + _ensure_repo_paths_importable, +) + +_ensure_repo_paths_importable(__file__) + +# Language name to dataset language field value mapping +# Dataset uses ISO 639-3 + ISO 15924 format (e.g., "eng-Latn") +LANGUAGE_MAPPING = { + "english": "eng-Latn", + "french": "fra-Latn", + "spanish": "spa-Latn", + "german": "deu-Latn", +} + +# ViDoRe v2 task configurations +# Prompts match MTEB task metadata prompts +VIDORE_V2_TASKS = { + "Vidore2ESGReportsRetrieval": { + "dataset_path": "vidore/esg_reports_v2", + "revision": "0542c0d03da0ec1c8cbc517c8d78e7e95c75d3d3", + "languages": ["french", "spanish", "english", "german"], + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "Vidore2EconomicsReportsRetrieval": { + "dataset_path": "vidore/economics_reports_v2", + "revision": "b3e3a04b07fbbaffe79be49dabf92f691fbca252", + "languages": ["french", "spanish", "english", "german"], + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "Vidore2BioMedicalLecturesRetrieval": { + "dataset_path": "vidore/biomedical_lectures_v2", + "revision": "a29202f0da409034d651614d87cd8938d254e2ea", + "languages": ["french", "spanish", "english", "german"], + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, + "Vidore2ESGReportsHLRetrieval": { + "dataset_path": "vidore/esg_reports_human_labeled_v2", + "revision": "6d467dedb09a75144ede1421747e47cf036857dd", + # Note: This dataset doesn't have language filtering - all queries are English + "languages": None, # No language filtering needed + "prompt": {"query": "Find a screenshot that relevant to the user's question."}, + }, +} + + +def load_vidore_v2_data( + dataset_path: str, + revision: Optional[str] = None, + split: str = "test", + language: Optional[str] = None, +): + """ + Load ViDoRe v2 dataset. + + Returns: + corpus: dict mapping corpus_id to PIL Image + queries: dict mapping query_id to query text + qrels: dict mapping query_id to dict of {corpus_id: relevance_score} + """ + print(f"Loading dataset: {dataset_path} (split={split}, language={language})") + + # Load queries - cast to Dataset since we know split returns Dataset not DatasetDict + query_ds = cast(Dataset, load_dataset(dataset_path, "queries", split=split, revision=revision)) + + # Check if dataset has language field before filtering + has_language_field = len(query_ds) > 0 and "language" in query_ds.column_names + + if language and has_language_field: + # Map language name to dataset language field value (e.g., "english" -> "eng-Latn") + dataset_language = LANGUAGE_MAPPING.get(language, language) + query_ds_filtered = query_ds.filter(lambda x: x.get("language") == dataset_language) + # Check if filtering resulted in empty dataset + if len(query_ds_filtered) == 0: + print( + f"Warning: No queries found after filtering by language '{language}' (mapped to '{dataset_language}')." + ) + # Try with original language value (dataset might use simple names like 'english') + print(f"Trying with original language value '{language}'...") + query_ds_filtered = query_ds.filter(lambda x: x.get("language") == language) + if len(query_ds_filtered) == 0: + # Try to get a sample to see actual language values + try: + sample_ds = cast( + Dataset, + load_dataset(dataset_path, "queries", split=split, revision=revision), + ) + if len(sample_ds) > 0 and "language" in sample_ds.column_names: + sample_langs = set(sample_ds["language"]) + print(f"Available language values in dataset: {sample_langs}") + except Exception: + pass + else: + print( + f"Found {len(query_ds_filtered)} queries using original language value '{language}'" + ) + query_ds = query_ds_filtered + + queries: dict[str, str] = {} + for row in query_ds: + row_dict = cast(dict[str, Any], row) + query_id = f"query-{split}-{row_dict['query-id']}" + queries[query_id] = row_dict["query"] + + # Load corpus (images) - cast to Dataset + corpus_ds = cast(Dataset, load_dataset(dataset_path, "corpus", split=split, revision=revision)) + + corpus: dict[str, Any] = {} + for row in corpus_ds: + row_dict = cast(dict[str, Any], row) + corpus_id = f"corpus-{split}-{row_dict['corpus-id']}" + # Extract image from the dataset row + if "image" in row_dict: + corpus[corpus_id] = row_dict["image"] + elif "page_image" in row_dict: + corpus[corpus_id] = row_dict["page_image"] + else: + raise ValueError( + f"No image field found in corpus. Available fields: {list(row_dict.keys())}" + ) + + # Load qrels (relevance judgments) - cast to Dataset + qrels_ds = cast(Dataset, load_dataset(dataset_path, "qrels", split=split, revision=revision)) + + qrels: dict[str, dict[str, int]] = {} + for row in qrels_ds: + row_dict = cast(dict[str, Any], row) + query_id = f"query-{split}-{row_dict['query-id']}" + corpus_id = f"corpus-{split}-{row_dict['corpus-id']}" + if query_id not in qrels: + qrels[query_id] = {} + qrels[query_id][corpus_id] = int(row_dict["score"]) + + print( + f"Loaded {len(queries)} queries, {len(corpus)} corpus items, {len(qrels)} query-relevance mappings" + ) + + # Filter qrels to only include queries that exist + qrels = {qid: rel_docs for qid, rel_docs in qrels.items() if qid in queries} + + # Filter out queries without any relevant documents (matching MTEB behavior) + # This is important for correct NDCG calculation + qrels_filtered = {qid: rel_docs for qid, rel_docs in qrels.items() if len(rel_docs) > 0} + queries_filtered = { + qid: query_text for qid, query_text in queries.items() if qid in qrels_filtered + } + + print( + f"After filtering queries without positives: {len(queries_filtered)} queries, {len(qrels_filtered)} query-relevance mappings" + ) + + return corpus, queries_filtered, qrels_filtered + + +def evaluate_task( + task_name: str, + model_name: str, + index_path: str, + use_fast_plaid: bool = False, + fast_plaid_index_path: Optional[str] = None, + language: Optional[str] = None, + rebuild_index: bool = False, + top_k: int = 100, + first_stage_k: int = 500, + k_values: Optional[list[int]] = None, + output_dir: Optional[str] = None, +): + """ + Evaluate a single ViDoRe v2 task. + """ + print(f"\n{'=' * 80}") + print(f"Evaluating task: {task_name}") + print(f"{'=' * 80}") + + # Get task config + if task_name not in VIDORE_V2_TASKS: + raise ValueError(f"Unknown task: {task_name}. Available: {list(VIDORE_V2_TASKS.keys())}") + + task_config = VIDORE_V2_TASKS[task_name] + dataset_path = str(task_config["dataset_path"]) + revision = str(task_config["revision"]) + + # Determine language + if language is None: + # Use first language if multiple available + languages = cast(Optional[list[str]], task_config.get("languages")) + if languages is None: + # Task doesn't support language filtering (e.g., Vidore2ESGReportsHLRetrieval) + language = None + elif len(languages) == 1: + language = languages[0] + else: + language = None + + # Initialize k_values if not provided + if k_values is None: + k_values = [1, 3, 5, 10, 100] + + # Load data + corpus, queries, qrels = load_vidore_v2_data( + dataset_path=dataset_path, + revision=revision, + split="test", + language=language, + ) + + # Check if we have any queries + if len(queries) == 0: + print( + f"\nWarning: No queries found for task {task_name} with language {language}. Skipping evaluation." + ) + # Return zero scores + scores = {} + for k in k_values: + scores[f"ndcg_at_{k}"] = 0.0 + scores[f"map_at_{k}"] = 0.0 + scores[f"recall_at_{k}"] = 0.0 + scores[f"precision_at_{k}"] = 0.0 + scores[f"mrr_at_{k}"] = 0.0 + return scores + + # Initialize evaluator + evaluator = ViDoReBenchmarkEvaluator( + model_name=model_name, + use_fast_plaid=use_fast_plaid, + top_k=top_k, + first_stage_k=first_stage_k, + k_values=k_values, + ) + + # Build or load index + index_path_full = index_path if not use_fast_plaid else fast_plaid_index_path + if index_path_full is None: + index_path_full = f"./indexes/{task_name}_{model_name}" + if use_fast_plaid: + index_path_full = f"./indexes/{task_name}_{model_name}_fastplaid" + + index_or_retriever, corpus_ids_ordered = evaluator.build_index_from_corpus( + corpus=corpus, + index_path=index_path_full, + rebuild=rebuild_index, + ) + + # Search queries + task_prompt = cast(Optional[dict[str, str]], task_config.get("prompt")) + results = evaluator.search_queries( + queries=queries, + corpus_ids=corpus_ids_ordered, + index_or_retriever=index_or_retriever, + fast_plaid_index_path=fast_plaid_index_path, + task_prompt=task_prompt, + ) + + # Evaluate + scores = evaluator.evaluate_results(results, qrels, k_values=k_values) + + # Print results + print(f"\n{'=' * 80}") + print(f"Results for {task_name}:") + print(f"{'=' * 80}") + for metric, value in scores.items(): + if isinstance(value, (int, float)): + print(f" {metric}: {value:.5f}") + + # Save results + if output_dir: + os.makedirs(output_dir, exist_ok=True) + results_file = os.path.join(output_dir, f"{task_name}_results.json") + scores_file = os.path.join(output_dir, f"{task_name}_scores.json") + + with open(results_file, "w") as f: + json.dump(results, f, indent=2) + print(f"\nSaved results to: {results_file}") + + with open(scores_file, "w") as f: + json.dump(scores, f, indent=2) + print(f"Saved scores to: {scores_file}") + + return scores + + +def main(): + parser = argparse.ArgumentParser( + description="Evaluate ViDoRe v2 benchmark using LEANN/Fast-Plaid indexing" + ) + parser.add_argument( + "--model", + type=str, + default="colqwen2", + choices=["colqwen2", "colpali"], + help="Model to use", + ) + parser.add_argument( + "--task", + type=str, + default=None, + help="Specific task to evaluate (or 'all' for all tasks)", + ) + parser.add_argument( + "--tasks", + type=str, + default="all", + help="Tasks to evaluate: 'all' or comma-separated list", + ) + parser.add_argument( + "--index-path", + type=str, + default=None, + help="Path to LEANN index (auto-generated if not provided)", + ) + parser.add_argument( + "--use-fast-plaid", + action="store_true", + help="Use Fast-Plaid instead of LEANN", + ) + parser.add_argument( + "--fast-plaid-index-path", + type=str, + default=None, + help="Path to Fast-Plaid index (auto-generated if not provided)", + ) + parser.add_argument( + "--rebuild-index", + action="store_true", + help="Rebuild index even if it exists", + ) + parser.add_argument( + "--language", + type=str, + default=None, + help="Language to evaluate (default: first available)", + ) + parser.add_argument( + "--top-k", + type=int, + default=100, + help="Top-k results to retrieve", + ) + parser.add_argument( + "--first-stage-k", + type=int, + default=500, + help="First stage k for LEANN search", + ) + parser.add_argument( + "--k-values", + type=str, + default="1,3,5,10,100", + help="Comma-separated k values for evaluation (e.g., '1,3,5,10,100')", + ) + parser.add_argument( + "--output-dir", + type=str, + default="./vidore_v2_results", + help="Output directory for results", + ) + + args = parser.parse_args() + + # Parse k_values + k_values = [int(k.strip()) for k in args.k_values.split(",")] + + # Determine tasks to evaluate + if args.task: + tasks_to_eval = [args.task] + elif args.tasks.lower() == "all": + tasks_to_eval = list(VIDORE_V2_TASKS.keys()) + else: + tasks_to_eval = [t.strip() for t in args.tasks.split(",")] + + print(f"Tasks to evaluate: {tasks_to_eval}") + + # Evaluate each task + all_scores = {} + for task_name in tasks_to_eval: + try: + scores = evaluate_task( + task_name=task_name, + model_name=args.model, + index_path=args.index_path, + use_fast_plaid=args.use_fast_plaid, + fast_plaid_index_path=args.fast_plaid_index_path, + language=args.language, + rebuild_index=args.rebuild_index, + top_k=args.top_k, + first_stage_k=args.first_stage_k, + k_values=k_values, + output_dir=args.output_dir, + ) + all_scores[task_name] = scores + except Exception as e: + print(f"\nError evaluating {task_name}: {e}") + import traceback + + traceback.print_exc() + continue + + # Print summary + if all_scores: + print(f"\n{'=' * 80}") + print("SUMMARY") + print(f"{'=' * 80}") + for task_name, scores in all_scores.items(): + print(f"\n{task_name}:") + # Print main metrics + for metric in ["ndcg_at_5", "ndcg_at_10", "ndcg_at_100", "map_at_10", "recall_at_10"]: + if metric in scores: + print(f" {metric}: {scores[metric]:.5f}") + + +if __name__ == "__main__": + main() diff --git a/apps/qwen_data/__init__.py b/apps/qwen_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/qwen_data/qwen_reader.py b/apps/qwen_data/qwen_reader.py new file mode 100644 index 0000000..865121d --- /dev/null +++ b/apps/qwen_data/qwen_reader.py @@ -0,0 +1,150 @@ +import json +import logging +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + + +class QwenReader: + """Reader for Qwen Code CLI history files.""" + + def __init__(self): + pass + + def load_data(self, history_dir: str, max_count: int = -1) -> list[dict[str, Any]]: + """ + Load data from Qwen Code history directory. + + Args: + history_dir: Path to .qwen-code directory + max_count: Max number of conversations to load + + Returns: + List of dictionaries with 'text' and 'metadata' keys + """ + history_path = Path(history_dir).expanduser() + if not history_path.exists(): + print(f"Qwen history directory not found: {history_path}") + return [] + + documents = [] + + # 1. Load Memory (QWEN.md or MEMORY.md) + for memory_filename in ["QWEN.md", "MEMORY.md"]: + memory_file = history_path / memory_filename + if memory_file.exists(): + try: + text = memory_file.read_text(encoding="utf-8") + if text.strip(): + documents.append( + { + "text": f"Qwen Code Memory:\n{text}", + "metadata": {"source": str(memory_file), "type": "memory"}, + } + ) + except Exception as e: + print(f"Error reading memory file {memory_filename}: {e}") + + # 2. Find Session Files + # Legacy JSON sessions + session_files = list(history_path.glob("session-*.json")) + # New JSONL sessions + session_files.extend(list(history_path.glob("session-*.jsonl"))) + # Checkpoints + session_files.extend(list(history_path.glob("checkpoint-*.json"))) + + # Sort by modification time (newest first) + session_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) + + print(f"Found {len(session_files)} session files.") + + count = 0 + for file_path in session_files: + if max_count > 0 and count >= max_count: + break + + try: + content = "" + if file_path.suffix == ".jsonl": + content = self._parse_jsonl_session(file_path) + elif file_path.suffix == ".json": + content = self._parse_json_session(file_path) + + if content: + documents.append( + { + "text": content, + "metadata": { + "source": str(file_path), + "type": "session", + "filename": file_path.name, + }, + } + ) + count += 1 + except Exception as e: + print(f"Error reading {file_path.name}: {e}") + + print(f"Successfully loaded {len(documents)} items from Qwen history.") + return documents + + def _parse_json_session(self, file_path: Path) -> str: + """Parse legacy JSON session file.""" + data = json.loads(file_path.read_text(encoding="utf-8")) + + # Handle dict format (standard session) + messages = [] + if isinstance(data, dict): + # Check for 'messages' key (standard format) + if "messages" in data: + for msg in data["messages"]: + role = msg.get("role", "unknown") + content = msg.get("content", "") + if content: + messages.append(f"{role.upper()}: {content}") + # Check for 'parts' key (checkpoint format sometimes) + elif "parts" in data: + messages.append(f"Saved Session Content: {data['parts']}") + + # Handle list format (some older array-based sessions) + elif isinstance(data, list): + for item in data: + if isinstance(item, dict): + role = item.get("role", "unknown") + content = item.get("content", "") or item.get("parts", "") + if content: + messages.append(f"{role.upper()}: {content}") + + if not messages: + return "" + + return f"File: {file_path.name}\n\n" + "\n\n".join(messages) + + def _parse_jsonl_session(self, file_path: Path) -> str: + """Parse JSONL session file.""" + messages = [] + try: + with open(file_path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + data = json.loads(line) + # Skip metadata lines if they don't have content + if "role" in data and "content" in data: + messages.append(f"{data['role'].upper()}: {data['content']}") + elif "parts" in data: # sometimes parts is used + messages.append( + f"{data.get('role', 'unknown').upper()}: {data['parts']}" + ) + except json.JSONDecodeError: + continue + except Exception: + return "" + + if not messages: + return "" + + return f"File: {file_path.name}\n\n" + "\n\n".join(messages) diff --git a/apps/qwen_rag.py b/apps/qwen_rag.py new file mode 100644 index 0000000..ae665be --- /dev/null +++ b/apps/qwen_rag.py @@ -0,0 +1,69 @@ +""" +Qwen Code RAG example. +Indexes and searches Qwen Code CLI history (~/.qwen-code). +""" + +import sys +from pathlib import Path +from typing import Any + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from base_rag_example import BaseRAGExample +from chunking import create_text_chunks + +from .qwen_data.qwen_reader import QwenReader + + +class QwenRAG(BaseRAGExample): + """RAG example for Qwen Code CLI history.""" + + def __init__(self): + super().__init__( + name="Qwen Code", + description="Process and query Qwen Code CLI history with LEANN", + default_index_name="qwen_index", + ) + + def _add_specific_arguments(self, parser): + """Add Qwen-specific arguments.""" + group = parser.add_argument_group("Qwen Parameters") + group.add_argument( + "--qwen-path", + type=str, + default="~/.qwen-code", + help="Path to .qwen-code directory (default: ~/.qwen-code)", + ) + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load Qwen history and convert to text chunks.""" + print(f"Loading Qwen history from: {args.qwen_path}") + + reader = QwenReader() + documents = reader.load_data(history_dir=args.qwen_path, max_count=args.max_items) + + if not documents: + print("No documents found! Check if ~/.qwen-code exists and has history.") + return [] + + # Convert dicts to Document objects for chunking + from llama_index.core import Document + + docs = [Document(text=d["text"], metadata=d["metadata"]) for d in documents] + + # Convert to text chunks + print(f"splitting {len(documents)} documents into chunks...") + chunks = create_text_chunks(docs) + + return chunks + + +if __name__ == "__main__": + import asyncio + + print("\n✨ Qwen Code RAG") + print("=" * 50) + + rag = QwenRAG() + asyncio.run(rag.run()) diff --git a/apps/semantic_file_search/leann-plus-temporal-search.py b/apps/semantic_file_search/leann-plus-temporal-search.py new file mode 100644 index 0000000..167189e --- /dev/null +++ b/apps/semantic_file_search/leann-plus-temporal-search.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +import re +import sys +from datetime import datetime, timedelta +from pathlib import Path + +from leann import LeannSearcher + +INDEX_PATH = str(Path("./").resolve() / "demo.leann") + + +class TimeParser: + def __init__(self): + # Main pattern: captures optional fuzzy modifier, number, unit, and optional "ago" + self.pattern = r"(?:(around|about|roughly|approximately)\s+)?(\d+)\s+(hour|day|week|month|year)s?(?:\s+ago)?" + + # Compile for performance + self.regex = re.compile(self.pattern, re.IGNORECASE) + + # Stop words to remove before regex parsing + self.stop_words = { + "in", + "at", + "of", + "by", + "as", + "me", + "the", + "a", + "an", + "and", + "any", + "find", + "search", + "list", + "ago", + "back", + "past", + "earlier", + } + + def clean_text(self, text): + """Remove stop words from text""" + words = text.split() + cleaned = " ".join(word for word in words if word.lower() not in self.stop_words) + return cleaned + + def parse(self, text): + """Extract all time expressions from text""" + # Clean text first + cleaned_text = self.clean_text(text) + + matches = [] + for match in self.regex.finditer(cleaned_text): + fuzzy = match.group(1) # "around", "about", etc. + number = int(match.group(2)) + unit = match.group(3).lower() + + matches.append( + { + "full_match": match.group(0), + "fuzzy": bool(fuzzy), + "number": number, + "unit": unit, + "range": self.calculate_range(number, unit, bool(fuzzy)), + } + ) + + return matches + + def calculate_range(self, number, unit, is_fuzzy): + """Convert to actual datetime range and return ISO format strings""" + units = { + "hour": timedelta(hours=number), + "day": timedelta(days=number), + "week": timedelta(weeks=number), + "month": timedelta(days=number * 30), + "year": timedelta(days=number * 365), + } + + delta = units[unit] + now = datetime.now() + target = now - delta + + if is_fuzzy: + buffer = delta * 0.2 # 20% buffer for fuzzy + start = (target - buffer).isoformat() + end = (target + buffer).isoformat() + else: + start = target.isoformat() + end = now.isoformat() + + return (start, end) + + +def search_files(query, top_k=15): + """Search the index and return results""" + # Parse time expressions + parser = TimeParser() + time_matches = parser.parse(query) + + # Remove time expressions from query for semantic search + clean_query = query + if time_matches: + for match in time_matches: + clean_query = clean_query.replace(match["full_match"], "").strip() + + # Check if clean_query is less than 4 characters + if len(clean_query) < 4: + print("Error: add more input for accurate results.") + return + + # Single query to vector DB + searcher = LeannSearcher(INDEX_PATH) + results = searcher.search( + clean_query if clean_query else query, top_k=top_k, recompute_embeddings=False + ) + + # Filter by time if time expression found + if time_matches: + time_range = time_matches[0]["range"] # Use first time expression + start_time, end_time = time_range + + filtered_results = [] + for result in results: + # Access metadata attribute directly (not .get()) + metadata = result.metadata if hasattr(result, "metadata") else {} + + if metadata: + # Check modification date first, fall back to creation date + date_str = metadata.get("modification_date") or metadata.get("creation_date") + + if date_str: + # Convert strings to datetime objects for proper comparison + try: + file_date = datetime.fromisoformat(date_str) + start_dt = datetime.fromisoformat(start_time) + end_dt = datetime.fromisoformat(end_time) + + # Compare dates properly + if start_dt <= file_date <= end_dt: + filtered_results.append(result) + except (ValueError, TypeError): + # Handle invalid date formats + print(f"Warning: Invalid date format in metadata: {date_str}") + continue + + results = filtered_results + + # Print results + print(f"\nSearch results for: '{query}'") + if time_matches: + print( + f"Time filter: {time_matches[0]['number']} {time_matches[0]['unit']}(s) {'(fuzzy)' if time_matches[0]['fuzzy'] else ''}" + ) + print( + f"Date range: {time_matches[0]['range'][0][:10]} to {time_matches[0]['range'][1][:10]}" + ) + print("-" * 80) + + for i, result in enumerate(results, 1): + print(f"\n[{i}] Score: {result.score:.4f}") + print(f"Content: {result.text}") + + # Show metadata if present + metadata = result.metadata if hasattr(result, "metadata") else None + if metadata: + if "creation_date" in metadata: + print(f"Created: {metadata['creation_date']}") + if "modification_date" in metadata: + print(f"Modified: {metadata['modification_date']}") + print("-" * 80) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print('Usage: python search_index.py "" [top_k]') + sys.exit(1) + + query = sys.argv[1] + top_k = int(sys.argv[2]) if len(sys.argv) > 2 else 15 + + search_files(query, top_k) diff --git a/apps/semantic_file_search/leann_index_builder.py b/apps/semantic_file_search/leann_index_builder.py new file mode 100644 index 0000000..958c697 --- /dev/null +++ b/apps/semantic_file_search/leann_index_builder.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +import json +import sys +from pathlib import Path + +from leann import LeannBuilder + + +def process_json_items(json_file_path): + """Load and process JSON file with metadata items""" + + with open(json_file_path, encoding="utf-8") as f: + items = json.load(f) + + # Guard against empty JSON + if not items: + print("⚠️ No items found in the JSON file. Exiting gracefully.") + return + + INDEX_PATH = str(Path("./").resolve() / "demo.leann") + builder = LeannBuilder(backend_name="hnsw", is_recompute=False) + + total_items = len(items) + items_added = 0 + print(f"Processing {total_items} items...") + + for idx, item in enumerate(items): + try: + # Create embedding text sentence + embedding_text = f"{item.get('Name', 'unknown')} located at {item.get('Path', 'unknown')} and size {item.get('Size', 'unknown')} bytes with content type {item.get('ContentType', 'unknown')} and kind {item.get('Kind', 'unknown')}" + + # Prepare metadata with dates + metadata = {} + if "CreationDate" in item: + metadata["creation_date"] = item["CreationDate"] + if "ContentChangeDate" in item: + metadata["modification_date"] = item["ContentChangeDate"] + + # Add to builder + builder.add_text(embedding_text, metadata=metadata) + items_added += 1 + + except Exception as e: + print(f"\n⚠️ Warning: Failed to process item {idx}: {e}") + continue + + # Show progress + progress = (idx + 1) / total_items * 100 + sys.stdout.write(f"\rProgress: {idx + 1}/{total_items} ({progress:.1f}%)") + sys.stdout.flush() + + print() # New line after progress + + # Guard against no successfully added items + if items_added == 0: + print("⚠️ No items were successfully added to the index. Exiting gracefully.") + return + + print(f"\nβœ… Successfully processed {items_added}/{total_items} items") + print("Building index...") + + try: + builder.build_index(INDEX_PATH) + print(f"βœ“ Index saved to {INDEX_PATH}") + except ValueError as e: + if "No chunks added" in str(e): + print("⚠️ No chunks were added to the builder. Index not created.") + else: + raise + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python build_index.py ") + sys.exit(1) + + json_file = sys.argv[1] + if not Path(json_file).exists(): + print(f"Error: File {json_file} not found") + sys.exit(1) + + process_json_items(json_file) diff --git a/apps/semantic_file_search/spotlight_index_dump.py b/apps/semantic_file_search/spotlight_index_dump.py new file mode 100644 index 0000000..84a1ff6 --- /dev/null +++ b/apps/semantic_file_search/spotlight_index_dump.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +""" +Spotlight Metadata Dumper for Vector DB +Extracts only essential metadata for semantic search embeddings +Output is optimized for vector database storage with minimal fields +""" + +import json +import sys +from datetime import datetime + +# Check platform before importing macOS-specific modules +if sys.platform != "darwin": + print("This script requires macOS (uses Spotlight)") + sys.exit(1) + +from Foundation import NSDate, NSMetadataQuery, NSPredicate, NSRunLoop + +# EDIT THIS LIST: Add or remove folders to search +# Can be either: +# - Folder names relative to home directory (e.g., "Desktop", "Downloads") +# - Absolute paths (e.g., "/Applications", "/System/Library") +SEARCH_FOLDERS = [ + "Desktop", + "Downloads", + "Documents", + "Music", + "Pictures", + "Movies", + # "Library", # Uncomment to include + # "/Applications", # Absolute path example + # "Code/Projects", # Subfolder example + # Add any other folders here +] + + +def convert_to_serializable(obj): + """Convert NS objects to Python serializable types""" + if obj is None: + return None + + # Handle NSDate + if hasattr(obj, "timeIntervalSince1970"): + return datetime.fromtimestamp(obj.timeIntervalSince1970()).isoformat() + + # Handle NSArray + if hasattr(obj, "count") and hasattr(obj, "objectAtIndex_"): + return [convert_to_serializable(obj.objectAtIndex_(i)) for i in range(obj.count())] + + # Convert to string + try: + return str(obj) + except Exception: + return repr(obj) + + +def dump_spotlight_data(max_items=10, output_file="spotlight_dump.json"): + """ + Dump Spotlight data using public.item predicate + """ + # Build full paths from SEARCH_FOLDERS + import os + + home_dir = os.path.expanduser("~") + search_paths = [] + + print("Search locations:") + for folder in SEARCH_FOLDERS: + # Check if it's an absolute path or relative + if folder.startswith("/"): + full_path = folder + else: + full_path = os.path.join(home_dir, folder) + + if os.path.exists(full_path): + search_paths.append(full_path) + print(f" βœ“ {full_path}") + else: + print(f" βœ— {full_path} (not found)") + + if not search_paths: + print("No valid search paths found!") + return [] + + print(f"\nDumping {max_items} items from Spotlight (public.item)...") + + # Create query with public.item predicate + query = NSMetadataQuery.alloc().init() + predicate = NSPredicate.predicateWithFormat_("kMDItemContentTypeTree CONTAINS 'public.item'") + query.setPredicate_(predicate) + + # Set search scopes to our specific folders + query.setSearchScopes_(search_paths) + + print("Starting query...") + query.startQuery() + + # Wait for gathering to complete + run_loop = NSRunLoop.currentRunLoop() + print("Gathering results...") + + # Let it gather for a few seconds + for i in range(50): # 5 seconds max + run_loop.runMode_beforeDate_( + "NSDefaultRunLoopMode", NSDate.dateWithTimeIntervalSinceNow_(0.1) + ) + # Check gathering status periodically + if i % 10 == 0: + current_count = query.resultCount() + if current_count > 0: + print(f" Found {current_count} items so far...") + + # Continue while still gathering (up to 2 more seconds) + timeout = NSDate.dateWithTimeIntervalSinceNow_(2.0) + while query.isGathering() and timeout.timeIntervalSinceNow() > 0: + run_loop.runMode_beforeDate_( + "NSDefaultRunLoopMode", NSDate.dateWithTimeIntervalSinceNow_(0.1) + ) + + query.stopQuery() + + total_results = query.resultCount() + print(f"Found {total_results} total items") + + if total_results == 0: + print("No results found") + return [] + + # Process items + items_to_process = min(total_results, max_items) + results = [] + + # ONLY relevant attributes for vector embeddings + # These provide essential context for semantic search without bloat + attributes = [ + "kMDItemPath", # Full path for file retrieval + "kMDItemFSName", # Filename for display & embedding + "kMDItemFSSize", # Size for filtering/ranking + "kMDItemContentType", # File type for categorization + "kMDItemKind", # Human-readable type for embedding + "kMDItemFSCreationDate", # Temporal context + "kMDItemFSContentChangeDate", # Recency for ranking + ] + + print(f"Processing {items_to_process} items...") + + for i in range(items_to_process): + try: + item = query.resultAtIndex_(i) + metadata = {} + + # Extract ONLY the relevant attributes + for attr in attributes: + try: + value = item.valueForAttribute_(attr) + if value is not None: + # Keep the attribute name clean (remove kMDItem prefix for cleaner JSON) + clean_key = attr.replace("kMDItem", "").replace("FS", "") + metadata[clean_key] = convert_to_serializable(value) + except (AttributeError, ValueError, TypeError): + continue + + # Only add if we have at least a path + if metadata.get("Path"): + results.append(metadata) + + except Exception as e: + print(f"Error processing item {i}: {e}") + continue + + # Save to JSON + with open(output_file, "w", encoding="utf-8") as f: + json.dump(results, f, indent=2, ensure_ascii=False) + + print(f"\nβœ“ Saved {len(results)} items to {output_file}") + + # Show summary + print("\nSample items:") + import os + + home_dir = os.path.expanduser("~") + + for i, item in enumerate(results[:3]): + print(f"\n[Item {i + 1}]") + print(f" Path: {item.get('Path', 'N/A')}") + print(f" Name: {item.get('Name', 'N/A')}") + print(f" Type: {item.get('ContentType', 'N/A')}") + print(f" Kind: {item.get('Kind', 'N/A')}") + + # Handle size properly + size = item.get("Size") + if size: + try: + size_int = int(size) + if size_int > 1024 * 1024: + print(f" Size: {size_int / (1024 * 1024):.2f} MB") + elif size_int > 1024: + print(f" Size: {size_int / 1024:.2f} KB") + else: + print(f" Size: {size_int} bytes") + except (ValueError, TypeError): + print(f" Size: {size}") + + # Show dates + if "CreationDate" in item: + print(f" Created: {item['CreationDate']}") + if "ContentChangeDate" in item: + print(f" Modified: {item['ContentChangeDate']}") + + # Count by type + type_counts = {} + for item in results: + content_type = item.get("ContentType", "unknown") + type_counts[content_type] = type_counts.get(content_type, 0) + 1 + + print(f"\nTotal items saved: {len(results)}") + + if type_counts: + print("\nTop content types:") + for ct, count in sorted(type_counts.items(), key=lambda x: x[1], reverse=True)[:5]: + print(f" {ct}: {count} items") + + # Count by folder + folder_counts = {} + for item in results: + path = item.get("Path", "") + for folder in SEARCH_FOLDERS: + # Build the full folder path + if folder.startswith("/"): + folder_path = folder + else: + folder_path = os.path.join(home_dir, folder) + + if path.startswith(folder_path): + folder_counts[folder] = folder_counts.get(folder, 0) + 1 + break + + if folder_counts: + print("\nItems by location:") + for folder, count in sorted(folder_counts.items(), key=lambda x: x[1], reverse=True): + print(f" {folder}: {count} items") + + return results + + +def main(): + # Parse arguments + if len(sys.argv) > 1: + try: + max_items = int(sys.argv[1]) + except ValueError: + print("Usage: python spot.py [number_of_items]") + print("Default: 10 items") + sys.exit(1) + else: + max_items = 10 + + output_file = sys.argv[2] if len(sys.argv) > 2 else "spotlight_dump.json" + + # Run dump + dump_spotlight_data(max_items=max_items, output_file=output_file) + + +if __name__ == "__main__": + main() diff --git a/apps/slack_data/__init__.py b/apps/slack_data/__init__.py new file mode 100644 index 0000000..2611c20 --- /dev/null +++ b/apps/slack_data/__init__.py @@ -0,0 +1 @@ +# Slack MCP data integration for LEANN diff --git a/apps/slack_data/slack_mcp_reader.py b/apps/slack_data/slack_mcp_reader.py new file mode 100644 index 0000000..e7ba4a2 --- /dev/null +++ b/apps/slack_data/slack_mcp_reader.py @@ -0,0 +1,519 @@ +#!/usr/bin/env python3 +""" +Slack MCP Reader for LEANN + +This module provides functionality to connect to Slack MCP servers and fetch message data +for indexing in LEANN. It supports various Slack MCP server implementations and provides +flexible message processing options. +""" + +import ast +import asyncio +import json +import logging +from typing import Any, Optional + +logger = logging.getLogger(__name__) + + +class SlackMCPReader: + """ + Reader for Slack data via MCP (Model Context Protocol) servers. + + This class connects to Slack MCP servers to fetch message data and convert it + into a format suitable for LEANN indexing. + """ + + def __init__( + self, + mcp_server_command: str, + workspace_name: Optional[str] = None, + concatenate_conversations: bool = True, + max_messages_per_conversation: int = 100, + max_retries: int = 5, + retry_delay: float = 2.0, + ): + """ + Initialize the Slack MCP Reader. + + Args: + mcp_server_command: Command to start the MCP server (e.g., 'slack-mcp-server') + workspace_name: Optional workspace name to filter messages + concatenate_conversations: Whether to group messages by channel/thread + max_messages_per_conversation: Maximum messages to include per conversation + max_retries: Maximum number of retries for failed operations + retry_delay: Initial delay between retries in seconds + """ + self.mcp_server_command = mcp_server_command + self.workspace_name = workspace_name + self.concatenate_conversations = concatenate_conversations + self.max_messages_per_conversation = max_messages_per_conversation + self.max_retries = max_retries + self.retry_delay = retry_delay + self.mcp_process: asyncio.subprocess.Process | None = None + + async def start_mcp_server(self): + """Start the MCP server process.""" + try: + self.mcp_process = await asyncio.create_subprocess_exec( + *self.mcp_server_command.split(), + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logger.info(f"Started MCP server: {self.mcp_server_command}") + except Exception as e: + logger.error(f"Failed to start MCP server: {e}") + raise + + async def stop_mcp_server(self): + """Stop the MCP server process.""" + if self.mcp_process: + self.mcp_process.terminate() + await self.mcp_process.wait() + logger.info("Stopped MCP server") + + async def send_mcp_request(self, request: dict[str, Any]) -> dict[str, Any]: + """Send a request to the MCP server and get response.""" + proc = self.mcp_process + if proc is None: + raise RuntimeError("MCP server not started") + if proc.stdin is None or proc.stdout is None: + raise RuntimeError("MCP server stdio not available") + + request_json = json.dumps(request) + "\n" + proc.stdin.write(request_json.encode()) + await proc.stdin.drain() + + response_line = await proc.stdout.readline() + if not response_line: + raise RuntimeError("No response from MCP server") + + return json.loads(response_line.decode().strip()) + + async def initialize_mcp_connection(self): + """Initialize the MCP connection.""" + init_request = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "leann-slack-reader", "version": "1.0.0"}, + }, + } + + response = await self.send_mcp_request(init_request) + if "error" in response: + raise RuntimeError(f"MCP initialization failed: {response['error']}") + + logger.info("MCP connection initialized successfully") + + async def list_available_tools(self) -> list[dict[str, Any]]: + """List available tools from the MCP server.""" + list_request = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}} + + response = await self.send_mcp_request(list_request) + if "error" in response: + raise RuntimeError(f"Failed to list tools: {response['error']}") + + return response.get("result", {}).get("tools", []) + + def _is_cache_sync_error(self, error: dict) -> bool: + """Check if the error is related to users cache not being ready.""" + if isinstance(error, dict): + message = error.get("message", "").lower() + return ( + "users cache is not ready" in message or "sync process is still running" in message + ) + return False + + async def _retry_with_backoff(self, func, *args, **kwargs): + """Retry a function with exponential backoff, especially for cache sync issues.""" + last_exception = None + + for attempt in range(self.max_retries + 1): + try: + return await func(*args, **kwargs) + except Exception as e: + last_exception = e + + # Check if this is a cache sync error + error_dict = {} + if hasattr(e, "args") and e.args and isinstance(e.args[0], dict): + error_dict = e.args[0] + elif "Failed to fetch messages" in str(e): + # Try to extract error from the exception message + import re + + match = re.search(r"'error':\s*(\{[^}]+\})", str(e)) + if match: + try: + error_dict = ast.literal_eval(match.group(1)) + except (ValueError, SyntaxError): + pass + else: + # Try alternative format + match = re.search(r"Failed to fetch messages:\s*(\{[^}]+\})", str(e)) + if match: + try: + error_dict = ast.literal_eval(match.group(1)) + except (ValueError, SyntaxError): + pass + + if self._is_cache_sync_error(error_dict): + if attempt < self.max_retries: + delay = self.retry_delay * (2**attempt) # Exponential backoff + logger.info( + f"Cache sync not ready, waiting {delay:.1f}s before retry {attempt + 1}/{self.max_retries}" + ) + await asyncio.sleep(delay) + continue + else: + logger.warning( + f"Cache sync still not ready after {self.max_retries} retries, giving up" + ) + break + else: + # Not a cache sync error, don't retry + break + + # If we get here, all retries failed or it's not a retryable error + if last_exception is not None: + raise last_exception + raise RuntimeError("Unexpected error: no exception captured during retry loop") + + async def fetch_slack_messages( + self, channel: Optional[str] = None, limit: int = 100 + ) -> list[dict[str, Any]]: + """ + Fetch Slack messages using MCP tools with retry logic for cache sync issues. + + Args: + channel: Optional channel name to filter messages + limit: Maximum number of messages to fetch + + Returns: + List of message dictionaries + """ + return await self._retry_with_backoff(self._fetch_slack_messages_impl, channel, limit) + + async def _fetch_slack_messages_impl( + self, channel: Optional[str] = None, limit: int = 100 + ) -> list[dict[str, Any]]: + """ + Internal implementation of fetch_slack_messages without retry logic. + """ + # This is a generic implementation - specific MCP servers may have different tool names + # Common tool names might be: 'get_messages', 'list_messages', 'fetch_channel_history' + + tools = await self.list_available_tools() + logger.info(f"Available tools: {[tool.get('name') for tool in tools]}") + message_tool = None + + # Look for a tool that can fetch messages - prioritize conversations_history + message_tool = None + + # First, try to find conversations_history specifically + for tool in tools: + tool_name = tool.get("name", "").lower() + if "conversations_history" in tool_name: + message_tool = tool + logger.info(f"Found conversations_history tool: {tool}") + break + + # If not found, look for other message-fetching tools + if not message_tool: + for tool in tools: + tool_name = tool.get("name", "").lower() + if any( + keyword in tool_name + for keyword in ["conversations_search", "message", "history"] + ): + message_tool = tool + break + + if not message_tool: + raise RuntimeError("No message fetching tool found in MCP server") + + # Prepare tool call parameters + tool_params = {"limit": "180d"} # Use 180 days to get older messages + if channel: + # For conversations_history, use channel_id parameter + if message_tool["name"] == "conversations_history": + tool_params["channel_id"] = channel + else: + # Try common parameter names for channel specification + for param_name in ["channel", "channel_id", "channel_name"]: + tool_params[param_name] = channel + break + + logger.info(f"Tool parameters: {tool_params}") + + fetch_request = { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": {"name": message_tool["name"], "arguments": tool_params}, + } + + response = await self.send_mcp_request(fetch_request) + if "error" in response: + raise RuntimeError(f"Failed to fetch messages: {response['error']}") + + # Extract messages from response - format may vary by MCP server + result = response.get("result", {}) + if "content" in result and isinstance(result["content"], list): + # Some MCP servers return content as a list + content = result["content"][0] if result["content"] else {} + if "text" in content: + try: + messages = json.loads(content["text"]) + except json.JSONDecodeError: + # If not JSON, try to parse as CSV format (Slack MCP server format) + text_content = content.get("text", "") + messages = self._parse_csv_messages( + text_content if text_content else "", channel or "unknown" + ) + else: + messages = result["content"] + else: + # Direct message format + messages = result.get("messages", [result]) + + return messages if isinstance(messages, list) else [messages] + + def _parse_csv_messages(self, csv_text: str, channel: str) -> list[dict[str, Any]]: + """Parse CSV format messages from Slack MCP server.""" + import csv + import io + + messages = [] + try: + # Split by lines and process each line as a CSV row + lines = csv_text.strip().split("\n") + if not lines: + return messages + + # Skip header line if it exists + start_idx = 0 + if lines[0].startswith("MsgID,UserID,UserName"): + start_idx = 1 + + for line in lines[start_idx:]: + if not line.strip(): + continue + + # Parse CSV line + reader = csv.reader(io.StringIO(line)) + try: + row = next(reader) + if len(row) >= 7: # Ensure we have enough columns + message = { + "ts": row[0], + "user": row[1], + "username": row[2], + "real_name": row[3], + "channel": row[4], + "thread_ts": row[5], + "text": row[6], + "time": row[7] if len(row) > 7 else "", + "reactions": row[8] if len(row) > 8 else "", + "cursor": row[9] if len(row) > 9 else "", + } + messages.append(message) + except Exception as e: + logger.warning(f"Failed to parse CSV line: {line[:100]}... Error: {e}") + continue + + except Exception as e: + logger.warning(f"Failed to parse CSV messages: {e}") + # Fallback: treat entire text as one message + messages = [{"text": csv_text, "channel": channel or "unknown"}] + + return messages + + def _format_message(self, message: dict[str, Any]) -> str: + """Format a single message for indexing.""" + text = message.get("text", "") + user = message.get("user", message.get("username", "Unknown")) + channel = message.get("channel", message.get("channel_name", "Unknown")) + timestamp = message.get("ts", message.get("timestamp", "")) + + # Format timestamp if available + formatted_time = "" + if timestamp: + try: + import datetime + + if isinstance(timestamp, str) and "." in timestamp: + dt = datetime.datetime.fromtimestamp(float(timestamp)) + formatted_time = dt.strftime("%Y-%m-%d %H:%M:%S") + elif isinstance(timestamp, (int, float)): + dt = datetime.datetime.fromtimestamp(timestamp) + formatted_time = dt.strftime("%Y-%m-%d %H:%M:%S") + else: + formatted_time = str(timestamp) + except (ValueError, TypeError): + formatted_time = str(timestamp) + + # Build formatted message + parts = [] + if channel: + parts.append(f"Channel: #{channel}") + if user: + parts.append(f"User: {user}") + if formatted_time: + parts.append(f"Time: {formatted_time}") + if text: + parts.append(f"Message: {text}") + + return "\n".join(parts) + + def _create_concatenated_content(self, messages: list[dict[str, Any]], channel: str) -> str: + """Create concatenated content from multiple messages in a channel.""" + if not messages: + return "" + + # Sort messages by timestamp if available + try: + messages.sort(key=lambda x: float(x.get("ts", x.get("timestamp", 0)))) + except (ValueError, TypeError): + pass # Keep original order if timestamps aren't numeric + + # Limit messages per conversation + if len(messages) > self.max_messages_per_conversation: + messages = messages[-self.max_messages_per_conversation :] + + # Create header + content_parts = [ + f"Slack Channel: #{channel}", + f"Message Count: {len(messages)}", + f"Workspace: {self.workspace_name or 'Unknown'}", + "=" * 50, + "", + ] + + # Add messages + for message in messages: + formatted_msg = self._format_message(message) + if formatted_msg.strip(): + content_parts.append(formatted_msg) + content_parts.append("-" * 30) + content_parts.append("") + + return "\n".join(content_parts) + + async def get_all_channels(self) -> list[str]: + """Get list of all available channels.""" + try: + channels_list_request = { + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": {"name": "channels_list", "arguments": {}}, + } + channels_response = await self.send_mcp_request(channels_list_request) + if "result" in channels_response: + result = channels_response["result"] + if "content" in result and isinstance(result["content"], list): + content = result["content"][0] if result["content"] else {} + if "text" in content: + # Parse the channels from the response + channels = [] + lines = content["text"].split("\n") + for line in lines: + if line.strip() and ("#" in line or "C" in line[:10]): + # Extract channel ID or name + parts = line.split() + for part in parts: + if part.startswith("C") and len(part) > 5: + channels.append(part) + elif part.startswith("#"): + channels.append(part[1:]) # Remove # + logger.info(f"Found {len(channels)} channels: {channels}") + return channels + return [] + except Exception as e: + logger.warning(f"Failed to get channels list: {e}") + return [] + + async def read_slack_data(self, channels: Optional[list[str]] = None) -> list[str]: + """ + Read Slack data and return formatted text chunks. + + Args: + channels: Optional list of channel names to fetch. If None, fetches from all available channels. + + Returns: + List of formatted text chunks ready for LEANN indexing + """ + try: + await self.start_mcp_server() + await self.initialize_mcp_connection() + + all_texts = [] + + if channels: + # Fetch specific channels + for channel in channels: + try: + messages = await self.fetch_slack_messages(channel=channel, limit=1000) + if messages: + if self.concatenate_conversations: + text_content = self._create_concatenated_content(messages, channel) + if text_content.strip(): + all_texts.append(text_content) + else: + # Process individual messages + for message in messages: + formatted_msg = self._format_message(message) + if formatted_msg.strip(): + all_texts.append(formatted_msg) + except Exception as e: + logger.warning(f"Failed to fetch messages from channel {channel}: {e}") + continue + else: + # Fetch from all available channels + logger.info("Fetching from all available channels...") + all_channels = await self.get_all_channels() + + if not all_channels: + # Fallback to common channel names if we can't get the list + all_channels = ["general", "random", "announcements", "C0GN5BX0F"] + logger.info(f"Using fallback channels: {all_channels}") + + for channel in all_channels: + try: + logger.info(f"Searching channel: {channel}") + messages = await self.fetch_slack_messages(channel=channel, limit=1000) + if messages: + if self.concatenate_conversations: + text_content = self._create_concatenated_content(messages, channel) + if text_content.strip(): + all_texts.append(text_content) + else: + # Process individual messages + for message in messages: + formatted_msg = self._format_message(message) + if formatted_msg.strip(): + all_texts.append(formatted_msg) + except Exception as e: + logger.warning(f"Failed to fetch messages from channel {channel}: {e}") + continue + + return all_texts + + finally: + await self.stop_mcp_server() + + async def __aenter__(self): + """Async context manager entry.""" + await self.start_mcp_server() + await self.initialize_mcp_connection() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + await self.stop_mcp_server() diff --git a/apps/slack_rag.py b/apps/slack_rag.py new file mode 100644 index 0000000..8980457 --- /dev/null +++ b/apps/slack_rag.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +Slack RAG Application with MCP Support + +This application enables RAG (Retrieval-Augmented Generation) on Slack messages +by connecting to Slack MCP servers to fetch live data and index it in LEANN. + +Usage: + python -m apps.slack_rag --mcp-server "slack-mcp-server" --query "What did the team discuss about the project?" +""" + +import argparse +import asyncio +from typing import Any + +from apps.base_rag_example import BaseRAGExample +from apps.slack_data.slack_mcp_reader import SlackMCPReader + + +class SlackMCPRAG(BaseRAGExample): + """ + RAG application for Slack messages via MCP servers. + + This class provides a complete RAG pipeline for Slack data, including + MCP server connection, data fetching, indexing, and interactive chat. + """ + + def __init__(self): + super().__init__( + name="Slack MCP RAG", + description="RAG application for Slack messages via MCP servers", + default_index_name="slack_messages", + ) + + def _add_specific_arguments(self, parser: argparse.ArgumentParser): + """Add Slack MCP-specific arguments.""" + parser.add_argument( + "--mcp-server", + type=str, + required=True, + help="Command to start the Slack MCP server (e.g., 'slack-mcp-server' or 'npx slack-mcp-server')", + ) + + parser.add_argument( + "--workspace-name", + type=str, + help="Slack workspace name for better organization and filtering", + ) + + parser.add_argument( + "--channels", + nargs="+", + help="Specific Slack channels to index (e.g., general random). If not specified, fetches from all available channels", + ) + + parser.add_argument( + "--concatenate-conversations", + action="store_true", + default=True, + help="Group messages by channel/thread for better context (default: True)", + ) + + parser.add_argument( + "--no-concatenate-conversations", + action="store_true", + help="Process individual messages instead of grouping by channel", + ) + + parser.add_argument( + "--max-messages-per-channel", + type=int, + default=100, + help="Maximum number of messages to include per channel (default: 100)", + ) + + parser.add_argument( + "--test-connection", + action="store_true", + help="Test MCP server connection and list available tools without indexing", + ) + + parser.add_argument( + "--max-retries", + type=int, + default=5, + help="Maximum number of retries for failed operations (default: 5)", + ) + + parser.add_argument( + "--retry-delay", + type=float, + default=2.0, + help="Initial delay between retries in seconds (default: 2.0)", + ) + + async def test_mcp_connection(self, args) -> bool: + """Test the MCP server connection and display available tools.""" + print(f"Testing connection to MCP server: {args.mcp_server}") + + try: + reader = SlackMCPReader( + mcp_server_command=args.mcp_server, + workspace_name=args.workspace_name, + concatenate_conversations=not args.no_concatenate_conversations, + max_messages_per_conversation=args.max_messages_per_channel, + max_retries=args.max_retries, + retry_delay=args.retry_delay, + ) + + async with reader: + tools = await reader.list_available_tools() + + print("Successfully connected to MCP server!") + print(f"Available tools ({len(tools)}):") + + for i, tool in enumerate(tools, 1): + name = tool.get("name", "Unknown") + description = tool.get("description", "No description available") + print(f"\n{i}. {name}") + print( + f" Description: {description[:100]}{'...' if len(description) > 100 else ''}" + ) + + # Show input schema if available + schema = tool.get("inputSchema", {}) + if schema.get("properties"): + props = list(schema["properties"].keys())[:3] # Show first 3 properties + print( + f" Parameters: {', '.join(props)}{'...' if len(schema['properties']) > 3 else ''}" + ) + + return True + + except Exception as e: + print(f"Failed to connect to MCP server: {e}") + print("\nTroubleshooting tips:") + print("1. Make sure the MCP server is installed and accessible") + print("2. Check if the server command is correct") + print("3. Ensure you have proper authentication/credentials configured") + print("4. Try running the MCP server command directly to test it") + return False + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load Slack messages via MCP server.""" + print(f"Connecting to Slack MCP server: {args.mcp_server}") + + if args.workspace_name: + print(f"Workspace: {args.workspace_name}") + + # Filter out empty strings from channels + channels = [ch for ch in args.channels if ch.strip()] if args.channels else None + + if channels: + print(f"Channels: {', '.join(channels)}") + else: + print("Fetching from all available channels") + + concatenate = not args.no_concatenate_conversations + print( + f"Processing mode: {'Concatenated conversations' if concatenate else 'Individual messages'}" + ) + + try: + reader = SlackMCPReader( + mcp_server_command=args.mcp_server, + workspace_name=args.workspace_name, + concatenate_conversations=concatenate, + max_messages_per_conversation=args.max_messages_per_channel, + max_retries=args.max_retries, + retry_delay=args.retry_delay, + ) + + texts = await reader.read_slack_data(channels=channels) + + if not texts: + print("No messages found! This could mean:") + print("- The MCP server couldn't fetch messages") + print("- The specified channels don't exist or are empty") + print("- Authentication issues with the Slack workspace") + return [] + + print(f"Successfully loaded {len(texts)} text chunks from Slack") + + # Show sample of what was loaded + if texts: + sample_text = texts[0][:200] + "..." if len(texts[0]) > 200 else texts[0] + print("\nSample content:") + print("-" * 40) + print(sample_text) + print("-" * 40) + + # Convert strings to dict format expected by base class + return [{"text": text, "metadata": {"source": "slack"}} for text in texts] + + except Exception as e: + print(f"Error loading Slack data: {e}") + print("\nThis might be due to:") + print("- MCP server connection issues") + print("- Authentication problems") + print("- Network connectivity issues") + print("- Incorrect channel names") + raise + + async def run(self): + """Main entry point with MCP connection testing.""" + args = self.parser.parse_args() + + # Test connection if requested + if args.test_connection: + success = await self.test_mcp_connection(args) + if not success: + return + print( + "MCP server is working! You can now run without --test-connection to start indexing." + ) + return + + # Run the standard RAG pipeline + await super().run() + + +async def main(): + """Main entry point for the Slack MCP RAG application.""" + app = SlackMCPRAG() + await app.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/apps/twitter_data/__init__.py b/apps/twitter_data/__init__.py new file mode 100644 index 0000000..e0104f3 --- /dev/null +++ b/apps/twitter_data/__init__.py @@ -0,0 +1 @@ +# Twitter MCP data integration for LEANN diff --git a/apps/twitter_data/twitter_mcp_reader.py b/apps/twitter_data/twitter_mcp_reader.py new file mode 100644 index 0000000..a9cfea2 --- /dev/null +++ b/apps/twitter_data/twitter_mcp_reader.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +""" +Twitter MCP Reader for LEANN + +This module provides functionality to connect to Twitter MCP servers and fetch bookmark data +for indexing in LEANN. It supports various Twitter MCP server implementations and provides +flexible bookmark processing options. +""" + +import asyncio +import json +import logging +from typing import Any, Optional + +logger = logging.getLogger(__name__) + + +class TwitterMCPReader: + """ + Reader for Twitter bookmark data via MCP (Model Context Protocol) servers. + + This class connects to Twitter MCP servers to fetch bookmark data and convert it + into a format suitable for LEANN indexing. + """ + + def __init__( + self, + mcp_server_command: str, + username: Optional[str] = None, + include_tweet_content: bool = True, + include_metadata: bool = True, + max_bookmarks: int = 1000, + ): + """ + Initialize the Twitter MCP Reader. + + Args: + mcp_server_command: Command to start the MCP server (e.g., 'twitter-mcp-server') + username: Optional Twitter username to filter bookmarks + include_tweet_content: Whether to include full tweet content + include_metadata: Whether to include tweet metadata (likes, retweets, etc.) + max_bookmarks: Maximum number of bookmarks to fetch + """ + self.mcp_server_command = mcp_server_command + self.username = username + self.include_tweet_content = include_tweet_content + self.include_metadata = include_metadata + self.max_bookmarks = max_bookmarks + self.mcp_process: asyncio.subprocess.Process | None = None + + async def start_mcp_server(self): + """Start the MCP server process.""" + try: + self.mcp_process = await asyncio.create_subprocess_exec( + *self.mcp_server_command.split(), + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logger.info(f"Started MCP server: {self.mcp_server_command}") + except Exception as e: + logger.error(f"Failed to start MCP server: {e}") + raise + + async def stop_mcp_server(self): + """Stop the MCP server process.""" + if self.mcp_process: + self.mcp_process.terminate() + await self.mcp_process.wait() + logger.info("Stopped MCP server") + + async def send_mcp_request(self, request: dict[str, Any]) -> dict[str, Any]: + """Send a request to the MCP server and get response.""" + proc = self.mcp_process + if proc is None: + raise RuntimeError("MCP server not started") + if proc.stdin is None or proc.stdout is None: + raise RuntimeError("MCP server stdio not available") + + request_json = json.dumps(request) + "\n" + proc.stdin.write(request_json.encode()) + await proc.stdin.drain() + + response_line = await proc.stdout.readline() + if not response_line: + raise RuntimeError("No response from MCP server") + + return json.loads(response_line.decode().strip()) + + async def initialize_mcp_connection(self): + """Initialize the MCP connection.""" + init_request = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "leann-twitter-reader", "version": "1.0.0"}, + }, + } + + response = await self.send_mcp_request(init_request) + if "error" in response: + raise RuntimeError(f"MCP initialization failed: {response['error']}") + + logger.info("MCP connection initialized successfully") + + async def list_available_tools(self) -> list[dict[str, Any]]: + """List available tools from the MCP server.""" + list_request = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}} + + response = await self.send_mcp_request(list_request) + if "error" in response: + raise RuntimeError(f"Failed to list tools: {response['error']}") + + return response.get("result", {}).get("tools", []) + + async def fetch_twitter_bookmarks(self, limit: Optional[int] = None) -> list[dict[str, Any]]: + """ + Fetch Twitter bookmarks using MCP tools. + + Args: + limit: Maximum number of bookmarks to fetch + + Returns: + List of bookmark dictionaries + """ + tools = await self.list_available_tools() + bookmark_tool = None + + # Look for a tool that can fetch bookmarks + for tool in tools: + tool_name = tool.get("name", "").lower() + if any(keyword in tool_name for keyword in ["bookmark", "saved", "favorite"]): + bookmark_tool = tool + break + + if not bookmark_tool: + raise RuntimeError("No bookmark fetching tool found in MCP server") + + # Prepare tool call parameters + tool_params = {} + if limit or self.max_bookmarks: + tool_params["limit"] = limit or self.max_bookmarks + if self.username: + tool_params["username"] = self.username + + fetch_request = { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": {"name": bookmark_tool["name"], "arguments": tool_params}, + } + + response = await self.send_mcp_request(fetch_request) + if "error" in response: + raise RuntimeError(f"Failed to fetch bookmarks: {response['error']}") + + # Extract bookmarks from response + result = response.get("result", {}) + if "content" in result and isinstance(result["content"], list): + content = result["content"][0] if result["content"] else {} + if "text" in content: + try: + bookmarks = json.loads(content["text"]) + except json.JSONDecodeError: + # If not JSON, treat as plain text + bookmarks = [{"text": content["text"], "source": "twitter"}] + else: + bookmarks = result["content"] + else: + bookmarks = result.get("bookmarks", result.get("tweets", [result])) + + return bookmarks if isinstance(bookmarks, list) else [bookmarks] + + def _format_bookmark(self, bookmark: dict[str, Any]) -> str: + """Format a single bookmark for indexing.""" + # Extract tweet information + text = bookmark.get("text", bookmark.get("content", "")) + author = bookmark.get( + "author", bookmark.get("username", bookmark.get("user", {}).get("username", "Unknown")) + ) + timestamp = bookmark.get("created_at", bookmark.get("timestamp", "")) + url = bookmark.get("url", bookmark.get("tweet_url", "")) + + # Extract metadata if available + likes = bookmark.get("likes", bookmark.get("favorite_count", 0)) + retweets = bookmark.get("retweets", bookmark.get("retweet_count", 0)) + replies = bookmark.get("replies", bookmark.get("reply_count", 0)) + + # Build formatted bookmark + parts = [] + + # Header + parts.append("=== Twitter Bookmark ===") + + if author: + parts.append(f"Author: @{author}") + + if timestamp: + # Format timestamp if it's a standard format + try: + import datetime + + if "T" in str(timestamp): # ISO format + dt = datetime.datetime.fromisoformat(timestamp.replace("Z", "+00:00")) + formatted_time = dt.strftime("%Y-%m-%d %H:%M:%S") + else: + formatted_time = str(timestamp) + parts.append(f"Date: {formatted_time}") + except (ValueError, TypeError): + parts.append(f"Date: {timestamp}") + + if url: + parts.append(f"URL: {url}") + + # Tweet content + if text and self.include_tweet_content: + parts.append("") + parts.append("Content:") + parts.append(text) + + # Metadata + if self.include_metadata and any([likes, retweets, replies]): + parts.append("") + parts.append("Engagement:") + if likes: + parts.append(f" Likes: {likes}") + if retweets: + parts.append(f" Retweets: {retweets}") + if replies: + parts.append(f" Replies: {replies}") + + # Extract hashtags and mentions if available + hashtags = bookmark.get("hashtags", []) + mentions = bookmark.get("mentions", []) + + if hashtags or mentions: + parts.append("") + if hashtags: + parts.append(f"Hashtags: {', '.join(hashtags)}") + if mentions: + parts.append(f"Mentions: {', '.join(mentions)}") + + return "\n".join(parts) + + async def read_twitter_bookmarks(self) -> list[str]: + """ + Read Twitter bookmark data and return formatted text chunks. + + Returns: + List of formatted text chunks ready for LEANN indexing + """ + try: + await self.start_mcp_server() + await self.initialize_mcp_connection() + + print(f"Fetching up to {self.max_bookmarks} bookmarks...") + if self.username: + print(f"Filtering for user: @{self.username}") + + bookmarks = await self.fetch_twitter_bookmarks() + + if not bookmarks: + print("No bookmarks found") + return [] + + print(f"Processing {len(bookmarks)} bookmarks...") + + all_texts = [] + processed_count = 0 + + for bookmark in bookmarks: + try: + formatted_bookmark = self._format_bookmark(bookmark) + if formatted_bookmark.strip(): + all_texts.append(formatted_bookmark) + processed_count += 1 + except Exception as e: + logger.warning(f"Failed to format bookmark: {e}") + continue + + print(f"Successfully processed {processed_count} bookmarks") + return all_texts + + finally: + await self.stop_mcp_server() + + async def __aenter__(self): + """Async context manager entry.""" + await self.start_mcp_server() + await self.initialize_mcp_connection() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + await self.stop_mcp_server() diff --git a/apps/twitter_rag.py b/apps/twitter_rag.py new file mode 100644 index 0000000..5446a5a --- /dev/null +++ b/apps/twitter_rag.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +Twitter RAG Application with MCP Support + +This application enables RAG (Retrieval-Augmented Generation) on Twitter bookmarks +by connecting to Twitter MCP servers to fetch live data and index it in LEANN. + +Usage: + python -m apps.twitter_rag --mcp-server "twitter-mcp-server" --query "What articles did I bookmark about AI?" +""" + +import argparse +import asyncio +from typing import Any + +from apps.base_rag_example import BaseRAGExample +from apps.twitter_data.twitter_mcp_reader import TwitterMCPReader + + +class TwitterMCPRAG(BaseRAGExample): + """ + RAG application for Twitter bookmarks via MCP servers. + + This class provides a complete RAG pipeline for Twitter bookmark data, including + MCP server connection, data fetching, indexing, and interactive chat. + """ + + def __init__(self): + super().__init__( + name="Twitter MCP RAG", + description="RAG application for Twitter bookmarks via MCP servers", + default_index_name="twitter_bookmarks", + ) + + def _add_specific_arguments(self, parser: argparse.ArgumentParser): + """Add Twitter MCP-specific arguments.""" + parser.add_argument( + "--mcp-server", + type=str, + required=True, + help="Command to start the Twitter MCP server (e.g., 'twitter-mcp-server' or 'npx twitter-mcp-server')", + ) + + parser.add_argument( + "--username", type=str, help="Twitter username to filter bookmarks (without @)" + ) + + parser.add_argument( + "--max-bookmarks", + type=int, + default=1000, + help="Maximum number of bookmarks to fetch (default: 1000)", + ) + + parser.add_argument( + "--no-tweet-content", + action="store_true", + help="Exclude tweet content, only include metadata", + ) + + parser.add_argument( + "--no-metadata", + action="store_true", + help="Exclude engagement metadata (likes, retweets, etc.)", + ) + + parser.add_argument( + "--test-connection", + action="store_true", + help="Test MCP server connection and list available tools without indexing", + ) + + async def test_mcp_connection(self, args) -> bool: + """Test the MCP server connection and display available tools.""" + print(f"Testing connection to MCP server: {args.mcp_server}") + + try: + reader = TwitterMCPReader( + mcp_server_command=args.mcp_server, + username=args.username, + include_tweet_content=not args.no_tweet_content, + include_metadata=not args.no_metadata, + max_bookmarks=args.max_bookmarks, + ) + + async with reader: + tools = await reader.list_available_tools() + + print("\nβœ… Successfully connected to MCP server!") + print(f"Available tools ({len(tools)}):") + + for i, tool in enumerate(tools, 1): + name = tool.get("name", "Unknown") + description = tool.get("description", "No description available") + print(f"\n{i}. {name}") + print( + f" Description: {description[:100]}{'...' if len(description) > 100 else ''}" + ) + + # Show input schema if available + schema = tool.get("inputSchema", {}) + if schema.get("properties"): + props = list(schema["properties"].keys())[:3] # Show first 3 properties + print( + f" Parameters: {', '.join(props)}{'...' if len(schema['properties']) > 3 else ''}" + ) + + return True + + except Exception as e: + print(f"\n❌ Failed to connect to MCP server: {e}") + print("\nTroubleshooting tips:") + print("1. Make sure the Twitter MCP server is installed and accessible") + print("2. Check if the server command is correct") + print("3. Ensure you have proper Twitter API credentials configured") + print("4. Verify your Twitter account has bookmarks to fetch") + print("5. Try running the MCP server command directly to test it") + return False + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load Twitter bookmarks via MCP server.""" + print(f"Connecting to Twitter MCP server: {args.mcp_server}") + + if args.username: + print(f"Username filter: @{args.username}") + + print(f"Max bookmarks: {args.max_bookmarks}") + print(f"Include tweet content: {not args.no_tweet_content}") + print(f"Include metadata: {not args.no_metadata}") + + try: + reader = TwitterMCPReader( + mcp_server_command=args.mcp_server, + username=args.username, + include_tweet_content=not args.no_tweet_content, + include_metadata=not args.no_metadata, + max_bookmarks=args.max_bookmarks, + ) + + texts = await reader.read_twitter_bookmarks() + + if not texts: + print("❌ No bookmarks found! This could mean:") + print("- You don't have any bookmarks on Twitter") + print("- The MCP server couldn't access your bookmarks") + print("- Authentication issues with Twitter API") + print("- The username filter didn't match any bookmarks") + return [] + + print(f"βœ… Successfully loaded {len(texts)} bookmarks from Twitter") + + # Show sample of what was loaded + if texts: + sample_text = texts[0][:300] + "..." if len(texts[0]) > 300 else texts[0] + print("\nSample bookmark:") + print("-" * 50) + print(sample_text) + print("-" * 50) + + # Convert strings to dict format expected by base class + return [{"text": text, "metadata": {"source": "twitter"}} for text in texts] + + except Exception as e: + print(f"❌ Error loading Twitter bookmarks: {e}") + print("\nThis might be due to:") + print("- MCP server connection issues") + print("- Twitter API authentication problems") + print("- Network connectivity issues") + print("- Rate limiting from Twitter API") + raise + + async def run(self): + """Main entry point with MCP connection testing.""" + args = self.parser.parse_args() + + # Test connection if requested + if args.test_connection: + success = await self.test_mcp_connection(args) + if not success: + return + print( + "\nπŸŽ‰ MCP server is working! You can now run without --test-connection to start indexing." + ) + return + + # Run the standard RAG pipeline + await super().run() + + +async def main(): + """Main entry point for the Twitter MCP RAG application.""" + app = TwitterMCPRAG() + await app.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/apps/wechat_rag.py b/apps/wechat_rag.py new file mode 100644 index 0000000..1e5dd31 --- /dev/null +++ b/apps/wechat_rag.py @@ -0,0 +1,190 @@ +""" +WeChat History RAG example using the unified interface. +Supports WeChat chat history export and search. +""" + +import subprocess +import sys +from pathlib import Path +from typing import Any + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from base_rag_example import BaseRAGExample + +from .history_data.wechat_history import WeChatHistoryReader + + +class WeChatRAG(BaseRAGExample): + """RAG example for WeChat chat history.""" + + def __init__(self): + # Set default values BEFORE calling super().__init__ + self.max_items_default = -1 # Match original default + self.embedding_model_default = ( + "sentence-transformers/all-MiniLM-L6-v2" # Fast 384-dim model + ) + + super().__init__( + name="WeChat History", + description="Process and query WeChat chat history with LEANN", + default_index_name="wechat_history_magic_test_11Debug_new", + ) + + def _add_specific_arguments(self, parser): + """Add WeChat-specific arguments.""" + wechat_group = parser.add_argument_group("WeChat Parameters") + wechat_group.add_argument( + "--export-dir", + type=str, + default="./wechat_export", + help="Directory to store WeChat exports (default: ./wechat_export)", + ) + wechat_group.add_argument( + "--force-export", + action="store_true", + help="Force re-export of WeChat data even if exports exist", + ) + wechat_group.add_argument( + "--chunk-size", type=int, default=192, help="Text chunk size (default: 192)" + ) + wechat_group.add_argument( + "--chunk-overlap", type=int, default=64, help="Text chunk overlap (default: 64)" + ) + + def _export_wechat_data(self, export_dir: Path) -> bool: + """Export WeChat data using wechattweak-cli.""" + print("Exporting WeChat data...") + + # Check if WeChat is running + try: + result = subprocess.run(["pgrep", "WeChat"], capture_output=True, text=True) + if result.returncode != 0: + print("WeChat is not running. Please start WeChat first.") + return False + except Exception: + pass # pgrep might not be available on all systems + + # Create export directory + export_dir.mkdir(parents=True, exist_ok=True) + + # Run export command + cmd = ["packages/wechat-exporter/wechattweak-cli", "export", str(export_dir)] + + try: + print(f"Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + print("WeChat data exported successfully!") + return True + else: + print(f"Export failed: {result.stderr}") + return False + + except FileNotFoundError: + print("\nError: wechattweak-cli not found!") + print("Please install it first:") + print(" sudo packages/wechat-exporter/wechattweak-cli install") + return False + except Exception as e: + print(f"Export error: {e}") + return False + + async def load_data(self, args) -> list[dict[str, Any]]: + """Load WeChat history and convert to text chunks.""" + # Initialize WeChat reader with export capabilities + reader = WeChatHistoryReader() + + # Find existing exports or create new ones using the centralized method + export_dirs = reader.find_or_export_wechat_data(args.export_dir) + if not export_dirs: + print("Failed to find or export WeChat data. Trying to find any existing exports...") + # Try to find any existing exports in common locations + export_dirs = reader.find_wechat_export_dirs() + if not export_dirs: + print("No WeChat data found. Please ensure WeChat exports exist.") + return [] + + # Load documents from all found export directories + all_documents = [] + total_processed = 0 + + for i, export_dir in enumerate(export_dirs): + print(f"\nProcessing WeChat export {i + 1}/{len(export_dirs)}: {export_dir}") + + try: + # Apply max_items limit per export + max_per_export = -1 + if args.max_items > 0: + remaining = args.max_items - total_processed + if remaining <= 0: + break + max_per_export = remaining + + documents = reader.load_data( + wechat_export_dir=str(export_dir), + max_count=max_per_export, + concatenate_messages=True, # Enable message concatenation for better context + ) + + if documents: + print(f"Loaded {len(documents)} chat documents from {export_dir}") + all_documents.extend(documents) + total_processed += len(documents) + else: + print(f"No documents loaded from {export_dir}") + + except Exception as e: + print(f"Error processing {export_dir}: {e}") + continue + + if not all_documents: + print("No documents loaded from any source. Exiting.") + return [] + + print(f"\nTotal loaded {len(all_documents)} chat documents from {len(export_dirs)} exports") + print("now starting to split into text chunks ... take some time") + + # Convert to text chunks with contact information + all_texts = [] + for doc in all_documents: + # Split the document into chunks + from llama_index.core.node_parser import SentenceSplitter + + text_splitter = SentenceSplitter( + chunk_size=args.chunk_size, chunk_overlap=args.chunk_overlap + ) + nodes = text_splitter.get_nodes_from_documents([doc]) + + for node in nodes: + # Add contact information to each chunk + contact_name = doc.metadata.get("contact_name", "Unknown") + text = f"[Contact] means the message is from: {contact_name}\n" + node.get_content() + all_texts.append(text) + + print(f"Created {len(all_texts)} text chunks from {len(all_documents)} documents") + return all_texts + + +if __name__ == "__main__": + import asyncio + + # Check platform + if sys.platform != "darwin": + print("\n⚠️ Warning: WeChat export is only supported on macOS") + print(" You can still query existing exports on other platforms\n") + + # Example queries for WeChat RAG + print("\nπŸ’¬ WeChat History RAG Example") + print("=" * 50) + print("\nExample queries you can try:") + print("- 'Show me conversations about travel plans'") + print("- 'Find group chats about weekend activities'") + print("- 'ζˆ‘ζƒ³δΉ°ι­”ζœ―εΈˆηΊ¦ηΏ°ι€Šηš„ηƒθ‘£,η»™ζˆ‘δΈ€δΊ›ε―ΉεΊ”θŠε€©θ°ε½•?'") + print("- 'What did we discuss about the project last month?'") + print("\nNote: WeChat must be running for export to work\n") + + rag = WeChatRAG() + asyncio.run(rag.run()) diff --git a/assets/wechat_user_group.JPG b/assets/wechat_user_group.JPG new file mode 100644 index 0000000..ab6236e Binary files /dev/null and b/assets/wechat_user_group.JPG differ diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..258f260 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,135 @@ +# πŸ§ͺ LEANN Benchmarks & Testing + +This directory contains performance benchmarks and comprehensive tests for the LEANN system, including backend comparisons and sanity checks across different configurations. + +## πŸ“ Test Files + +### `diskann_vs_hnsw_speed_comparison.py` +Performance comparison between DiskANN and HNSW backends: +- βœ… **Search latency** comparison with both backends using recompute +- βœ… **Index size** and **build time** measurements +- βœ… **Score validity** testing (ensures no -inf scores) +- βœ… **Configurable dataset sizes** for different scales + +```bash +# Quick comparison with 500 docs, 10 queries +python benchmarks/diskann_vs_hnsw_speed_comparison.py + +# Large-scale comparison with 2000 docs, 20 queries +python benchmarks/diskann_vs_hnsw_speed_comparison.py 2000 20 +``` + +### `test_distance_functions.py` +Tests all supported distance functions across DiskANN backend: +- βœ… **MIPS** (Maximum Inner Product Search) +- βœ… **L2** (Euclidean Distance) +- βœ… **Cosine** (Cosine Similarity) + +```bash +uv run python tests/sanity_checks/test_distance_functions.py +``` + +### `test_l2_verification.py` +Specifically verifies that L2 distance is correctly implemented by: +- Building indices with L2 vs Cosine metrics +- Comparing search results and score ranges +- Validating that different metrics produce expected score patterns + +```bash +uv run python tests/sanity_checks/test_l2_verification.py +``` + +### `test_sanity_check.py` +Comprehensive end-to-end verification including: +- Distance function testing +- Embedding model compatibility +- Search result correctness validation +- Backend integration testing + +```bash +uv run python tests/sanity_checks/test_sanity_check.py +``` + +## 🎯 What These Tests Verify + +### βœ… Distance Function Support +- All three distance metrics (MIPS, L2, Cosine) work correctly +- Score ranges are appropriate for each metric type +- Different metrics can produce different rankings (as expected) + +### βœ… Backend Integration +- DiskANN backend properly initializes and builds indices +- Graph construction completes without errors +- Search operations return valid results + +### βœ… Embedding Pipeline +- Real-time embedding computation works +- Multiple embedding models are supported +- ZMQ server communication functions correctly + +### βœ… End-to-End Functionality +- Index building β†’ searching β†’ result retrieval pipeline +- Metadata preservation through the entire flow +- Error handling and graceful degradation + +## πŸ” Expected Output + +When all tests pass, you should see: + +``` +πŸ“Š ζ΅‹θ―•η»“ζžœζ€»η»“: + mips : βœ… ι€šθΏ‡ + l2 : βœ… ι€šθΏ‡ + cosine : βœ… ι€šθΏ‡ + +πŸŽ‰ ζ΅‹θ―•εŒζˆ! +``` + +## πŸ› Troubleshooting + +### Common Issues + +**Import Errors**: Ensure you're running from the project root: +```bash +cd /path/to/leann +uv run python tests/sanity_checks/test_distance_functions.py +``` + +**Memory Issues**: Reduce graph complexity for resource-constrained systems: +```python +builder = LeannBuilder( + backend_name="diskann", + graph_degree=8, # Reduced from 16 + complexity=16 # Reduced from 32 +) +``` + +**ZMQ Port Conflicts**: The tests use different ports to avoid conflicts, but you may need to kill existing processes: +```bash +pkill -f "embedding_server" +``` + +## πŸ“Š Performance Expectations + +### Typical Timing (3 documents, consumer hardware): +- **Index Building**: 2-5 seconds per distance function +- **Search Query**: 50-200ms +- **Recompute Mode**: 5-15 seconds (higher accuracy) + +### Memory Usage: +- **Index Storage**: ~1-2 MB per distance function +- **Runtime Memory**: ~500MB (including model loading) + +## πŸ”— Integration with CI/CD + +These tests are designed to be run in automated environments: + +```yaml +# GitHub Actions example +- name: Run Sanity Checks + run: | + uv run python tests/sanity_checks/test_distance_functions.py + uv run python tests/sanity_checks/test_l2_verification.py +``` + +The tests are deterministic and should produce consistent results across different platforms. diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/benchmarks/benchmark_embeddings.py b/benchmarks/benchmark_embeddings.py new file mode 100644 index 0000000..c44610d --- /dev/null +++ b/benchmarks/benchmark_embeddings.py @@ -0,0 +1,141 @@ +import time + +import matplotlib.pyplot as plt +import mlx.core as mx +import numpy as np +import torch +from mlx_lm import load +from sentence_transformers import SentenceTransformer + +# --- Configuration --- +MODEL_NAME_TORCH = "Qwen/Qwen3-Embedding-0.6B" +MODEL_NAME_MLX = "mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ" +BATCH_SIZES = [1, 8, 16, 32, 64, 128] +NUM_RUNS = 10 # Number of runs to average for each batch size +WARMUP_RUNS = 2 # Number of warm-up runs + +# --- Generate Dummy Data --- +DUMMY_SENTENCES = ["This is a test sentence for benchmarking." * 5] * max(BATCH_SIZES) + +# --- Benchmark Functions ---b + + +def benchmark_torch(model, sentences): + start_time = time.time() + model.encode(sentences, convert_to_numpy=True) + end_time = time.time() + return (end_time - start_time) * 1000 # Return time in ms + + +def benchmark_mlx(model, tokenizer, sentences): + start_time = time.time() + + # Tokenize sentences using MLX tokenizer + tokens = [] + for sentence in sentences: + token_ids = tokenizer.encode(sentence) + tokens.append(token_ids) + + # Pad sequences to the same length + max_len = max(len(t) for t in tokens) + input_ids = [] + attention_mask = [] + + for token_seq in tokens: + # Pad sequence + padded = token_seq + [tokenizer.eos_token_id] * (max_len - len(token_seq)) + input_ids.append(padded) + # Create attention mask (1 for real tokens, 0 for padding) + mask = [1] * len(token_seq) + [0] * (max_len - len(token_seq)) + attention_mask.append(mask) + + # Convert to MLX arrays + input_ids = mx.array(input_ids) + attention_mask = mx.array(attention_mask) + + # Get embeddings + embeddings = model(input_ids) + + # Mean pooling + mask = mx.expand_dims(attention_mask, -1) + sum_embeddings = (embeddings * mask).sum(axis=1) + sum_mask = mask.sum(axis=1) + _ = sum_embeddings / sum_mask + + mx.eval() # Ensure computation is finished + end_time = time.time() + return (end_time - start_time) * 1000 # Return time in ms + + +# --- Main Execution --- +def main(): + print("--- Initializing Models ---") + # Load PyTorch model + print(f"Loading PyTorch model: {MODEL_NAME_TORCH}") + device = "mps" if torch.backends.mps.is_available() else "cpu" + model_torch = SentenceTransformer(MODEL_NAME_TORCH, device=device) + print(f"PyTorch model loaded on: {device}") + + # Load MLX model + print(f"Loading MLX model: {MODEL_NAME_MLX}") + model_mlx, tokenizer_mlx = load(MODEL_NAME_MLX) + print("MLX model loaded.") + + # --- Warm-up --- + print("\n--- Performing Warm-up Runs ---") + for _ in range(WARMUP_RUNS): + benchmark_torch(model_torch, DUMMY_SENTENCES[:1]) + benchmark_mlx(model_mlx, tokenizer_mlx, DUMMY_SENTENCES[:1]) + print("Warm-up complete.") + + # --- Benchmarking --- + print("\n--- Starting Benchmark ---") + results_torch = [] + results_mlx = [] + + for batch_size in BATCH_SIZES: + print(f"Benchmarking batch size: {batch_size}") + sentences_batch = DUMMY_SENTENCES[:batch_size] + + # Benchmark PyTorch + torch_times = [benchmark_torch(model_torch, sentences_batch) for _ in range(NUM_RUNS)] + results_torch.append(np.mean(torch_times)) + + # Benchmark MLX + mlx_times = [ + benchmark_mlx(model_mlx, tokenizer_mlx, sentences_batch) for _ in range(NUM_RUNS) + ] + results_mlx.append(np.mean(mlx_times)) + + print("\n--- Benchmark Results (Average time per batch in ms) ---") + print(f"Batch Sizes: {BATCH_SIZES}") + print(f"PyTorch (mps): {[f'{t:.2f}' for t in results_torch]}") + print(f"MLX: {[f'{t:.2f}' for t in results_mlx]}") + + # --- Plotting --- + print("\n--- Generating Plot ---") + plt.figure(figsize=(10, 6)) + plt.plot( + BATCH_SIZES, + results_torch, + marker="o", + linestyle="-", + label=f"PyTorch ({device})", + ) + plt.plot(BATCH_SIZES, results_mlx, marker="s", linestyle="-", label="MLX") + + plt.title(f"Embedding Performance: MLX vs PyTorch\nModel: {MODEL_NAME_TORCH}") + plt.xlabel("Batch Size") + plt.ylabel("Average Time per Batch (ms)") + plt.xticks(BATCH_SIZES) + plt.grid(True) + plt.legend() + + # Save the plot + output_filename = "embedding_benchmark.png" + plt.savefig(output_filename) + print(f"Plot saved to {output_filename}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/benchmark_no_recompute.py b/benchmarks/benchmark_no_recompute.py new file mode 100644 index 0000000..1c402c0 --- /dev/null +++ b/benchmarks/benchmark_no_recompute.py @@ -0,0 +1,148 @@ +import argparse +import os +import time +from pathlib import Path + +from leann import LeannBuilder, LeannSearcher + + +def _meta_exists(index_path: str) -> bool: + p = Path(index_path) + return (p.parent / f"{p.stem}.meta.json").exists() + + +def ensure_index(index_path: str, backend_name: str, num_docs: int, is_recompute: bool) -> None: + # if _meta_exists(index_path): + # return + kwargs = {} + if backend_name == "hnsw": + kwargs["is_compact"] = is_recompute + builder = LeannBuilder( + backend_name=backend_name, + embedding_model=os.getenv("LEANN_EMBED_MODEL", "facebook/contriever"), + embedding_mode=os.getenv("LEANN_EMBED_MODE", "sentence-transformers"), + graph_degree=32, + complexity=64, + is_recompute=is_recompute, + num_threads=4, + **kwargs, + ) + for i in range(num_docs): + builder.add_text( + f"This is a test document number {i}. It contains some repeated text for benchmarking." + ) + builder.build_index(index_path) + + +def _bench_group( + index_path: str, + recompute: bool, + query: str, + repeats: int, + complexity: int = 32, + top_k: int = 10, +) -> float: + # Independent searcher per group; fixed port when recompute + searcher = LeannSearcher(index_path=index_path) + + # Warm-up once + _ = searcher.search( + query, + top_k=top_k, + complexity=complexity, + recompute_embeddings=recompute, + ) + + def _once() -> float: + t0 = time.time() + _ = searcher.search( + query, + top_k=top_k, + complexity=complexity, + recompute_embeddings=recompute, + ) + return time.time() - t0 + + if repeats <= 1: + t = _once() + else: + vals = [_once() for _ in range(repeats)] + vals.sort() + t = vals[len(vals) // 2] + + searcher.cleanup() + return t + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--num-docs", type=int, default=5000) + parser.add_argument("--repeats", type=int, default=3) + parser.add_argument("--complexity", type=int, default=32) + args = parser.parse_args() + + base = Path.cwd() / ".leann" / "indexes" / f"bench_n{args.num_docs}" + base.parent.mkdir(parents=True, exist_ok=True) + # ---------- Build HNSW variants ---------- + hnsw_r = str(base / f"hnsw_recompute_n{args.num_docs}.leann") + hnsw_nr = str(base / f"hnsw_norecompute_n{args.num_docs}.leann") + ensure_index(hnsw_r, "hnsw", args.num_docs, True) + ensure_index(hnsw_nr, "hnsw", args.num_docs, False) + + # ---------- Build DiskANN variants ---------- + diskann_r = str(base / "diskann_r.leann") + diskann_nr = str(base / "diskann_nr.leann") + ensure_index(diskann_r, "diskann", args.num_docs, True) + ensure_index(diskann_nr, "diskann", args.num_docs, False) + + # ---------- Helpers ---------- + def _size_for(prefix: str) -> int: + p = Path(prefix) + base_dir = p.parent + stem = p.stem + total = 0 + for f in base_dir.iterdir(): + if f.is_file() and f.name.startswith(stem): + total += f.stat().st_size + return total + + # ---------- HNSW benchmark ---------- + t_hnsw_r = _bench_group( + hnsw_r, True, "test document number 42", repeats=args.repeats, complexity=args.complexity + ) + t_hnsw_nr = _bench_group( + hnsw_nr, False, "test document number 42", repeats=args.repeats, complexity=args.complexity + ) + size_hnsw_r = _size_for(hnsw_r) + size_hnsw_nr = _size_for(hnsw_nr) + + print("Benchmark results (HNSW):") + print(f" recompute=True: search_time={t_hnsw_r:.3f}s, size={size_hnsw_r / 1024 / 1024:.1f}MB") + print( + f" recompute=False: search_time={t_hnsw_nr:.3f}s, size={size_hnsw_nr / 1024 / 1024:.1f}MB" + ) + print(" Expectation: no-recompute should be faster but larger on disk.") + + # ---------- DiskANN benchmark ---------- + t_diskann_r = _bench_group( + diskann_r, True, "DiskANN R test doc 123", repeats=args.repeats, complexity=args.complexity + ) + t_diskann_nr = _bench_group( + diskann_nr, + False, + "DiskANN NR test doc 123", + repeats=args.repeats, + complexity=args.complexity, + ) + size_diskann_r = _size_for(diskann_r) + size_diskann_nr = _size_for(diskann_nr) + + print("\nBenchmark results (DiskANN):") + print(f" build(recompute=True, partition): size={size_diskann_r / 1024 / 1024:.1f}MB") + print(f" build(recompute=False): size={size_diskann_nr / 1024 / 1024:.1f}MB") + print(f" search recompute=True (final rerank): {t_diskann_r:.3f}s") + print(f" search recompute=False (PQ only): {t_diskann_nr:.3f}s") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/bm25_diskann_baselines/README.md b/benchmarks/bm25_diskann_baselines/README.md new file mode 100644 index 0000000..cd624ef --- /dev/null +++ b/benchmarks/bm25_diskann_baselines/README.md @@ -0,0 +1,23 @@ +BM25 vs DiskANN Baselines + +```bash +aws s3 sync s3://powerrag-diskann-rpj-wiki-20250824-224037-194d640c/bm25_rpj_wiki/index_en_only/ benchmarks/data/indices/bm25_index/ +aws s3 sync s3://powerrag-diskann-rpj-wiki-20250824-224037-194d640c/diskann_rpj_wiki/ benchmarks/data/indices/diskann_rpj_wiki/ +``` + +- Dataset: `benchmarks/data/queries/nq_open.jsonl` (Natural Questions) +- Machine-specific; results measured locally with the current repo. + +DiskANN (NQ queries, search-only) +- Command: `uv run --script benchmarks/bm25_diskann_baselines/run_diskann.py` +- Settings: `recompute_embeddings=False`, embeddings precomputed (excluded from timing), batching off, caching off (`cache_mechanism=2`, `num_nodes_to_cache=0`) +- Result: avg 0.011093 s/query, QPS 90.15 (p50 0.010731 s, p95 0.015000 s) + +BM25 +- Command: `uv run --script benchmarks/bm25_diskann_baselines/run_bm25.py` +- Settings: `k=10`, `k1=0.9`, `b=0.4`, queries=100 +- Result: avg 0.028589 s/query, QPS 34.97 (p50 0.026060 s, p90 0.043695 s, p95 0.053260 s, p99 0.055257 s) + +Notes +- DiskANN measures search-only latency on real NQ queries (embeddings computed beforehand and excluded from timing). +- Use `benchmarks/bm25_diskann_baselines/run_diskann.py` for DiskANN; `benchmarks/bm25_diskann_baselines/run_bm25.py` for BM25. diff --git a/benchmarks/bm25_diskann_baselines/run_bm25.py b/benchmarks/bm25_diskann_baselines/run_bm25.py new file mode 100644 index 0000000..f10ad93 --- /dev/null +++ b/benchmarks/bm25_diskann_baselines/run_bm25.py @@ -0,0 +1,183 @@ +# /// script +# dependencies = [ +# "pyserini" +# ] +# /// +# sudo pacman -S jdk21-openjdk +# export JAVA_HOME=/usr/lib/jvm/java-21-openjdk +# sudo archlinux-java status +# sudo archlinux-java set java-21-openjdk +# set -Ux JAVA_HOME /usr/lib/jvm/java-21-openjdk +# fish_add_path --global $JAVA_HOME/bin +# set -Ux LD_LIBRARY_PATH $JAVA_HOME/lib/server $LD_LIBRARY_PATH +# which javac # Should be /usr/lib/jvm/java-21-openjdk/bin/javac + +import argparse +import json +import os +import sys +import time +from statistics import mean + + +def load_queries(path: str, limit: int | None) -> list[str]: + queries: list[str] = [] + # Try JSONL with a 'query' or 'text' field; fallback to plain text (one query per line) + _, ext = os.path.splitext(path) + if ext.lower() in {".jsonl", ".json"}: + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + except json.JSONDecodeError: + # Not strict JSONL? treat the whole line as the query + queries.append(line) + continue + q = obj.get("query") or obj.get("text") or obj.get("question") + if q: + queries.append(str(q)) + else: + with open(path, encoding="utf-8") as f: + for line in f: + s = line.strip() + if s: + queries.append(s) + + if limit is not None and limit > 0: + queries = queries[:limit] + return queries + + +def percentile(values: list[float], p: float) -> float: + if not values: + return 0.0 + s = sorted(values) + k = (len(s) - 1) * (p / 100.0) + f = int(k) + c = min(f + 1, len(s) - 1) + if f == c: + return s[f] + return s[f] + (s[c] - s[f]) * (k - f) + + +def main(): + ap = argparse.ArgumentParser(description="Standalone BM25 latency benchmark (Pyserini)") + ap.add_argument( + "--bm25-index", + default="benchmarks/data/indices/bm25_index", + help="Path to Pyserini Lucene index directory", + ) + ap.add_argument( + "--queries", + default="benchmarks/data/queries/nq_open.jsonl", + help="Path to queries file (JSONL with 'query'/'text' or plain txt one-per-line)", + ) + ap.add_argument("--k", type=int, default=10, help="Top-k to retrieve (default: 10)") + ap.add_argument("--k1", type=float, default=0.9, help="BM25 k1 (default: 0.9)") + ap.add_argument("--b", type=float, default=0.4, help="BM25 b (default: 0.4)") + ap.add_argument("--limit", type=int, default=100, help="Max queries to run (default: 100)") + ap.add_argument( + "--warmup", type=int, default=5, help="Warmup queries not counted in latency (default: 5)" + ) + ap.add_argument( + "--fetch-docs", action="store_true", help="Also fetch doc contents (slower; default: off)" + ) + ap.add_argument("--report", type=str, default=None, help="Optional JSON report path") + args = ap.parse_args() + + try: + from pyserini.search.lucene import LuceneSearcher + except Exception: + print("Pyserini not found. Install with: pip install pyserini", file=sys.stderr) + raise + + if not os.path.isdir(args.bm25_index): + print(f"Index directory not found: {args.bm25_index}", file=sys.stderr) + sys.exit(1) + + queries = load_queries(args.queries, args.limit) + if not queries: + print("No queries loaded.", file=sys.stderr) + sys.exit(1) + + print(f"Loaded {len(queries)} queries from {args.queries}") + print(f"Opening BM25 index: {args.bm25_index}") + searcher = LuceneSearcher(args.bm25_index) + # Some builds of pyserini require explicit set_bm25; others ignore + try: + searcher.set_bm25(k1=args.k1, b=args.b) + except Exception: + pass + + latencies: list[float] = [] + total_searches = 0 + + # Warmup + for i in range(min(args.warmup, len(queries))): + _ = searcher.search(queries[i], k=args.k) + + t0 = time.time() + for i, q in enumerate(queries): + t1 = time.time() + hits = searcher.search(q, k=args.k) + t2 = time.time() + latencies.append(t2 - t1) + total_searches += 1 + + if args.fetch_docs: + # Optional doc fetch to include I/O time + for h in hits: + try: + _ = searcher.doc(h.docid) + except Exception: + pass + + if (i + 1) % 50 == 0: + print(f"Processed {i + 1}/{len(queries)} queries") + + t1 = time.time() + total_time = t1 - t0 + + if latencies: + avg = mean(latencies) + p50 = percentile(latencies, 50) + p90 = percentile(latencies, 90) + p95 = percentile(latencies, 95) + p99 = percentile(latencies, 99) + qps = total_searches / total_time if total_time > 0 else 0.0 + else: + avg = p50 = p90 = p95 = p99 = qps = 0.0 + + print("BM25 Latency Report") + print(f" queries: {total_searches}") + print(f" k: {args.k}, k1: {args.k1}, b: {args.b}") + print(f" avg per query: {avg:.6f} s") + print(f" p50/p90/p95/p99: {p50:.6f}/{p90:.6f}/{p95:.6f}/{p99:.6f} s") + print(f" total time: {total_time:.3f} s, qps: {qps:.2f}") + + if args.report: + payload = { + "queries": total_searches, + "k": args.k, + "k1": args.k1, + "b": args.b, + "avg_s": avg, + "p50_s": p50, + "p90_s": p90, + "p95_s": p95, + "p99_s": p99, + "total_time_s": total_time, + "qps": qps, + "index_dir": os.path.abspath(args.bm25_index), + "fetch_docs": bool(args.fetch_docs), + } + with open(args.report, "w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + print(f"Saved report to {args.report}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/bm25_diskann_baselines/run_diskann.py b/benchmarks/bm25_diskann_baselines/run_diskann.py new file mode 100644 index 0000000..a4dd558 --- /dev/null +++ b/benchmarks/bm25_diskann_baselines/run_diskann.py @@ -0,0 +1,124 @@ +# /// script +# dependencies = [ +# "leann-backend-diskann" +# ] +# /// + +import argparse +import json +import time +from pathlib import Path + +import numpy as np + + +def load_queries(path: Path, limit: int | None) -> list[str]: + out: list[str] = [] + with open(path, encoding="utf-8") as f: + for line in f: + obj = json.loads(line) + out.append(obj["query"]) + if limit and len(out) >= limit: + break + return out + + +def main() -> None: + ap = argparse.ArgumentParser( + description="DiskANN baseline on real NQ queries (search-only timing)" + ) + ap.add_argument( + "--index-dir", + default="benchmarks/data/indices/diskann_rpj_wiki", + help="Directory containing DiskANN files", + ) + ap.add_argument("--index-prefix", default="ann") + ap.add_argument("--queries-file", default="benchmarks/data/queries/nq_open.jsonl") + ap.add_argument("--num-queries", type=int, default=200) + ap.add_argument("--top-k", type=int, default=10) + ap.add_argument("--complexity", type=int, default=62) + ap.add_argument("--threads", type=int, default=1) + ap.add_argument("--beam-width", type=int, default=1) + ap.add_argument("--cache-mechanism", type=int, default=2) + ap.add_argument("--num-nodes-to-cache", type=int, default=0) + args = ap.parse_args() + + index_dir = Path(args.index_dir).resolve() + if not index_dir.is_dir(): + raise SystemExit(f"Index dir not found: {index_dir}") + + qpath = Path(args.queries_file).resolve() + if not qpath.exists(): + raise SystemExit(f"Queries file not found: {qpath}") + + queries = load_queries(qpath, args.num_queries) + print(f"Loaded {len(queries)} queries from {qpath}") + + # Compute embeddings once (exclude from timing) + from leann.api import compute_embeddings as _compute + + embs = _compute( + queries, + model_name="facebook/contriever-msmarco", + mode="sentence-transformers", + use_server=False, + ).astype(np.float32) + if embs.ndim != 2: + raise SystemExit("Embedding compute failed or returned wrong shape") + + # Build searcher + from leann_backend_diskann.diskann_backend import DiskannSearcher as _DiskannSearcher + + index_prefix_path = str(index_dir / args.index_prefix) + searcher = _DiskannSearcher( + index_prefix_path, + num_threads=int(args.threads), + cache_mechanism=int(args.cache_mechanism), + num_nodes_to_cache=int(args.num_nodes_to_cache), + ) + + # Warmup (not timed) + _ = searcher.search( + embs[0:1], + top_k=args.top_k, + complexity=args.complexity, + beam_width=args.beam_width, + prune_ratio=0.0, + recompute_embeddings=False, + batch_recompute=False, + dedup_node_dis=False, + ) + + # Timed loop + times: list[float] = [] + for i in range(embs.shape[0]): + t0 = time.time() + _ = searcher.search( + embs[i : i + 1], + top_k=args.top_k, + complexity=args.complexity, + beam_width=args.beam_width, + prune_ratio=0.0, + recompute_embeddings=False, + batch_recompute=False, + dedup_node_dis=False, + ) + times.append(time.time() - t0) + + times_sorted = sorted(times) + avg = float(sum(times) / len(times)) + p50 = times_sorted[len(times) // 2] + p95 = times_sorted[max(0, int(len(times) * 0.95) - 1)] + + print("\nDiskANN (NQ, search-only) Report") + print(f" queries: {len(times)}") + print( + f" k: {args.top_k}, complexity: {args.complexity}, beam_width: {args.beam_width}, threads: {args.threads}" + ) + print(f" avg per query: {avg:.6f} s") + print(f" p50/p95: {p50:.6f}/{p95:.6f} s") + print(f" QPS: {1.0 / avg:.2f}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/compare_faiss_vs_leann.py b/benchmarks/compare_faiss_vs_leann.py new file mode 100644 index 0000000..03cf508 --- /dev/null +++ b/benchmarks/compare_faiss_vs_leann.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +""" +Memory comparison between Faiss HNSW and LEANN HNSW backend +""" + +import gc +import logging +import os +import subprocess +import sys +import time +from pathlib import Path + +import psutil +from llama_index.core.node_parser import SentenceSplitter + +# Setup logging +logging.basicConfig(stream=sys.stdout, level=logging.INFO) +logger = logging.getLogger(__name__) + + +def get_memory_usage(): + """Get current memory usage in MB""" + process = psutil.Process() + return process.memory_info().rss / 1024 / 1024 + + +def print_memory_stats(stage: str, start_mem: float): + """Print memory statistics""" + current_mem = get_memory_usage() + diff = current_mem - start_mem + print(f"[{stage}] Memory: {current_mem:.1f} MB (+{diff:.1f} MB)") + return current_mem + + +class MemoryTracker: + def __init__(self, name: str): + self.name = name + self.start_mem = get_memory_usage() + self.stages = [] + + def checkpoint(self, stage: str): + current_mem = print_memory_stats(f"{self.name} - {stage}", self.start_mem) + self.stages.append((stage, current_mem)) + return current_mem + + def summary(self): + print(f"\n=== {self.name} Memory Summary ===") + for stage, mem in self.stages: + print(f"{stage}: {mem:.1f} MB") + peak_mem = max(mem for _, mem in self.stages) + print(f"Peak Memory: {peak_mem:.1f} MB") + print(f"Total Memory Increase: {peak_mem - self.start_mem:.1f} MB") + return peak_mem + + +def test_faiss_hnsw(): + """Test Faiss HNSW Vector Store in subprocess""" + print("\n" + "=" * 50) + print("TESTING FAISS HNSW VECTOR STORE") + print("=" * 50) + + try: + result = subprocess.run( + [sys.executable, "benchmarks/faiss_only.py"], + capture_output=True, + text=True, + timeout=300, + ) + + print(result.stdout) + if result.stderr: + print("Stderr:", result.stderr) + + if result.returncode != 0: + return { + "peak_memory": float("inf"), + "error": f"Process failed with code {result.returncode}", + } + + # Parse peak memory from output + lines = result.stdout.split("\n") + peak_memory = 0.0 + + for line in lines: + if "Peak Memory:" in line: + peak_memory = float(line.split("Peak Memory:")[1].split("MB")[0].strip()) + + return {"peak_memory": peak_memory} + + except Exception as e: + return { + "peak_memory": float("inf"), + "error": str(e), + } + + +def test_leann_hnsw(): + """Test LEANN HNSW Search Memory (load existing index)""" + print("\n" + "=" * 50) + print("TESTING LEANN HNSW SEARCH MEMORY") + print("=" * 50) + + tracker = MemoryTracker("LEANN HNSW Search") + + # Import and setup + tracker.checkpoint("Initial") + + from leann.api import LeannSearcher + + tracker.checkpoint("After imports") + + from leann.api import LeannBuilder + from llama_index.core import SimpleDirectoryReader + + # Load and parse documents + documents = SimpleDirectoryReader( + "data", + recursive=True, + encoding="utf-8", + required_exts=[".pdf", ".txt", ".md"], + ).load_data() + + tracker.checkpoint("After document loading") + + # Parse into chunks + node_parser = SentenceSplitter( + chunk_size=256, chunk_overlap=20, separator=" ", paragraph_separator="\n\n" + ) + + all_texts = [] + for doc in documents: + nodes = node_parser.get_nodes_from_documents([doc]) + for node in nodes: + all_texts.append(node.get_content()) + print(f"Total number of chunks: {len(all_texts)}") + + tracker.checkpoint("After text chunking") + + # Build LEANN index + INDEX_DIR = Path("./test_leann_comparison") + INDEX_PATH = str(INDEX_DIR / "comparison.leann") + + # Check if index already exists + if os.path.exists(INDEX_PATH + ".meta.json"): + print("Loading existing LEANN HNSW index...") + tracker.checkpoint("After loading existing index") + else: + print("Building new LEANN HNSW index...") + # Clean up previous index + import shutil + + if INDEX_DIR.exists(): + shutil.rmtree(INDEX_DIR) + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + graph_degree=32, + complexity=64, + is_compact=True, + is_recompute=True, + num_threads=1, + ) + + tracker.checkpoint("After builder setup") + + print("Building LEANN HNSW index...") + + for chunk_text in all_texts: + builder.add_text(chunk_text) + + builder.build_index(INDEX_PATH) + del builder + gc.collect() + + tracker.checkpoint("After index building") + + # Find existing LEANN index + index_paths = [ + "./test_leann_comparison/comparison.leann", + ] + index_path = None + for path in index_paths: + if os.path.exists(path + ".meta.json"): + index_path = path + break + + if not index_path: + print("❌ LEANN index not found. Please build it first") + return {"peak_memory": float("inf"), "error": "Index not found"} + + # Measure runtime memory overhead + print("\nMeasuring runtime memory overhead...") + runtime_start_mem = get_memory_usage() + print(f"Before load memory: {runtime_start_mem:.1f} MB") + tracker.checkpoint("Before load memory") + + # Load searcher + searcher = LeannSearcher(index_path) + tracker.checkpoint("After searcher loading") + + print("Running search queries...") + queries = [ + "δ»€δΉˆζ˜―η›˜ε€ε€§ζ¨‘εž‹δ»₯εŠη›˜ε€εΌ€ε‘θΏ‡η¨‹δΈ­ι‡εˆ°δΊ†δ»€δΉˆι˜΄ζš—ι’,δ»»εŠ‘δ»€δΈ€θˆ¬εœ¨δ»€δΉˆεŸŽεΈ‚ι’ε‘", + "What is LEANN and how does it work?", + "εŽδΈΊθ―ΊδΊšζ–ΉθˆŸεžιͺŒε€ηš„主要研穢内εΉ", + ] + + for i, query in enumerate(queries): + start_time = time.time() + # Use same parameters as Faiss: top_k=20, ef=120 (complexity parameter) + _ = searcher.search(query, top_k=20, ef=120) + query_time = time.time() - start_time + print(f"Query {i + 1} time: {query_time:.3f}s") + tracker.checkpoint(f"After query {i + 1}") + + runtime_end_mem = get_memory_usage() + runtime_overhead = runtime_end_mem - runtime_start_mem + + peak_memory = tracker.summary() + print(f"Runtime Memory Overhead: {runtime_overhead:.1f} MB") + + # Get storage size before cleanup + storage_size = 0 + INDEX_DIR = Path(index_path).parent + if INDEX_DIR.exists(): + total_size = 0 + for dirpath, _, filenames in os.walk(str(INDEX_DIR)): + for filename in filenames: + # Only count actual index files, skip text data and backups + if filename.endswith((".old", ".tmp", ".bak", ".jsonl", ".json")): + continue + # Count .index, .idx, .map files (actual index structures) + if filename.endswith((".index", ".idx", ".map")): + filepath = os.path.join(dirpath, filename) + total_size += os.path.getsize(filepath) + storage_size = total_size / (1024 * 1024) # Convert to MB + + # Clean up + del searcher + gc.collect() + + return { + "peak_memory": peak_memory, + "storage_size": storage_size, + } + + +def main(): + """Run comparison tests""" + print("Storage + Search Memory Comparison: Faiss HNSW vs LEANN HNSW") + print("=" * 60) + + # Test Faiss HNSW + faiss_results = test_faiss_hnsw() + + # Force garbage collection + gc.collect() + time.sleep(2) + + # Test LEANN HNSW + leann_results = test_leann_hnsw() + + # Final comparison + print("\n" + "=" * 60) + print("STORAGE + SEARCH MEMORY COMPARISON") + print("=" * 60) + + # Get storage sizes + faiss_storage_size = 0 + leann_storage_size = leann_results.get("storage_size", 0) + + # Get Faiss storage size using Python + if os.path.exists("./storage_faiss"): + total_size = 0 + for dirpath, _, filenames in os.walk("./storage_faiss"): + for filename in filenames: + filepath = os.path.join(dirpath, filename) + total_size += os.path.getsize(filepath) + faiss_storage_size = total_size / (1024 * 1024) # Convert to MB + + print("Faiss HNSW:") + if "error" in faiss_results: + print(f" ❌ Failed: {faiss_results['error']}") + else: + print(f" Search Memory: {faiss_results['peak_memory']:.1f} MB") + print(f" Storage Size: {faiss_storage_size:.1f} MB") + + print("\nLEANN HNSW:") + if "error" in leann_results: + print(f" ❌ Failed: {leann_results['error']}") + else: + print(f" Search Memory: {leann_results['peak_memory']:.1f} MB") + print(f" Storage Size: {leann_storage_size:.1f} MB") + + # Calculate improvements only if both tests succeeded + if "error" not in faiss_results and "error" not in leann_results: + memory_ratio = faiss_results["peak_memory"] / leann_results["peak_memory"] + + print("\nLEANN vs Faiss Performance:") + memory_saving = faiss_results["peak_memory"] - leann_results["peak_memory"] + print(f" Search Memory: {memory_ratio:.1f}x less ({memory_saving:.1f} MB saved)") + + # Storage comparison + if leann_storage_size > faiss_storage_size: + storage_ratio = leann_storage_size / faiss_storage_size + print(f" Storage Size: {storage_ratio:.1f}x larger (LEANN uses more storage)") + elif faiss_storage_size > leann_storage_size: + storage_ratio = faiss_storage_size / leann_storage_size + print(f" Storage Size: {storage_ratio:.1f}x smaller (LEANN uses less storage)") + else: + print(" Storage Size: similar") + else: + if "error" not in leann_results: + print("\nβœ… LEANN HNSW completed successfully!") + print(f"πŸ“Š Search Memory: {leann_results['peak_memory']:.1f} MB") + print(f"πŸ“Š Storage Size: {leann_storage_size:.1f} MB") + if "error" not in faiss_results: + print("\nβœ… Faiss HNSW completed successfully!") + print(f"πŸ“Š Search Memory: {faiss_results['peak_memory']:.1f} MB") + print(f"πŸ“Š Storage Size: {faiss_storage_size:.1f} MB") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/contextbench/.gitignore b/benchmarks/contextbench/.gitignore new file mode 100644 index 0000000..e914675 --- /dev/null +++ b/benchmarks/contextbench/.gitignore @@ -0,0 +1,25 @@ +.DS_Store +__pycache__/ +*.py[cod] + +.venv/ +.eval-venv/ +.mitmproxy-venv/ + +.leann/ +scripts/.leann/ +scripts/contextbench_eval_repos/ +scripts/contextbench_work_dir_claude/ +scripts/contextbench_work_dir_claude_overlap160/ +scripts/scripts/ + +logs/ +traces/ +*.log +*.jsonl +eval_report.json +contextbench_accuracy_report.json +prepare_repos_with_leann_failures.jsonl +task_metrics.jsonl +task_session_usage.log +all_predictions*.jsonl diff --git a/benchmarks/contextbench/README.md b/benchmarks/contextbench/README.md new file mode 100644 index 0000000..439ef2c --- /dev/null +++ b/benchmarks/contextbench/README.md @@ -0,0 +1,103 @@ +# ContextBench LEANN Runner + +This directory keeps a small local runner around the upstream ContextBench repo. + +## Kept Files + +- `contextbench_official_repo/`: upstream ContextBench code and data. +- `scripts/*.py`: local preparation, run, and evaluation scripts. +- `mitmproxy_addons/trace_recorder.py`: HTTP trace recorder used while Claude runs. +- `requirements-run.txt`: extra Python dependencies for these local scripts. + +Generated directories such as `.venv/`, `.mitmproxy-venv/`, `traces/`, +`logs/`, `scripts/contextbench_work_dir_*`, and `scripts/contextbench_eval_repos/` +can be deleted and regenerated. + +## 1. Create Python Environment + +Run from this directory: + +```bash +python3.11 -m venv .venv +source .venv/bin/activate +pip install -r contextbench_official_repo/requirements.txt +pip install -r requirements-run.txt +``` + +## 2. Install Runtime CLIs + +Install LEANN: + +```bash +uv tool install leann-core --with leann +``` + +Install `mitmdump` in a separate environment: + +```bash +python3.11 -m venv .mitmproxy-venv +.mitmproxy-venv/bin/python -m pip install mitmproxy +``` + +The run script also expects: + +- `claude` CLI available on `PATH`. +- Node/npm available for `npx ccusage`. +- A Claude login session or `ANTHROPIC_API_KEY` in the environment. +- If using LEANN MCP mode, a Claude MCP server named `leann-server` or + `LEANN_MCP_SERVER`/`CLAUDE_MCP_CONFIG_PATH` configured accordingly. + +## 3. Prepare Repos And LEANN Indexes + +```bash +cd scripts +WORK_ROOT=contextbench_work_dir_claude python prepare_repos_with_leann.py +``` + +## 4. Run Selected Tasks + +```bash +cd scripts +LEANN_ENABLED=1 \ +WORK_ROOT=contextbench_work_dir_claude \ +OUTPUT_FILE=all_predictions_claude.jsonl \ +python batch_run_selected.py +``` + +Run without LEANN: + +```bash +LEANN_ENABLED=0 \ +WORK_ROOT=contextbench_work_dir_claude \ +OUTPUT_FILE=all_predictions_claude_baseline.jsonl \ +python batch_run_selected.py +``` + +Run specific IDs without editing the script: + +```bash +SELECTED_IDS=id1,id2 python batch_run_selected.py +``` + +## 5. Evaluate Results + +Context retrieval metrics: + +```bash +cd ".../contextbench_official_repo" + +PYTHONPATH=. python -m contextbench.evaluate \ + --gold data/full.parquet \ + --pred "../scripts/all_predictions_claude.jsonl" \ + --cache "../scripts/contextbench_eval_repos" \ + --out "../scripts/contextbench_official_eval_claude.jsonl" \ + 2>&1 | tee "../scripts/contextbench_official_eval_claude.log" +``` + +## 6. Clean Generated Files + +```bash +rm -rf .venv .mitmproxy-venv .eval-venv .leann .pycache_tmp logs traces +rm -rf scripts/.leann scripts/scripts +rm -rf scripts/contextbench_eval_repos scripts/contextbench_work_dir_claude scripts/contextbench_work_dir_claude_overlap160 +``` diff --git a/benchmarks/contextbench/mitmproxy_addons/trace_recorder.py b/benchmarks/contextbench/mitmproxy_addons/trace_recorder.py new file mode 100644 index 0000000..942bdfd --- /dev/null +++ b/benchmarks/contextbench/mitmproxy_addons/trace_recorder.py @@ -0,0 +1,72 @@ +import json +import os +import time +from pathlib import Path + +from mitmproxy import http + +TASK_ID = os.environ.get("TASK_INSTANCE", "unknown_task") +TRACE_DIR = Path( + os.environ.get("TRACE_DIR", Path(__file__).resolve().parents[1] / "traces" / "raw") +) +TRACE_DIR.mkdir(parents=True, exist_ok=True) +HOST_FILTER = os.environ.get("TRACE_HOST_FILTER", "").strip().lower() + +OUTPUT_FILE = TRACE_DIR / f"{TASK_ID}_trace.jsonl" + + +def _should_record(flow: http.HTTPFlow) -> bool: + host = (flow.request.host or "").lower() + + # filter out telemetry traffic + if "statsig" in host: + return False + + if HOST_FILTER and HOST_FILTER not in host: + return False + + return True + + +def request(flow: http.HTTPFlow): + if "statsig" in flow.request.pretty_host: + flow.response = http.Response.make(204) + return + + +def response(flow: http.HTTPFlow): + should = _should_record(flow) + + if not should: + return + + if flow.response and flow.response.stream: + flow.response.stream = False + + try: + entry = { + "task_id": TASK_ID, + "timestamp": time.time(), + "id": flow.id, + "request": { + "method": flow.request.method, + "url": flow.request.pretty_url, + "headers": dict(flow.request.headers), + "text": flow.request.get_text(strict=False) or "", + }, + "response": { + "status_code": flow.response.status_code if flow.response else None, + "headers": dict(flow.response.headers) if flow.response else {}, + "text": flow.response.get_text(strict=False) if flow.response else "", + }, + } + + with open(OUTPUT_FILE, "a", encoding="utf-8") as f: + f.write(json.dumps(entry, ensure_ascii=False) + "\n") + + except Exception as e: + print(f"DEBUG: Error recording trace: {e}") + import traceback + + traceback.print_exc() + pass diff --git a/benchmarks/contextbench/scripts/auto_run.py b/benchmarks/contextbench/scripts/auto_run.py new file mode 100644 index 0000000..7a456e8 --- /dev/null +++ b/benchmarks/contextbench/scripts/auto_run.py @@ -0,0 +1,1034 @@ +import json +import os +import re +import shutil +import signal +import socket +import subprocess +import time +from pathlib import Path +from typing import Optional + +import pexpect +from datasets import load_dataset +from git import Repo + +DATASET_NAME = os.environ.get("DATASET_NAME", "Contextbench/ContextBench") +DATASET_SPLIT = os.environ.get("DATASET_SPLIT", "train") + + +# Parse integer environment variables with validation and defaults. +def _get_int_env(name: str, default: int, min_value: int = 1) -> int: + raw = os.environ.get(name, "").strip() + if not raw: + return default + try: + value = int(raw) + except ValueError: + print(f"⚠️ Invalid {name}={raw!r}; falling back to {default}") + return default + if value < min_value: + print(f"⚠️ {name} must be >= {min_value}; falling back to {default}") + return default + return value + + +LEANN_TOP_K = _get_int_env("LEANN_TOP_K", 5, min_value=1) +USAGE_LOG_FILE = os.environ.get("USAGE_LOG_FILE", "task_session_usage.log").strip() +TASK_METRICS_FILE = os.environ.get("TASK_METRICS_FILE", "task_metrics.jsonl").strip() +PREFETCH_STRICT = os.environ.get("PREFETCH_STRICT", "1").strip() != "0" + + +# Get uncommitted changes compared to the current HEAD commit. +def get_git_diff(repo_dir: Path) -> str: + try: + repo = Repo(repo_dir) + return repo.git.diff(repo.head.commit) + except Exception as e: + print(f"⚠️ Diff Error: {e}") + return "" + + +# Prepare repository state for one ContextBench task instance. +def setup_task_environment( + instance_id: str, repo_url: str, work_root: Path, task: Optional[dict] = None +) -> Path: + print(f"πŸš€ [1/4] Preparing environment: {instance_id}") + if not task: + ds = load_dataset(DATASET_NAME, split=DATASET_SPLIT) + task = next((x for x in ds if x["instance_id"] == instance_id), None) + if not task: + raise RuntimeError(f"Instance {instance_id} not found in dataset.") + + target_dir = work_root / instance_id + if not target_dir.exists(): + Repo.clone_from(repo_url, target_dir) + + repo = Repo(target_dir) + repo.git.reset("--hard") + # Preserve LEANN indexes if present. + repo.git.clean("-fdx", "-e", ".leann/") + repo.git.checkout(task["base_commit"]) + (target_dir / "PROBLEM.md").write_text(task["problem_statement"], encoding="utf-8") + return target_dir + + +# Pre-download all task repositories before model execution starts. +def prefetch_task_repositories(tasks: list[dict], work_root: Path) -> None: + if not tasks: + print("πŸ“¦ [Prefetch] No tasks to download.") + return + + print(f"πŸ“¦ [Prefetch] Downloading repositories for {len(tasks)} tasks...") + failures: list[str] = [] + + for i, task in enumerate(tasks, start=1): + instance_id = task.get("instance_id", "").strip() + repo_url = task.get("repo_url", "").strip() + target_dir = work_root / instance_id + + if not instance_id or not repo_url: + failures.append(f"task[{i}] missing instance_id/repo_url") + print(f" [{i}/{len(tasks)}] ❌ Invalid task metadata; skipping.") + continue + + if target_dir.exists(): + # A previous interrupted clone can leave a broken .git dir. + # Treat only repos with a valid HEAD as reusable. + try: + repo = Repo(target_dir) + repo.git.rev_parse("HEAD") + print(f" [{i}/{len(tasks)}] βœ… Already present: {instance_id}") + continue + except Exception: + print(f" [{i}/{len(tasks)}] ♻️ Found incomplete repo, re-cloning: {instance_id}") + shutil.rmtree(target_dir, ignore_errors=True) + + cloned = False + last_error: Optional[Exception] = None + for attempt in range(1, 3 + 1): + try: + print(f" [{i}/{len(tasks)}] ⬇️ Cloning: {instance_id} (attempt {attempt}/3)") + Repo.clone_from(repo_url, target_dir) + print(f" [{i}/{len(tasks)}] βœ… Cloned: {instance_id}") + cloned = True + break + except Exception as e: + last_error = e + print(f" [{i}/{len(tasks)}] ❌ Clone attempt failed: {instance_id} -> {e}") + shutil.rmtree(target_dir, ignore_errors=True) + if attempt < 3: + sleep_s = 4 * attempt + print(f" [{i}/{len(tasks)}] ⏳ Retrying in {sleep_s}s...") + time.sleep(sleep_s) + + if not cloned: + failures.append(f"{instance_id} ({last_error})") + + if failures: + sample = ", ".join(failures[:3]) + extra = f" (+{len(failures) - 3} more)" if len(failures) > 3 else "" + message = f"Repository prefetch failed for {len(failures)} task(s): {sample}{extra}" + if PREFETCH_STRICT: + raise RuntimeError(message) + print(f"⚠️ {message}") + print("⚠️ PREFETCH_STRICT=0; continuing with available repositories.") + print("⚠️ [Prefetch] Completed with partial failures.") + return + + print("βœ… [Prefetch] All repositories are ready.") + + +# Wait until a local TCP port starts accepting connections. +def wait_for_port(port: int, timeout: int = 10): + start = time.time() + while time.time() - start < timeout: + try: + with socket.create_connection(("127.0.0.1", port), timeout=0.2): + return + except OSError: + time.sleep(0.1) + print(f"⚠️ Warning: Proxy port {port} did not open in {timeout}s") + + +# Resolve mitmproxy CA cert path for Python HTTPS clients behind proxy. +def _resolve_mitm_ca_cert() -> Optional[Path]: + env_cert = os.environ.get("MITM_CA_CERT_PATH", "").strip() + candidates = [] + if env_cert: + candidates.append(Path(env_cert).expanduser()) + candidates.extend( + [ + Path.home() / ".mitmproxy" / "mitmproxy-ca-cert.pem", + Path.home() / ".mitmproxy" / "mitmproxy-ca-cert.cer", + ] + ) + for cert_path in candidates: + if cert_path.exists(): + return cert_path + return None + + +def _resolve_mitmdump_bin() -> str: + env_bin = os.environ.get("MITMDUMP_BIN", "").strip() + if env_bin: + return env_bin + + # Prefer a dedicated local mitmproxy venv to avoid dependency conflicts + # with the main project environment. + project_root = Path(__file__).resolve().parents[1] + local_bin = project_root / ".mitmproxy-venv" / "bin" / "mitmdump" + if local_bin.exists(): + return str(local_bin) + + return "mitmdump" + + +# Start mitmproxy traffic recorder for the current task. +def start_mitmproxy(instance_id: str, mitm_script_path: str) -> subprocess.Popen: + print("πŸ•΅οΈ [2/4] Starting Traffic Recorder...") + env = os.environ.copy() + env["TASK_INSTANCE"] = instance_id + mitmdump_bin = _resolve_mitmdump_bin() + cmd = [mitmdump_bin, "--no-http2", "-s", mitm_script_path, "-q"] + try: + process = subprocess.Popen(cmd, env=env, preexec_fn=os.setsid) + except FileNotFoundError as exc: + raise RuntimeError( + "mitmdump not found. Install mitmproxy in a dedicated env with:\n" + " python3 -m venv .mitmproxy-venv\n" + " .mitmproxy-venv/bin/python -m pip install mitmproxy\n" + "or set MITMDUMP_BIN to an existing mitmdump path." + ) from exc + wait_for_port(8080) + return process + + +# Check whether a server name exists in dict/list MCP server containers. +def _server_in_container(servers_obj, server_name: str) -> bool: + if isinstance(servers_obj, dict): + return server_name in servers_obj + if isinstance(servers_obj, list): + for item in servers_obj: + if isinstance(item, dict) and item.get("name") == server_name: + return True + return False + + +# Recursively scan a config tree for a specific MCP server entry. +def _has_mcp_server_config(node, server_name: str, parent_key: str = "") -> bool: + if isinstance(node, dict): + for key, value in node.items(): + key_l = key.lower() + if key_l in {"mcpservers", "mcp_servers"} and _server_in_container(value, server_name): + return True + if ( + parent_key.lower() == "mcp" + and key_l == "servers" + and _server_in_container(value, server_name) + ): + return True + if _has_mcp_server_config(value, server_name, key): + return True + elif isinstance(node, list): + for item in node: + if _has_mcp_server_config(item, server_name, parent_key): + return True + return False + + +# Resolve Claude settings path that defines the required MCP server. +def resolve_claude_mcp_config(server_name: str) -> Optional[Path]: + env_path = os.environ.get("CLAUDE_MCP_CONFIG_PATH", "").strip() + candidates = [] + if env_path: + candidates.append(Path(env_path).expanduser()) + candidates.extend( + [ + Path.home() / ".claude" / "settings.json", + Path.home() / ".config" / "claude" / "settings.json", + Path.home() / ".config" / "claude-code" / "settings.json", + Path.home() / ".claude.json", + ] + ) + + seen = set() + for cfg_path in candidates: + cfg_path = cfg_path.resolve() + if str(cfg_path) in seen: + continue + seen.add(str(cfg_path)) + if not cfg_path.exists(): + continue + try: + data = json.loads(cfg_path.read_text(encoding="utf-8")) + except Exception: + continue + if _has_mcp_server_config(data, server_name): + return cfg_path + return None + + +def _normalize_mcp_servers(servers_obj) -> dict[str, dict]: + servers: dict[str, dict] = {} + if isinstance(servers_obj, dict): + for name, config in servers_obj.items(): + if isinstance(name, str) and isinstance(config, dict): + servers[name] = config + elif isinstance(servers_obj, list): + for item in servers_obj: + if not isinstance(item, dict): + continue + name = item.get("name") + if not isinstance(name, str) or not name: + continue + config = dict(item) + config.pop("name", None) + servers[name] = config + return servers + + +def _extract_mcp_servers(node, parent_key: str = "") -> dict[str, dict]: + servers: dict[str, dict] = {} + if isinstance(node, dict): + for key, value in node.items(): + key_l = key.lower() + if key_l in {"mcpservers", "mcp_servers"}: + servers.update(_normalize_mcp_servers(value)) + continue + if parent_key.lower() == "mcp" and key_l == "servers": + servers.update(_normalize_mcp_servers(value)) + continue + servers.update(_extract_mcp_servers(value, key)) + elif isinstance(node, list): + for item in node: + servers.update(_extract_mcp_servers(item, parent_key)) + return servers + + +def build_strict_mcp_config_without_server(server_name: str) -> Optional[str]: + cfg_path = resolve_claude_mcp_config(server_name) + if not cfg_path: + return None + try: + data = json.loads(cfg_path.read_text(encoding="utf-8")) + except Exception: + return None + servers = _extract_mcp_servers(data) + servers.pop(server_name, None) + return json.dumps({"mcpServers": servers}, separators=(",", ":")) + + +# Decide whether LEANN/MCP integration is available for this task repo. +def resolve_leann_integration(target_dir: Path) -> dict[str, str]: + leann_enabled = os.environ.get("LEANN_ENABLED", "1") != "0" + use_mcp = os.environ.get("LEANN_USE_MCP", "1") != "0" + mcp_server_name = os.environ.get("LEANN_MCP_SERVER", "leann-server") + leann_index_exists = (target_dir / ".leann").exists() + + mode = "none" + if leann_enabled and leann_index_exists: + if use_mcp: + mode = "mcp" + print( + f" -> πŸ” LEANN MCP enabled (server: {mcp_server_name}, " + f"forced top_k={LEANN_TOP_K})" + ) + else: + print(" -> ⚠️ LEANN_USE_MCP=0 but CLI mode is disabled; continuing without LEANN") + elif leann_enabled: + print(" -> ⚠️ LEANN enabled but no .leann index found; continuing without LEANN") + + print(mode) + return { + "mode": mode, + "mcp_server_name": mcp_server_name, + } + + +# Build the initial Claude prompt from PROBLEM.md plus optional LEANN hints. +def build_initial_prompt( + target_dir: Path, leann_info: dict[str, str], instance_id: str = "" +) -> str: + problem_file = target_dir / "PROBLEM.md" + try: + problem_text = problem_file.read_text(encoding="utf-8").strip() + if leann_info.get("mode") == "mcp": + leann_info.get("mcp_server_name", "leann-server") + mcp_hint = ( + f"Your LEANN index name is: {instance_id}\n" + "Use LEANN MCP as a cost-aware semantic entry-point router. " + "Before broad exploration, call leann_search once. " + "Use top_k=5 by default, top_k=10 only for clearly multi-hop tasks spanning multiple subsystems; never use top_k>10. " + "Use show_metadata=false.\n" + "After LEANN, open the top 1-3 likely implementation/source files first, preferring source files over tests/docs/examples/generated files. " + "Do not open many retrieved files just because they were returned. " + "After identifying the likely fix location, use targeted Grep to find existing callers, API/server entry points, tests, and config files that may need to be updated. " + "This targeted Grep is preferred over additional LEANN once concrete symbols, functions, file paths, config keys, or error strings are known.\n" + "Run at most one additional leann_search only if no plausible implementation entry point is found or a required subsystem is clearly missing. " + "The second query must be more specific, using concrete identifiers/literals found so far. " + "Use show_metadata=false unless metadata is necessary to disambiguate files. " + "Never run more than two leann_search calls total.\n" + ) + return f"{mcp_hint}\n\n{problem_text}" + return problem_text + except Exception as e: + raise RuntimeError(f"Failed to read problem statement from {problem_file}: {e}") + + +# Run Claude CLI autonomously in the prepared repository. +def run_claude_autonomous( + target_dir: Path, + model: str, + leann_info: Optional[dict[str, str]] = None, + instance_id: str = "", +): + print("πŸ€– [3/4] Launching Claude Code") + if (target_dir / ".claude").exists(): + shutil.rmtree(target_dir / ".claude") + + if leann_info is None: + leann_info = resolve_leann_integration(target_dir) + server_name = leann_info.get("mcp_server_name", "leann-server") + if leann_info.get("mode") == "mcp": + cfg_path = resolve_claude_mcp_config(server_name) + if not cfg_path: + raise RuntimeError( + f"LEANN_USE_MCP=1 but MCP server '{server_name}' was not found in Claude config. " + "Set CLAUDE_MCP_CONFIG_PATH or configure this server in Claude settings." + ) + print(f" -> βœ… Claude MCP config found for '{server_name}': {cfg_path}") + initial_prompt = build_initial_prompt(target_dir, leann_info, instance_id=instance_id) + + env = os.environ.copy() + env.update( + { + "HTTP_PROXY": "http://127.0.0.1:8080", + "HTTPS_PROXY": "http://127.0.0.1:8080", + "NODE_TLS_REJECT_UNAUTHORIZED": "0", + } + ) + + claude_args = ["-p", initial_prompt, "--dangerously-skip-permissions"] + if model: + claude_args.extend(["--model", model]) + if leann_info.get("mode") != "mcp": + filtered_mcp_config = build_strict_mcp_config_without_server(server_name) + if filtered_mcp_config is not None: + claude_args.extend( + [ + "--strict-mcp-config", + "--mcp-config", + filtered_mcp_config, + ] + ) + print( + f" -> 🚫 Non-LEANN mode; removed MCP server '{server_name}' from strict MCP config" + ) + + child = pexpect.spawn( + "claude", claude_args, cwd=target_dir, env=env, encoding="utf-8", timeout=1200 + ) + + try: + while True: + index = child.expect( + [r"\[y/n\]", r"Allow execution", pexpect.EOF, pexpect.TIMEOUT], timeout=5 + ) + if index in [0, 1]: + child.sendline("y") + elif index == 2: + break + except Exception as e: + print(f"❌ Interaction error: {e}") + finally: + if child.isalive(): + child.terminate(force=True) + + +# --------------------------------------------------------------------------- +# Trajectory extraction: parse mitmproxy trace β†’ ContextBench traj_data +# --------------------------------------------------------------------------- + + +def _parse_sse_tool_uses(text: str) -> list[dict]: + """Reconstruct tool_use calls from a streamed SSE response body.""" + tool_blocks: dict[int, dict] = {} # index -> {name, partial_json} + completed: list[dict] = [] + + for line in text.split("\n"): + if not line.startswith("data: "): + continue + data_str = line[6:] + try: + event = json.loads(data_str) + except Exception: + continue + + event_type = event.get("type", "") + + if event_type == "content_block_start": + idx = event.get("index", 0) + block = event.get("content_block", {}) + if block.get("type") == "tool_use": + tool_blocks[idx] = {"name": block.get("name", ""), "partial_json": ""} + + elif event_type == "content_block_delta": + idx = event.get("index", 0) + delta = event.get("delta", {}) + if idx in tool_blocks and delta.get("type") == "input_json_delta": + tool_blocks[idx]["partial_json"] += delta.get("partial_json", "") + + elif event_type == "content_block_stop": + idx = event.get("index", 0) + if idx in tool_blocks: + tool = tool_blocks.pop(idx) + try: + input_data = json.loads(tool["partial_json"]) if tool["partial_json"] else {} + except Exception: + input_data = {} + completed.append({"name": tool["name"], "input": input_data}) + + return completed + + +def _parse_json_tool_uses(text: str) -> list[dict]: + """Extract tool_use calls from a non-streamed JSON response body.""" + try: + resp = json.loads(text) + except Exception: + return [] + completed = [] + for block in resp.get("content", []): + if block.get("type") == "tool_use": + completed.append({"name": block.get("name", ""), "input": block.get("input", {})}) + return completed + + +def _make_relative(file_path: str, target_dir: Path) -> str: + """Convert an absolute file path to one relative to target_dir.""" + try: + return str(Path(file_path).relative_to(target_dir.resolve())) + except ValueError: + return file_path + + +def _is_leann_search_tool(name: str) -> bool: + return bool(name) and ( + name == "leann_search" or name.endswith("__leann_search") or "leann_search" in name + ) + + +def _extract_trace_artifacts( + trace_path: Path, + target_dir: Path, + since_timestamp: Optional[float] = None, +) -> dict: + """ + Parse the mitmproxy JSONL trace once and derive both ContextBench trajectory data + and LEANN usage statistics. + + Each Anthropic /messages response that contains tool_use calls becomes one pred_step. + Read calls with line offsets are mapped to line spans; other file-touching tool calls + (Glob, Grep, Bash cat/head) contribute to pred_files only. + """ + pred_steps: list[dict] = [] + leann_search_calls = 0 + + if not trace_path.exists(): + print(f"⚠️ Trace file not found: {trace_path}") + return { + "traj_data": _empty_traj_data(), + "leann_tool_used": False, + "leann_search_calls": 0, + } + + with open(trace_path, encoding="utf-8") as f: + for raw_line in f: + try: + entry = json.loads(raw_line) + except Exception: + continue + + ts = entry.get("timestamp") + if ( + since_timestamp is not None + and isinstance(ts, (int, float)) + and ts < since_timestamp + ): + continue + + url = entry.get("request", {}).get("url", "") + if "anthropic.com" not in url or "/messages" not in url: + continue + + response_text = entry.get("response", {}).get("text", "") or "" + if not response_text: + continue + + # Support both streaming (SSE) and non-streaming responses. + if "data: " in response_text: + tool_uses = _parse_sse_tool_uses(response_text) + else: + tool_uses = _parse_json_tool_uses(response_text) + + if not tool_uses: + continue + + step_files: list[str] = [] + step_spans: dict[str, list[dict]] = {} + + for tool in tool_uses: + name = tool.get("name", "") + inp = tool.get("input", {}) or {} + + if _is_leann_search_tool(name): + leann_search_calls += 1 + + if name == "Read": + raw_path = inp.get("file_path", "") + if not raw_path: + continue + rel = _make_relative(raw_path, target_dir) + if rel not in step_files: + step_files.append(rel) + offset = inp.get("offset") + limit = inp.get("limit") + if offset is not None: + start = int(offset) + end = start + int(limit) if limit is not None else start + 2000 + step_spans.setdefault(rel, []).append({"start": start, "end": end}) + + elif name in ("Grep", "Glob"): + # These don't return exact spans; record any explicit path argument. + raw_path = inp.get("path", "") + if raw_path and not raw_path.endswith(("*", "/")): + rel = _make_relative(raw_path, target_dir) + if rel not in step_files: + step_files.append(rel) + + elif name == "Bash": + # Heuristic: detect `cat`, `head`, `tail` calls on files. + cmd = inp.get("command", "") or "" + for m in re.finditer( + r"(?:cat|head|tail|sed|awk)\s+(?:-[^\s]+\s+)*([^\s|><;]+)", cmd + ): + candidate = m.group(1) + if "/" in candidate or candidate.endswith( + (".py", ".go", ".ts", ".js", ".java", ".c", ".cpp", ".rs") + ): + rel = _make_relative(candidate, target_dir) + if rel not in step_files: + step_files.append(rel) + + if step_files: + pred_steps.append( + { + "files": step_files, + "spans": step_spans, + "symbols": {}, + } + ) + + # Aggregate across all steps. + all_files: list[str] = [] + all_spans: dict[str, list[dict]] = {} + for step in pred_steps: + for f in step["files"]: + if f not in all_files: + all_files.append(f) + for f, spans in step["spans"].items(): + all_spans.setdefault(f, []).extend(spans) + + return { + "traj_data": { + "pred_steps": pred_steps, + "pred_files": all_files, + "pred_spans": all_spans, + "pred_symbols": {}, + }, + "leann_tool_used": leann_search_calls > 0, + "leann_search_calls": leann_search_calls, + } + + +def extract_trajectory_from_traces( + trace_path: Path, + target_dir: Path, + since_timestamp: Optional[float] = None, +) -> dict: + return _extract_trace_artifacts( + trace_path, + target_dir, + since_timestamp=since_timestamp, + )["traj_data"] + + +def detect_messages_api_error( + trace_path: Path, + since_timestamp: Optional[float] = None, +) -> Optional[str]: + """Return a concise API error message if /v1/messages requests failed.""" + if not trace_path.exists(): + return None + try: + with open(trace_path, encoding="utf-8") as f: + for raw_line in f: + try: + entry = json.loads(raw_line) + except Exception: + continue + ts = entry.get("timestamp") + if ( + since_timestamp is not None + and isinstance(ts, (int, float)) + and ts < since_timestamp + ): + continue + + url = entry.get("request", {}).get("url", "") + if "/v1/messages" not in url: + continue + response = entry.get("response", {}) or {} + status_code = response.get("status_code") + if not isinstance(status_code, int) or status_code < 400: + continue + text = response.get("text", "") or "" + err_type = "" + err_message = "" + try: + payload = json.loads(text) + error_obj = payload.get("error", {}) if isinstance(payload, dict) else {} + if isinstance(error_obj, dict): + err_type = str(error_obj.get("type", "") or "") + err_message = str(error_obj.get("message", "") or "") + except Exception: + pass + detail = f"/v1/messages returned HTTP {status_code}" + if err_type: + detail += f" ({err_type})" + if err_message: + detail += f": {err_message}" + return detail + except Exception: + return None + return None + + +def _empty_traj_data() -> dict: + return {"pred_steps": [], "pred_files": [], "pred_spans": {}, "pred_symbols": {}} + + +# --------------------------------------------------------------------------- +# Usage tracking (mirrored from swebench runner) +# --------------------------------------------------------------------------- + + +def _blank_usage() -> dict: + return { + "input_tokens": 0, + "output_tokens": 0, + "cache_creation_tokens": 0, + "cache_read_tokens": 0, + "total_tokens": 0, + "cost_usd": 0.0, + } + + +def _normalize_usage(raw: Optional[dict]) -> dict: + base = _blank_usage() + if not isinstance(raw, dict): + return base + base["input_tokens"] = int(raw.get("inputTokens", 0) or 0) + base["output_tokens"] = int(raw.get("outputTokens", 0) or 0) + base["cache_creation_tokens"] = int(raw.get("cacheCreationTokens", 0) or 0) + base["cache_read_tokens"] = int(raw.get("cacheReadTokens", 0) or 0) + base["total_tokens"] = int(raw.get("totalTokens", 0) or 0) + base["cost_usd"] = float(raw.get("totalCost", 0) or 0.0) + return base + + +def _usage_delta(before: dict, after: dict) -> dict: + return { + "input_tokens": after["input_tokens"] - before["input_tokens"], + "output_tokens": after["output_tokens"] - before["output_tokens"], + "cache_creation_tokens": after["cache_creation_tokens"] - before["cache_creation_tokens"], + "cache_read_tokens": after["cache_read_tokens"] - before["cache_read_tokens"], + "total_tokens": after["total_tokens"] - before["total_tokens"], + "cost_usd": round(after["cost_usd"] - before["cost_usd"], 6), + } + + +def _has_positive_delta(usage: dict) -> bool: + return ( + usage["total_tokens"] > 0 + or usage["input_tokens"] > 0 + or usage["output_tokens"] > 0 + or usage["cache_creation_tokens"] > 0 + or usage["cache_read_tokens"] > 0 + or usage["cost_usd"] > 0 + ) + + +def _format_usage(usage: dict) -> str: + return ( + f"input: {usage['input_tokens']:,}, output: {usage['output_tokens']:,}, " + f"cache_read: {usage['cache_read_tokens']:,}, cache_create: {usage['cache_creation_tokens']:,} " + f"total: {usage['total_tokens']:,}, cost: ${usage['cost_usd']:.4f}" + ) + + +def append_usage_log(line: str) -> None: + if not USAGE_LOG_FILE: + return + log_path = Path(USAGE_LOG_FILE).expanduser() + try: + if log_path.parent and str(log_path.parent) != ".": + log_path.parent.mkdir(parents=True, exist_ok=True) + with log_path.open("a", encoding="utf-8") as f: + f.write(f"{line}\n") + except Exception as e: + print(f"⚠️ Failed to write usage log {log_path}: {e}") + + +def append_task_metrics(entry: dict) -> None: + if not TASK_METRICS_FILE: + return + metrics_path = Path(TASK_METRICS_FILE).expanduser() + try: + if metrics_path.parent and str(metrics_path.parent) != ".": + metrics_path.parent.mkdir(parents=True, exist_ok=True) + with metrics_path.open("a", encoding="utf-8") as f: + f.write(json.dumps(entry) + "\n") + except Exception as e: + print(f"⚠️ Failed to write task metrics {metrics_path}: {e}") + + +def get_ccusage_sessions() -> Optional[dict[str, dict]]: + try: + result = subprocess.run( + ["npx", "ccusage", "session", "--json", "--offline"], + capture_output=True, + text=True, + timeout=30, + ) + if result.returncode != 0: + stderr = (result.stderr or "").strip() + raise RuntimeError(stderr or "ccusage returned non-zero exit code") + data = json.loads(result.stdout or "{}") + sessions = data.get("sessions", []) + if not isinstance(sessions, list): + return {} + by_session: dict[str, dict] = {} + for row in sessions: + if not isinstance(row, dict): + continue + session_id = str(row.get("sessionId", "")).strip() + if not session_id: + continue + by_session[session_id] = _normalize_usage(row) + return by_session + except Exception as e: + print(f"⚠️ Failed to get ccusage sessions: {e}") + return None + + +def compute_usage_diff( + before_sessions: Optional[dict[str, dict]], + after_sessions: Optional[dict[str, dict]], + instance_id: str, +) -> Optional[dict]: + if before_sessions is None or after_sessions is None: + return None + + changed_sessions: dict[str, dict] = {} + all_session_ids = set(before_sessions.keys()) | set(after_sessions.keys()) + for session_id in all_session_ids: + before = before_sessions.get(session_id, _blank_usage()) + after = after_sessions.get(session_id, _blank_usage()) + delta = _usage_delta(before, after) + if _has_positive_delta(delta): + changed_sessions[session_id] = delta + + if not changed_sessions: + return None + + instance_lower = instance_id.lower() + subagent_ids = [ + sid for sid in changed_sessions if sid.lower() == "subagents" or "subagent" in sid.lower() + ] + non_subagent_items = [ + (sid, usage) for sid, usage in changed_sessions.items() if sid not in subagent_ids + ] + matched_non_subagent = [ + (sid, usage) + for sid, usage in non_subagent_items + if sid.lower() in instance_lower or instance_lower in sid.lower() + ] + primary_candidates = matched_non_subagent or non_subagent_items + if not primary_candidates: + return None + + primary_session_id, session_usage = max( + primary_candidates, key=lambda x: (x[1]["total_tokens"], x[1]["cost_usd"]) + ) + return { + "session_id": primary_session_id, + "input_tokens": session_usage["input_tokens"], + "output_tokens": session_usage["output_tokens"], + "cache_creation_tokens": session_usage["cache_creation_tokens"], + "cache_read_tokens": session_usage["cache_read_tokens"], + "total_tokens": session_usage["total_tokens"], + "cost_usd": session_usage["cost_usd"], + "session_usage": session_usage, + } + + +# --------------------------------------------------------------------------- +# Main task runner +# --------------------------------------------------------------------------- + + +def run_single_task( + instance_id: str, + repo_url: str, + work_root: Path, + mitm_script_path: str, + trace_dir: Path, + model: str = "", + task: Optional[dict] = None, +) -> tuple: + """ + Run one ContextBench instance end-to-end. + + Returns: (patch, latency_seconds, agent_seconds, traj_data, usage) + - patch : git diff string (the model's code changes) + - latency_seconds : end-to-end wall-clock seconds + - agent_seconds : Claude agent runtime only + - traj_data : ContextBench trajectory dict (pred_steps, pred_files, …) + - usage : token/cost usage dict, or None + """ + print(f"\n🌊 Starting Claude Pipeline for {instance_id}") + work_root = Path(work_root) + trace_dir = Path(trace_dir) + mitm_proc = None + start_time = time.time() + usage_before: Optional[dict[str, dict]] = None + usage: Optional[dict] = None + status = "failed" + error_message: Optional[str] = None + claude_start_ts: Optional[float] = None + claude_end_ts: Optional[float] = None + leann_mode: Optional[str] = None + leann_tool_used = False + leann_search_calls = 0 + + try: + target_dir = setup_task_environment(instance_id, repo_url, work_root, task=task) + leann_info = resolve_leann_integration(target_dir) + leann_mode = leann_info.get("mode") + mitm_proc = start_mitmproxy(instance_id, str(mitm_script_path)) + + usage_before = get_ccusage_sessions() + claude_start_ts = time.time() + run_claude_autonomous(target_dir, model, leann_info=leann_info, instance_id=instance_id) + claude_end_ts = time.time() + usage_after = get_ccusage_sessions() + + trace_path = trace_dir / f"{instance_id}_trace.jsonl" + api_error = detect_messages_api_error(trace_path, since_timestamp=claude_start_ts) + if api_error: + raise RuntimeError( + f"Claude API request failed for {instance_id}: {api_error}. " + "Set MODEL/CLAUDE_MODEL to an available model for your account." + ) + + usage = compute_usage_diff(usage_before, usage_after, instance_id=instance_id) + if usage: + task_usage_line = ( + f"πŸ’° Task session usage ({usage['session_id']}) β€” " + f"{_format_usage(usage['session_usage'])}" + ) + print(task_usage_line) + + print(f"πŸ“ [4/4] Extracting trajectory from trace: {trace_path.name}") + trace_artifacts = _extract_trace_artifacts( + trace_path, + target_dir, + since_timestamp=claude_start_ts, + ) + traj_data = trace_artifacts["traj_data"] + leann_tool_used = bool(trace_artifacts["leann_tool_used"]) + leann_search_calls = int(trace_artifacts["leann_search_calls"]) + print( + f" -> {len(traj_data['pred_steps'])} steps, " + f"{len(traj_data['pred_files'])} unique files, " + f"leann_calls={leann_search_calls}" + ) + + patch = get_git_diff(target_dir) + elapsed = time.time() - start_time + agent_elapsed = ( + max(0.0, claude_end_ts - claude_start_ts) + if claude_start_ts is not None and claude_end_ts is not None + else None + ) + print(f"⏱️ Task completed in {elapsed:.1f}s ({elapsed / 60:.1f}min)") + status = "succeeded" + return patch, elapsed, agent_elapsed, traj_data, usage + except Exception as e: + error_message = str(e) + raise + + finally: + elapsed = time.time() - start_time + agent_elapsed = None + if claude_start_ts is not None: + agent_end = claude_end_ts if claude_end_ts is not None else time.time() + agent_elapsed = max(0.0, agent_end - claude_start_ts) + if usage is None and usage_before is not None: + usage_after = get_ccusage_sessions() + usage = compute_usage_diff(usage_before, usage_after, instance_id=instance_id) + + metrics_entry: dict = { + "instance_id": instance_id, + "repo_url": repo_url, + "model": model or "cli-default", + "status": status, + "latency_seconds": round(elapsed, 1), + "timestamp_unix": int(time.time()), + "token_usage": usage, + "leann_mode": leann_mode, + "leann_tool_used": leann_tool_used, + "leann_search_calls": leann_search_calls, + } + if agent_elapsed is not None: + metrics_entry["agent_seconds"] = round(agent_elapsed, 1) + if error_message: + metrics_entry["error"] = error_message + append_task_metrics(metrics_entry) + + metric_line = ( + f"πŸ“Š Task metrics ({instance_id}) β€” status={status}, " + f"agent={agent_elapsed:.1f}s, latency={elapsed:.1f}s" + if agent_elapsed is not None + else f"πŸ“Š Task metrics ({instance_id}) β€” status={status}, latency={elapsed:.1f}s" + ) + if usage: + metric_line += ( + f", session={usage['session_id']}, {_format_usage(usage['session_usage'])}" + ) + else: + metric_line += ", token usage unavailable" + metric_line += ( + f", leann_mode={leann_mode}, " + f"leann_tool_used={str(leann_tool_used).lower()}, " + f"leann_search_calls={leann_search_calls}" + ) + if error_message: + metric_line += f", error={error_message}" + print(metric_line) + append_usage_log(metric_line) + + if mitm_proc: + os.killpg(os.getpgid(mitm_proc.pid), signal.SIGTERM) diff --git a/benchmarks/contextbench/scripts/batch_run_random.py b/benchmarks/contextbench/scripts/batch_run_random.py new file mode 100644 index 0000000..b1f1a4c --- /dev/null +++ b/benchmarks/contextbench/scripts/batch_run_random.py @@ -0,0 +1,140 @@ +import json +import os +import random +import subprocess +import time +from pathlib import Path + +from auto_run import prefetch_task_repositories +from datasets import load_dataset + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT_FILE = os.environ.get("OUTPUT_FILE", "all_predictions_claude.jsonl") +NUM_TASKS = int(os.environ.get("NUM_TASKS", "31")) +WORK_ROOT = os.environ.get("WORK_ROOT", "contextbench_work_dir_claude") +MODEL = os.environ.get("MODEL", os.environ.get("CLAUDE_MODEL", "")).strip() +DATASET_NAME = os.environ.get("DATASET_NAME", "Contextbench/ContextBench") +DATASET_SPLIT = os.environ.get("DATASET_SPLIT", "train") +# Optionally restrict to one benchmark split: Verified | Pro | Poly | Multi +BENCH_FILTER = os.environ.get("BENCH_FILTER", "Pro").strip() +# Optionally restrict random sampling to one repo (supports partial match), +# e.g. "django/django" or "sympy". +REPO_FILTER = os.environ.get("REPO_FILTER", "").strip().lower() +PREFETCH_REPOS = os.environ.get("PREFETCH_REPOS", "4").strip() != "0" +MITM_SCRIPT = ROOT / "mitmproxy_addons" / "trace_recorder.py" +TRACE_DIR = ROOT / "traces" / "raw" + + +def cleanup_residuals(): + print("🧹 Cleaning up residual processes (Claude & Mitm)...") + try: + subprocess.run(["pkill", "-f", "claude"], stderr=subprocess.DEVNULL) + subprocess.run(["pkill", "-f", "mitmdump"], stderr=subprocess.DEVNULL) + time.sleep(2) + except Exception: + pass + + +def main(): + Path(WORK_ROOT).mkdir(parents=True, exist_ok=True) + TRACE_DIR.mkdir(parents=True, exist_ok=True) + + print(f"🧠 Model: {MODEL or '(Claude CLI default)'}") + print(f"πŸ”Ž Bench filter: {BENCH_FILTER or '(all)'}") + if REPO_FILTER: + print(f"πŸ“ Repo filter: {REPO_FILTER}") + + api_key = os.environ.get("ANTHROPIC_API_KEY") + if api_key: + print(f"πŸ”‘ Using API key from environment: {api_key[:20]}...") + else: + print("πŸ” ANTHROPIC_API_KEY not set; using Claude CLI logged-in session.") + + existing_ids: set = set() + output_path = Path(OUTPUT_FILE) + if output_path.exists(): + with open(output_path) as f: + for line in f: + try: + data = json.loads(line) + existing_ids.add(data["instance_id"]) + except Exception: + continue + print(f"βœ… Found {len(existing_ids)} already completed tasks.") + + print(f"πŸ“š Loading dataset: {DATASET_NAME} ({DATASET_SPLIT})...") + ds = load_dataset(DATASET_NAME, split=DATASET_SPLIT) + + pending_tasks = [ + t + for t in ds + if t["instance_id"] not in existing_ids + and (not BENCH_FILTER or t.get("source", "") == BENCH_FILTER) + and ( + not REPO_FILTER + or REPO_FILTER in (t.get("repo", "") or "").lower() + or REPO_FILTER in (t.get("repo_url", "") or "").lower() + ) + ] + + if not pending_tasks: + print("πŸŽ‰ No pending tasks to run!") + return + + selected_tasks = random.sample(pending_tasks, min(NUM_TASKS, len(pending_tasks))) + print(f"πŸš€ Randomly selected {len(selected_tasks)} tasks to process.") + if PREFETCH_REPOS: + prefetch_task_repositories(selected_tasks, Path(WORK_ROOT)) + else: + print("⏭️ PREFETCH_REPOS=0; skipping prefetch step.") + + for i, task in enumerate(selected_tasks): + instance_id = task["instance_id"] + repo_url = task["repo_url"] + + print(f"\n{'-' * 60}") + print(f"πŸ“¦ [{i + 1}/{len(selected_tasks)}] Running: {instance_id}") + print(f" repo: {repo_url} source: {task.get('source', '?')}") + + # try: + # patch, elapsed, traj_data, usage = run_single_task( + # instance_id=instance_id, + # repo_url=repo_url, + # work_root=WORK_ROOT, + # mitm_script_path=str(MITM_SCRIPT), + # trace_dir=TRACE_DIR, + # model=MODEL, + # task=task, + # ) + + # result_entry = { + # "instance_id": instance_id, + # "model_patch": patch if patch else "", + # "model_name_or_path": "claude-code-cli", + # "elapsed_seconds": round(elapsed, 1), + # "traj_data": traj_data, + # "token_usage": usage, + # } + + # with open(OUTPUT_FILE, "a", encoding="utf-8") as f: + # f.write(json.dumps(result_entry) + "\n") + # print(f"βœ… Result saved for {instance_id}") + # success_count += 1 + + # except Exception as e: + # print(f"❌ Error processing {instance_id}: {e}") + # failure_count += 1 + # finally: + # cleanup_residuals() + # print("πŸ’€ Cooldown...") + # time.sleep(20) + + # print( + # f"\nβœ… Finished {len(selected_tasks)} random tasks: " + # f"{success_count} succeeded, {failure_count} failed. " + # f"Results in {OUTPUT_FILE}" + # ) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/contextbench/scripts/batch_run_selected.py b/benchmarks/contextbench/scripts/batch_run_selected.py new file mode 100644 index 0000000..8854850 --- /dev/null +++ b/benchmarks/contextbench/scripts/batch_run_selected.py @@ -0,0 +1,208 @@ +import json +import os +import subprocess +import time +from pathlib import Path + +from auto_run import prefetch_task_repositories, run_single_task +from datasets import load_dataset + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT_FILE = os.environ.get("OUTPUT_FILE", "all_predictions_claude.jsonl") +WORK_ROOT = os.environ.get("WORK_ROOT", "contextbench_work_dir_claude") +MODEL = os.environ.get("MODEL", os.environ.get("CLAUDE_MODEL", "")).strip() +DATASET_NAME = os.environ.get("DATASET_NAME", "Contextbench/ContextBench") +DATASET_SPLIT = os.environ.get("DATASET_SPLIT", "train") +BENCH_FILTER = os.environ.get("BENCH_FILTER", "").strip() # e.g. "Verified", "Pro", "Poly", "Multi" +PREFETCH_REPOS = os.environ.get("PREFETCH_REPOS", "1").strip() != "0" +MITM_SCRIPT = ROOT / "mitmproxy_addons" / "trace_recorder.py" +TRACE_DIR = ROOT / "traces" / "raw" + +# Instances to run. Set instance_ids here or pass via SELECTED_IDS env var (comma-separated). +SELECTED_IDS = [ + # "SWE-Bench-Pro__python__maintenance__bugfix__19a1fba2", + # "SWE-Bench-Pro__python__maintenance__bugfix__2464eadb", + # "SWE-Bench-Pro__python__maintenance__bugfix__38dc8f4e", + # "SWE-Bench-Pro__javascript__maintenance__bugfix__2bfb5681", + # "SWE-Bench-Pro__python__maintenance__bugfix__71253eae", + # "SWE-Bench-Pro__javascript__maintenance__bugfix__93b583ae", + # "SWE-Bench-Pro__python__maintenance__bugfix__dcc84d4c", + # "SWE-Bench-Pro__python__maintenance__bugfix__462b957d", + # "SWE-Bench-Pro__python__maintenance__bugfix__9af74069", + # "SWE-Bench-Pro__python__maintenance__bugfix__7b688a35", + # "SWE-Bench-Pro__python__maintenance__bugfix__64fffdfa", + # "SWE-Bench-Pro__python__maintenance__bugfix__22a1484c", + # "SWE-Bench-Pro__go__maintenance__bugfix__1177cd53", + # "SWE-Bench-Pro__python__maintenance__bugfix__a4287775", + # "SWE-Bench-Pro__python__maintenance__bugfix__ba13492e", + # "SWE-Bench-Pro__go__maintenance__bugfix__b91d5788", + # "SWE-Bench-Pro__python__maintenance__bugfix__091dae2f", + # "SWE-Bench-Pro__python__maintenance__bugfix__b6eff698", + # "SWE-Bench-Pro__python__maintenance__bugfix__fcb506a5", + # "SWE-Bench-Pro__python__maintenance__bugfix__3cfd9a02", + # "SWE-Bench-Pro__python__maintenance__bugfix__4c132bfd", + # "SWE-Bench-Pro__python__maintenance__bugfix__7c2efe8a", + "SWE-Bench-Pro__go__maintenance__bugfix__40a717e5", + "SWE-Bench-Pro__go__maintenance__bugfix__52d866b3", + "SWE-Bench-Pro__go__maintenance__bugfix__720b4d92", + "SWE-Bench-Pro__go__maintenance__bugfix__997c7afd", + "SWE-Bench-Pro__javascript__maintenance__bugfix__82518720", + "SWE-Bench-Pro__javascript__maintenance__bugfix__e31ec45c", + "SWE-Bench-Pro__python__maintenance__bugfix__07bb383a", + "SWE-Bench-Pro__python__maintenance__bugfix__0bac5789", + "SWE-Bench-Pro__python__maintenance__bugfix__18d7bbbc", + "SWE-Bench-Pro__python__maintenance__bugfix__1cf3e889", + "SWE-Bench-Pro__python__maintenance__bugfix__20dad82b", + "SWE-Bench-Pro__python__maintenance__bugfix__20f502e0", + "SWE-Bench-Pro__python__maintenance__bugfix__509a20d9", + "SWE-Bench-Pro__python__maintenance__bugfix__53ca6a30", + "SWE-Bench-Pro__python__maintenance__bugfix__552343cd", + "SWE-Bench-Pro__python__maintenance__bugfix__5b2cf9bb", + "SWE-Bench-Pro__python__maintenance__bugfix__66e05eaa", + "SWE-Bench-Pro__python__maintenance__bugfix__6ebb54dc", + "SWE-Bench-Pro__python__maintenance__bugfix__87bfb374", + "SWE-Bench-Pro__python__maintenance__bugfix__89932d58", + "SWE-Bench-Pro__python__maintenance__bugfix__942d0b14", + "SWE-Bench-Pro__python__maintenance__bugfix__983f2896", + "SWE-Bench-Pro__python__maintenance__bugfix__a984b409", + "SWE-Bench-Pro__python__maintenance__bugfix__aa07d0c3", + "SWE-Bench-Pro__python__maintenance__bugfix__cf01f471", + "SWE-Bench-Pro__python__maintenance__bugfix__d2506f10", + "SWE-Bench-Pro__python__maintenance__bugfix__e579f2f0", + "SWE-Bench-Pro__python__maintenance__bugfix__eafb1f0b", + "SWE-Bench-Pro__python__maintenance__bugfix__ef8756b1", + "SWE-Bench-Pro__python__maintenance__bugfix__f87209f8", + "SWE-Bench-Pro__python__maintenance__bugfix__ff79bafd", +] + +if os.environ.get("SELECTED_IDS"): + SELECTED_IDS = [x.strip() for x in os.environ["SELECTED_IDS"].split(",") if x.strip()] + + +def cleanup_residuals(): + print("🧹 Cleaning up residual processes (Claude & Mitm)...") + try: + subprocess.run(["pkill", "-f", "claude"], stderr=subprocess.DEVNULL) + subprocess.run(["pkill", "-f", "mitmdump"], stderr=subprocess.DEVNULL) + time.sleep(2) + except Exception: + pass + + +def main(): + if not SELECTED_IDS: + print( + "⚠️ Warning: SELECTED_IDS list is empty. Add instance IDs to the script or set SELECTED_IDS env var." + ) + return + + Path(WORK_ROOT).mkdir(parents=True, exist_ok=True) + TRACE_DIR.mkdir(parents=True, exist_ok=True) + + print(f"🧠 Model: {MODEL or '(Claude CLI default)'}") + if BENCH_FILTER: + print(f"πŸ”Ž Bench filter: {BENCH_FILTER}") + + api_key = os.environ.get("ANTHROPIC_API_KEY") + if api_key: + print(f"πŸ”‘ Using API key from environment: {api_key[:20]}...") + else: + print("πŸ” ANTHROPIC_API_KEY not set; using Claude CLI logged-in session.") + + # Load already-completed instance IDs to support resuming. + existing_ids: set = set() + output_path = Path(OUTPUT_FILE) + if output_path.exists(): + with open(output_path) as f: + for line in f: + try: + data = json.loads(line) + existing_ids.add(data["instance_id"]) + except Exception: + continue + print(f"βœ… Found {len(existing_ids)} already completed tasks.") + + print(f"πŸ“š Loading dataset: {DATASET_NAME} ({DATASET_SPLIT})...") + ds = load_dataset(DATASET_NAME, split=DATASET_SPLIT) + + # Build a lookup dict for fast access. + task_lookup = {t["instance_id"]: t for t in ds} + + selected_tasks = [] + for iid in SELECTED_IDS: + if iid in existing_ids: + print(f"⏩ Skipping {iid} (already completed)") + continue + task = task_lookup.get(iid) + if task is None: + print(f"⚠️ Instance {iid} not found in dataset; skipping.") + continue + if BENCH_FILTER and task.get("source", "") != BENCH_FILTER: + print(f"⏩ Skipping {iid} (source={task.get('source')} != {BENCH_FILTER})") + continue + selected_tasks.append(task) + + if not selected_tasks: + print("πŸŽ‰ No pending selected tasks to run!") + return + + print(f"πŸš€ Selected {len(selected_tasks)} tasks to process.") + if PREFETCH_REPOS: + prefetch_task_repositories(selected_tasks, Path(WORK_ROOT)) + else: + print("⏭️ PREFETCH_REPOS=0; skipping prefetch step.") + success_count = 0 + failure_count = 0 + + for i, task in enumerate(selected_tasks): + instance_id = task["instance_id"] + repo_url = task["repo_url"] + + print(f"\n{'-' * 60}") + print(f"πŸ“¦ [{i + 1}/{len(selected_tasks)}] Running: {instance_id}") + print(f" repo: {repo_url} source: {task.get('source', '?')}") + + try: + patch, elapsed, agent_seconds, traj_data, usage = run_single_task( + instance_id=instance_id, + repo_url=repo_url, + work_root=WORK_ROOT, + mitm_script_path=str(MITM_SCRIPT), + trace_dir=TRACE_DIR, + model=MODEL, + task=task, + ) + + result_entry = { + "instance_id": instance_id, + "model_patch": patch if patch else "", + "model_name_or_path": "claude-code-cli", + "elapsed_seconds": round(elapsed, 1), + "latency_seconds": round(elapsed, 1), + "agent_seconds": round(agent_seconds, 1) if agent_seconds is not None else None, + "traj_data": traj_data, + "token_usage": usage, + } + + with open(OUTPUT_FILE, "a", encoding="utf-8") as f: + f.write(json.dumps(result_entry) + "\n") + print(f"βœ… Result saved for {instance_id}") + success_count += 1 + + except Exception as e: + print(f"❌ Error processing {instance_id}: {e}") + failure_count += 1 + finally: + cleanup_residuals() + print("πŸ’€ Cooldown...") + time.sleep(20) + + print( + f"\nβœ… Finished {len(selected_tasks)} selected tasks: " + f"{success_count} succeeded, {failure_count} failed. " + f"Results in {OUTPUT_FILE}" + ) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/contextbench/scripts/prepare_repos_with_leann.py b/benchmarks/contextbench/scripts/prepare_repos_with_leann.py new file mode 100644 index 0000000..f85c8f2 --- /dev/null +++ b/benchmarks/contextbench/scripts/prepare_repos_with_leann.py @@ -0,0 +1,462 @@ +""" +Prepare ContextBench repositories and build LEANN indexes. + +For each selected ContextBench instance: + 1. Clone the repo into / + 2. Checkout base_commit + 3. Build LEANN index under //.leann/ + +Usage: + cd scripts + python prepare_repos_with_leann.py +""" + +import json +import os +import shlex +import shutil +import subprocess +import time +from pathlib import Path +from typing import Optional + +from datasets import load_dataset +from git import Repo + +WORK_ROOT = os.environ.get("WORK_ROOT", "contextbench_work_dir_claude") +LEANN_BIN = os.environ.get("LEANN_BIN", "leann") +DATASET_NAME = os.environ.get("DATASET_NAME", "Contextbench/ContextBench") +DATASET_SPLIT = os.environ.get("DATASET_SPLIT", "train") +BENCH_FILTER = os.environ.get("BENCH_FILTER", "").strip() # Verified | Pro | Poly | Multi +LEANN_SOURCE_EXTENSIONS = os.environ.get( + "LEANN_SOURCE_EXTENSIONS", + "py,go,js,jsx,ts,tsx,java,kt,kts,rs,rb,php,cs,c,cc,cpp,h,hpp,m,mm,swift,scala,sh,sql,lua,r", +) + +# LEANN index build parameters β€” override via env vars for sweeping configs. +# ast-chunk-size is in non-whitespace CHARACTERS (not tokens). bge-base-en-v1.5 +# has a 512-token limit; at ~1.2 tokens/char: 300 chars + 64 overlap β‰ˆ 436 tokens. +LEANN_EMBEDDING_MODEL = os.environ.get( + "LEANN_EMBEDDING_MODEL", "jinaai/jina-embeddings-v2-base-code" +) +LEANN_AST_CHUNK_SIZE = os.environ.get("LEANN_AST_CHUNK_SIZE", "600") +LEANN_AST_CHUNK_OVERLAP = os.environ.get("LEANN_AST_CHUNK_OVERLAP", "96") +# Set to "0" to disable vendor/generated exclusion (e.g. for ablation experiments). +LEANN_EXCLUDE_VENDOR = os.environ.get("LEANN_EXCLUDE_VENDOR", "1").strip() != "0" +# Set to "1" to exclude test files (e.g. for ablation experiments). Default off +# to avoid missing bugfix targets that touch test/fixture/spec files. +LEANN_EXCLUDE_TESTS = os.environ.get("LEANN_EXCLUDE_TESTS", "0").strip() != "0" +FAILED_INSTANCES_LOG = os.environ.get( + "FAILED_INSTANCES_LOG", "prepare_repos_with_leann_failures.jsonl" +).strip() + +# Fill in ContextBench instance_ids. Leave empty to prepare all tasks +# (optionally filtered by BENCH_FILTER). +SELECTED_IDS: list[str] = [ + # "SWE-Bench-Pro__python__maintenance__bugfix__19a1fba2", + # "SWE-Bench-Pro__python__maintenance__bugfix__2464eadb", + # "SWE-Bench-Pro__python__maintenance__bugfix__38dc8f4e", + # "SWE-Bench-Pro__javascript__maintenance__bugfix__2bfb5681", + # "SWE-Bench-Pro__python__maintenance__bugfix__71253eae", + # "SWE-Bench-Pro__javascript__maintenance__bugfix__93b583ae", + # "SWE-Bench-Pro__python__maintenance__bugfix__dcc84d4c", + # "SWE-Bench-Pro__python__maintenance__bugfix__462b957d", + # "SWE-Bench-Pro__python__maintenance__bugfix__9af74069", + # "SWE-Bench-Pro__python__maintenance__bugfix__7b688a35", + # "SWE-Bench-Pro__python__maintenance__bugfix__64fffdfa", + # "SWE-Bench-Pro__python__maintenance__bugfix__22a1484c", + # "SWE-Bench-Pro__go__maintenance__bugfix__1177cd53", + # "SWE-Bench-Pro__python__maintenance__bugfix__a4287775", + # "SWE-Bench-Pro__python__maintenance__bugfix__ba13492e", + # "SWE-Bench-Pro__go__maintenance__bugfix__b91d5788", + # "SWE-Bench-Pro__python__maintenance__bugfix__091dae2f", + # "SWE-Bench-Pro__python__maintenance__bugfix__b6eff698", + # "SWE-Bench-Pro__python__maintenance__bugfix__fcb506a5", + # "SWE-Bench-Pro__python__maintenance__bugfix__3cfd9a02", + # "SWE-Bench-Pro__python__maintenance__bugfix__4c132bfd", + # "SWE-Bench-Pro__python__maintenance__bugfix__7c2efe8a", + "SWE-Bench-Pro__go__maintenance__bugfix__40a717e5", + "SWE-Bench-Pro__go__maintenance__bugfix__52d866b3", + "SWE-Bench-Pro__go__maintenance__bugfix__720b4d92", + "SWE-Bench-Pro__go__maintenance__bugfix__997c7afd", + "SWE-Bench-Pro__javascript__maintenance__bugfix__82518720", + "SWE-Bench-Pro__javascript__maintenance__bugfix__e31ec45c", + "SWE-Bench-Pro__python__maintenance__bugfix__07bb383a", + "SWE-Bench-Pro__python__maintenance__bugfix__0bac5789", + "SWE-Bench-Pro__python__maintenance__bugfix__18d7bbbc", + "SWE-Bench-Pro__python__maintenance__bugfix__1cf3e889", + "SWE-Bench-Pro__python__maintenance__bugfix__20dad82b", + "SWE-Bench-Pro__python__maintenance__bugfix__20f502e0", + "SWE-Bench-Pro__python__maintenance__bugfix__509a20d9", + "SWE-Bench-Pro__python__maintenance__bugfix__53ca6a30", + "SWE-Bench-Pro__python__maintenance__bugfix__552343cd", + "SWE-Bench-Pro__python__maintenance__bugfix__5b2cf9bb", + "SWE-Bench-Pro__python__maintenance__bugfix__66e05eaa", + "SWE-Bench-Pro__python__maintenance__bugfix__6ebb54dc", + "SWE-Bench-Pro__python__maintenance__bugfix__87bfb374", + "SWE-Bench-Pro__python__maintenance__bugfix__89932d58", + "SWE-Bench-Pro__python__maintenance__bugfix__942d0b14", + "SWE-Bench-Pro__python__maintenance__bugfix__983f2896", + "SWE-Bench-Pro__python__maintenance__bugfix__a984b409", + "SWE-Bench-Pro__python__maintenance__bugfix__aa07d0c3", + "SWE-Bench-Pro__python__maintenance__bugfix__cf01f471", + "SWE-Bench-Pro__python__maintenance__bugfix__d2506f10", + "SWE-Bench-Pro__python__maintenance__bugfix__e579f2f0", + "SWE-Bench-Pro__python__maintenance__bugfix__eafb1f0b", + "SWE-Bench-Pro__python__maintenance__bugfix__ef8756b1", + "SWE-Bench-Pro__python__maintenance__bugfix__f87209f8", + "SWE-Bench-Pro__python__maintenance__bugfix__ff79bafd", +] + +if os.environ.get("SELECTED_IDS"): + SELECTED_IDS = [x.strip() for x in os.environ["SELECTED_IDS"].split(",") if x.strip()] + +# Vendor and generated-code directory/file patterns to exclude from the index. +# These are third-party or machine-generated files that are never the target of +# a bugfix, so indexing them only adds noise to search results. +_VENDOR_DIR_PATTERNS = ( + "vendor/", + "node_modules/", + "third_party/", + "thirdparty/", + "externals/", + ".cache/", +) +_GENERATED_FILE_PATTERNS = ( + "_pb.go", + ".pb.go", + "_gen.go", + ".pb.cc", + ".pb.h", +) +# Filenames like `zz_generated.deepcopy.go` end in `.go`, not `zz_generated`; +# match these as path substrings (controller-gen / k8s-style outputs). +_GENERATED_FILE_SUBSTRINGS = ("zz_generated",) + +# Test file path/name patterns to exclude from the index. +_TEST_PATH_PATTERNS = ( + "/test/", + "/tests/", + "/__tests__/", + "/spec/", + "/testdata/", + "/test_", + "/fixtures/", +) +_TEST_FILE_PATTERNS = ( + "_test.py", + "_test.go", + ".test.js", + ".test.ts", + ".test.jsx", + ".test.tsx", + ".spec.js", + ".spec.ts", + ".spec.jsx", + ".spec.tsx", + "_spec.rb", +) + + +def _run_command(cmd: list[str], cwd: Path) -> subprocess.CompletedProcess: + return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True) + + +def _read_json_file(path: Path) -> Optional[dict]: + if not path.exists(): + return None + try: + return json.loads(path.read_text(encoding="utf-8")) + except Exception: + return None + + +def _count_index_chunks(repo_dir: Path, instance_id: str) -> Optional[int]: + ids_path = repo_dir / ".leann" / "indexes" / instance_id / "documents.ids.txt" + if not ids_path.exists(): + return None + with ids_path.open("r", encoding="utf-8") as f: + return sum(1 for _ in f) + + +def _print_subprocess_output(label: str, text: str, max_lines: int = 20) -> None: + lines = [line for line in (text or "").splitlines() if line.strip()] + if not lines: + return + print(f" πŸ“„ {label}:") + for line in lines[:max_lines]: + print(f" {line}") + if len(lines) > max_lines: + print(f" ... ({len(lines) - max_lines} more lines)") + + +def _write_failure_report(failures: list[dict]) -> Optional[Path]: + if not failures: + return None + report_path = Path(FAILED_INSTANCES_LOG) + report_path.parent.mkdir(parents=True, exist_ok=True) + with report_path.open("w", encoding="utf-8") as f: + for item in failures: + f.write(json.dumps(item, ensure_ascii=True) + "\n") + return report_path + + +def _load_tasks() -> list[dict]: + print(f"πŸ“š Loading dataset: {DATASET_NAME} ({DATASET_SPLIT})...") + ds = load_dataset(DATASET_NAME, split=DATASET_SPLIT) + tasks: list[dict] = list(ds) + if BENCH_FILTER: + tasks = [t for t in tasks if t.get("source", "") == BENCH_FILTER] + + if SELECTED_IDS: + task_lookup = {t["instance_id"]: t for t in tasks} + selected: list[dict] = [] + for iid in SELECTED_IDS: + task = task_lookup.get(iid) + if not task: + print(f"⚠️ Instance not found in dataset/split/filter: {iid}") + continue + selected.append(task) + return selected + + return tasks + + +def _is_pytest_style_test_py(normalized_path: str) -> bool: + """True for pytest-style modules: basename test_*.py (e.g. test_foo.py).""" + name = Path(normalized_path).name + return name.startswith("test_") and name.lower().endswith(".py") + + +def build_leann_index(instance_id: str, repo_dir: Path) -> tuple[bool, Optional[str]]: + print(f" πŸ” Building LEANN index for {instance_id}...") + + result = _run_command(["git", "ls-files"], cwd=repo_dir) + if result.returncode != 0: + error = f"Could not list git files: {result.stderr.strip()}" + print(f" ⚠️ {error}") + return False, error + + tracked_files = [f for f in result.stdout.strip().split("\n") if f] + allowed_exts = { + f".{ext.strip().lstrip('.').lower()}" + for ext in LEANN_SOURCE_EXTENSIONS.split(",") + if ext.strip() + } + source_files = [f for f in tracked_files if Path(f).suffix.lower() in allowed_exts] + + # Exclude vendor/generated files β€” they are never bugfix targets and add noise. + if LEANN_EXCLUDE_VENDOR: + before = len(source_files) + normalized = [f.replace("\\", "/") for f in source_files] + source_files = [ + f + for f, n in zip(source_files, normalized) + if not any(pat in n for pat in _VENDOR_DIR_PATTERNS) + and not any(n.endswith(pat) for pat in _GENERATED_FILE_PATTERNS) + and not any(sub in n for sub in _GENERATED_FILE_SUBSTRINGS) + ] + excluded = before - len(source_files) + if excluded: + print( + f" 🚫 Excluded {excluded} vendor/generated files ({before} β†’ {len(source_files)})" + ) + + # Exclude test files β€” they are rarely bugfix targets and consistently rank + # high in semantic search due to mirroring production code patterns. + if LEANN_EXCLUDE_TESTS: + before = len(source_files) + normalized = [f.replace("\\", "/") for f in source_files] + source_files = [ + f + for f, n in zip(source_files, normalized) + if not any(pat in n for pat in _TEST_PATH_PATTERNS) + and not _is_pytest_style_test_py(n) + and not any(n.endswith(pat) for pat in _TEST_FILE_PATTERNS) + ] + excluded = before - len(source_files) + if excluded: + print(f" 🚫 Excluded {excluded} test files ({before} β†’ {len(source_files)})") + + if not source_files: + error = f"No source files found for extensions: {sorted(allowed_exts)}" + print(f" ⚠️ {error}") + return False, error + + # Derive --file-types from the actual extensions present after filtering, + # so all indexed file types benefit from AST-aware chunking. + indexed_exts = sorted( + {Path(f).suffix.lstrip(".").lower() for f in source_files if Path(f).suffix} + ) + file_types_arg = ",".join(indexed_exts) + + print(f" πŸ“Š Found {len(source_files)} source files (types: {file_types_arg})") + leann_cmd = [ + LEANN_BIN, + "build", + instance_id, + "--docs", + *source_files, + "--embedding-mode", + "sentence-transformers", + "--embedding-model", + LEANN_EMBEDDING_MODEL, + "--backend", + "hnsw", + "--file-types", + file_types_arg, + "--force", + "--ast-chunk-size", + LEANN_AST_CHUNK_SIZE, + "--ast-chunk-overlap", + LEANN_AST_CHUNK_OVERLAP, + "--use-ast-chunking", + "--no-recompute", + ] + debug_cmd = [ + LEANN_BIN, + "build", + instance_id, + "--docs", + f"<{len(source_files)} files>", + *leann_cmd[len(["leann", "build", instance_id, "--docs"]) + len(source_files) :], + ] + print(f" πŸ§ͺ LEANN command: {shlex.join(debug_cmd)}") + + try: + proc = subprocess.run( + leann_cmd, + cwd=repo_dir, + capture_output=True, + text=True, + timeout=1800, + env={ + **os.environ, + "LEANN_EMBEDDING_DEVICE": os.environ.get("LEANN_EMBEDDING_DEVICE", "mps"), + "LEANN_BATCH_SIZE": os.environ.get("LEANN_BATCH_SIZE", "32"), + }, + ) + except subprocess.TimeoutExpired: + error = "LEANN build timed out" + print(f" ⏰ {error}") + return False, error + except Exception as e: + error = f"LEANN error: {e}" + print(f" ❌ {error}") + return False, error + + _print_subprocess_output("LEANN stdout", proc.stdout) + _print_subprocess_output("LEANN stderr", proc.stderr) + + if proc.returncode != 0: + stderr = (proc.stderr or "").strip() + print(f" ❌ LEANN build failed: {stderr}") + return False, f"LEANN build failed: {stderr}" + + meta_path = repo_dir / ".leann" / "indexes" / instance_id / "documents.leann.meta.json" + meta = _read_json_file(meta_path) + if meta: + print(f" 🧠 Embedding model in index: {meta.get('embedding_model', 'unknown')}") + print(" 🌲 AST chunking requested: yes (--use-ast-chunking)") + chunk_count = _count_index_chunks(repo_dir, instance_id) + if chunk_count is not None: + print(f" 🧩 Indexed chunks: {chunk_count}") + + print(" βœ… LEANN index built successfully") + return True, None + + +def prepare_single_task(task: dict) -> tuple[bool, Optional[str]]: + instance_id = task["instance_id"] + repo_url = task["repo_url"] + base_commit = task["base_commit"] + target_dir = Path(WORK_ROOT) / instance_id + + print(f"\n{'=' * 72}") + print(f"πŸ“¦ Preparing: {instance_id}") + print(f" repo: {repo_url}") + print(f" commit: {base_commit[:12]}...") + print(f"{'=' * 72}") + + if not target_dir.exists(): + print(f" πŸ“₯ Cloning {repo_url}...") + try: + Repo.clone_from(repo_url, target_dir) + except Exception as e: + print(f" ❌ Clone failed: {e}") + return False, f"Clone failed: {e}" + else: + print(" βœ“ Repo already exists") + + try: + repo = Repo(target_dir) + print(f" πŸ”€ Checking out {base_commit[:8]}...") + repo.git.reset("--hard") + repo.git.checkout(base_commit) + repo.git.clean("-fdx", "-e", ".leann/") + (target_dir / "PROBLEM.md").write_text(task.get("problem_statement", ""), encoding="utf-8") + except Exception as e: + print(f" ❌ Checkout/clean failed: {e}") + return False, f"Checkout/clean failed: {e}" + + return build_leann_index(instance_id, target_dir) + + +def main(): + print("πŸš€ ContextBench Repository Preparation with LEANN Indexing") + print("=" * 72) + + leann_path = shutil.which(LEANN_BIN) + if not leann_path: + print("❌ LEANN not found. Install: uv tool install leann-core --with leann") + return + print(f"βœ… LEANN found: {leann_path}") + + tasks = _load_tasks() + if not tasks: + print("⚠️ No tasks selected. Set SELECTED_IDS or adjust BENCH_FILTER.") + return + + Path(WORK_ROOT).mkdir(parents=True, exist_ok=True) + print(f"\nπŸ“‚ Work root: {WORK_ROOT}") + print(f"🎯 Tasks to prepare: {len(tasks)}") + if BENCH_FILTER: + print(f"πŸ”Ž Bench filter: {BENCH_FILTER}") + + success_count = 0 + fail_count = 0 + failures: list[dict] = [] + for i, task in enumerate(tasks, start=1): + print(f"\n[{i}/{len(tasks)}]") + succeeded, error = prepare_single_task(task) + if succeeded: + success_count += 1 + else: + fail_count += 1 + failures.append( + { + "instance_id": task.get("instance_id", ""), + "repo_url": task.get("repo_url", ""), + "base_commit": task.get("base_commit", ""), + "error": error or "unknown error", + "failed_at_unix": int(time.time()), + } + ) + + print(f"\n{'=' * 72}") + print(f"πŸŽ‰ Done! βœ… {success_count} succeeded ❌ {fail_count} failed") + if failures: + print("\nFailed instances:") + for item in failures: + print(f" - {item['instance_id']}: {item['error']}") + report_path = _write_failure_report(failures) + if report_path is not None: + print(f"\nπŸ“ Failure report written to: {report_path}") + print("\nNext steps:") + print(f" 1. Verify indexes: ls {WORK_ROOT}/*/.leann") + print(" 2. Run with LEANN: LEANN_ENABLED=1 python batch_run_selected.py") + print(" 3. Baseline run: LEANN_ENABLED=0 python batch_run_selected.py") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/diskann_vs_hnsw_speed_comparison.py b/benchmarks/diskann_vs_hnsw_speed_comparison.py new file mode 100644 index 0000000..b4e6159 --- /dev/null +++ b/benchmarks/diskann_vs_hnsw_speed_comparison.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +""" +DiskANN vs HNSW Search Performance Comparison + +This benchmark compares search performance between DiskANN and HNSW backends: +- DiskANN: With graph partitioning enabled (is_recompute=True) +- HNSW: With recompute enabled (is_recompute=True) +- Tests performance across different dataset sizes +- Measures search latency, recall, and index size +""" + +import gc +import multiprocessing as mp +import tempfile +import time +from pathlib import Path +from typing import Any + +import numpy as np + +# Prefer 'fork' start method to avoid POSIX semaphore leaks on macOS +try: + mp.set_start_method("fork", force=True) +except Exception: + pass + + +def create_test_texts(n_docs: int) -> list[str]: + """Create synthetic test documents for benchmarking.""" + np.random.seed(42) + topics = [ + "machine learning and artificial intelligence", + "natural language processing and text analysis", + "computer vision and image recognition", + "data science and statistical analysis", + "deep learning and neural networks", + "information retrieval and search engines", + "database systems and data management", + "software engineering and programming", + "cybersecurity and network protection", + "cloud computing and distributed systems", + ] + + texts = [] + for i in range(n_docs): + topic = topics[i % len(topics)] + variation = np.random.randint(1, 100) + text = ( + f"This is document {i} about {topic}. Content variation {variation}. " + f"Additional information about {topic} with details and examples. " + f"Technical discussion of {topic} including implementation aspects." + ) + texts.append(text) + + return texts + + +def benchmark_backend( + backend_name: str, texts: list[str], test_queries: list[str], backend_kwargs: dict[str, Any] +) -> dict[str, float]: + """Benchmark a specific backend with the given configuration.""" + from leann.api import LeannBuilder, LeannSearcher + + print(f"\nπŸ”§ Testing {backend_name.upper()} backend...") + + with tempfile.TemporaryDirectory() as temp_dir: + index_path = str(Path(temp_dir) / f"benchmark_{backend_name}.leann") + + # Build index + print(f"πŸ“¦ Building {backend_name} index with {len(texts)} documents...") + start_time = time.time() + + builder = LeannBuilder( + backend_name=backend_name, + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + **backend_kwargs, + ) + + for text in texts: + builder.add_text(text) + + builder.build_index(index_path) + build_time = time.time() - start_time + + # Measure index size + index_dir = Path(index_path).parent + index_files = list(index_dir.glob(f"{Path(index_path).stem}.*")) + total_size = sum(f.stat().st_size for f in index_files if f.is_file()) + size_mb = total_size / (1024 * 1024) + + print(f" βœ… Build completed in {build_time:.2f}s, index size: {size_mb:.1f}MB") + + # Search benchmark + print("πŸ” Running search benchmark...") + searcher = LeannSearcher(index_path) + + search_times = [] + all_results = [] + + for query in test_queries: + start_time = time.time() + results = searcher.search(query, top_k=5) + search_time = time.time() - start_time + search_times.append(search_time) + all_results.append(results) + + avg_search_time = np.mean(search_times) * 1000 # Convert to ms + print(f" βœ… Average search time: {avg_search_time:.1f}ms") + + # Check for valid scores (detect -inf issues) + all_scores = [ + result.score + for results in all_results + for result in results + if result.score is not None + ] + valid_scores = [ + score for score in all_scores if score != float("-inf") and score != float("inf") + ] + score_validity_rate = len(valid_scores) / len(all_scores) if all_scores else 0 + + # Clean up (ensure embedding server shutdown and object GC) + try: + if hasattr(searcher, "cleanup"): + searcher.cleanup() + del searcher + del builder + gc.collect() + except Exception as e: + print(f"⚠️ Warning: Resource cleanup error: {e}") + + return { + "build_time": build_time, + "avg_search_time_ms": avg_search_time, + "index_size_mb": size_mb, + "score_validity_rate": score_validity_rate, + } + + +def run_comparison(n_docs: int = 500, n_queries: int = 10): + """Run performance comparison between DiskANN and HNSW.""" + print("πŸš€ Starting DiskANN vs HNSW Performance Comparison") + print(f"πŸ“Š Dataset: {n_docs} documents, {n_queries} test queries") + + # Create test data + texts = create_test_texts(n_docs) + test_queries = [ + "machine learning algorithms", + "natural language processing", + "computer vision techniques", + "data analysis methods", + "neural network architectures", + "database query optimization", + "software development practices", + "security vulnerabilities", + "cloud infrastructure", + "distributed computing", + ][:n_queries] + + # HNSW benchmark + hnsw_results = benchmark_backend( + backend_name="hnsw", + texts=texts, + test_queries=test_queries, + backend_kwargs={ + "is_recompute": True, # Enable recompute for fair comparison + "M": 16, + "efConstruction": 200, + }, + ) + + # DiskANN benchmark + diskann_results = benchmark_backend( + backend_name="diskann", + texts=texts, + test_queries=test_queries, + backend_kwargs={ + "is_recompute": True, # Enable graph partitioning + "num_neighbors": 32, + "search_list_size": 50, + }, + ) + + # Performance comparison + print("\nπŸ“ˆ Performance Comparison Results") + print(f"{'=' * 60}") + print(f"{'Metric':<25} {'HNSW':<15} {'DiskANN':<15} {'Speedup':<10}") + print(f"{'-' * 60}") + + # Build time comparison + build_speedup = hnsw_results["build_time"] / diskann_results["build_time"] + print( + f"{'Build Time (s)':<25} {hnsw_results['build_time']:<15.2f} {diskann_results['build_time']:<15.2f} {build_speedup:<10.2f}x" + ) + + # Search time comparison + search_speedup = hnsw_results["avg_search_time_ms"] / diskann_results["avg_search_time_ms"] + print( + f"{'Search Time (ms)':<25} {hnsw_results['avg_search_time_ms']:<15.1f} {diskann_results['avg_search_time_ms']:<15.1f} {search_speedup:<10.2f}x" + ) + + # Index size comparison + size_ratio = diskann_results["index_size_mb"] / hnsw_results["index_size_mb"] + print( + f"{'Index Size (MB)':<25} {hnsw_results['index_size_mb']:<15.1f} {diskann_results['index_size_mb']:<15.1f} {size_ratio:<10.2f}x" + ) + + # Score validity + print( + f"{'Score Validity (%)':<25} {hnsw_results['score_validity_rate'] * 100:<15.1f} {diskann_results['score_validity_rate'] * 100:<15.1f}" + ) + + print(f"{'=' * 60}") + print("\n🎯 Summary:") + if search_speedup > 1: + print(f" DiskANN is {search_speedup:.2f}x faster than HNSW for search") + else: + print(f" HNSW is {1 / search_speedup:.2f}x faster than DiskANN for search") + + if size_ratio > 1: + print(f" DiskANN uses {size_ratio:.2f}x more storage than HNSW") + else: + print(f" DiskANN uses {1 / size_ratio:.2f}x less storage than HNSW") + + print( + f" Both backends achieved {min(hnsw_results['score_validity_rate'], diskann_results['score_validity_rate']) * 100:.1f}% score validity" + ) + + +if __name__ == "__main__": + import sys + + try: + # Handle help request + if len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help", "help"]: + print("DiskANN vs HNSW Performance Comparison") + print("=" * 50) + print(f"Usage: python {sys.argv[0]} [n_docs] [n_queries]") + print() + print("Arguments:") + print(" n_docs Number of documents to index (default: 500)") + print(" n_queries Number of test queries to run (default: 10)") + print() + print("Examples:") + print(" python benchmarks/diskann_vs_hnsw_speed_comparison.py") + print(" python benchmarks/diskann_vs_hnsw_speed_comparison.py 1000") + print(" python benchmarks/diskann_vs_hnsw_speed_comparison.py 2000 20") + sys.exit(0) + + # Parse command line arguments + n_docs = int(sys.argv[1]) if len(sys.argv) > 1 else 500 + n_queries = int(sys.argv[2]) if len(sys.argv) > 2 else 10 + + print("DiskANN vs HNSW Performance Comparison") + print("=" * 50) + print(f"Dataset: {n_docs} documents, {n_queries} queries") + print() + + run_comparison(n_docs=n_docs, n_queries=n_queries) + + except KeyboardInterrupt: + print("\n⚠️ Benchmark interrupted by user") + sys.exit(130) + except Exception as e: + print(f"\n❌ Benchmark failed: {e}") + sys.exit(1) + finally: + # Ensure clean exit (forceful to prevent rare hangs from atexit/threads) + try: + gc.collect() + print("\n🧹 Cleanup completed") + # Flush stdio to ensure message is visible before hard-exit + try: + import sys as _sys + + _sys.stdout.flush() + _sys.stderr.flush() + except Exception: + pass + except Exception: + pass + # Use os._exit to bypass atexit handlers that may hang in rare cases + import os as _os + + _os._exit(0) diff --git a/benchmarks/enron_emails/README.md b/benchmarks/enron_emails/README.md new file mode 100644 index 0000000..fdeae69 --- /dev/null +++ b/benchmarks/enron_emails/README.md @@ -0,0 +1,141 @@ +# Enron Emails Benchmark + +A comprehensive RAG benchmark for evaluating LEANN search and generation on the Enron email corpus. It mirrors the structure and CLI of the existing FinanceBench and LAION benches, using stage-based evaluation with Recall@3 and generation timing. + +- Dataset: Enron email CSV (e.g., Kaggle wcukierski/enron-email-dataset) for passages +- Queries: corbt/enron_emails_sample_questions (filtered for realistic questions) +- Metrics: Recall@3 vs FAISS Flat baseline + Generation evaluation with Qwen3-8B + +## Layout + +benchmarks/enron_emails/ +- setup_enron_emails.py: Prepare passages, build LEANN index, build FAISS baseline +- evaluate_enron_emails.py: Evaluate retrieval recall (Stages 2-5) + generation with Qwen3-8B +- data/: Generated passages, queries, embeddings-related files +- baseline/: FAISS Flat baseline files +- llm_utils.py: LLM utilities for Qwen3-8B generation (in parent directory) + +## Quickstart + +1) Prepare the data and index + +cd benchmarks/enron_emails +python setup_enron_emails.py --data-dir data + +Notes: +- If `--emails-csv` is omitted, the script attempts to download from Kaggle dataset `wcukierski/enron-email-dataset` using Kaggle API (requires `KAGGLE_USERNAME` and `KAGGLE_KEY`). + Alternatively, pass a local path to `--emails-csv`. + +Notes: +- The script parses emails, chunks header/body into passages, builds a compact LEANN index, and then builds a FAISS Flat baseline from the same passages and embedding model. +- Optionally, it will also create evaluation queries from HuggingFace dataset `corbt/enron_emails_sample_questions`. + +2) Run recall evaluation (Stage 2) + +python evaluate_enron_emails.py --index data/enron_index_hnsw.leann --stage 2 + +3) Complexity sweep (Stage 3) + +python evaluate_enron_emails.py --index data/enron_index_hnsw.leann --stage 3 --target-recall 0.90 --max-queries 200 + +Stage 3 uses binary search over complexity to find the minimal value achieving the target Recall@3 (assumes recall is non-decreasing with complexity). The search expands the upper bound as needed and snaps complexity to multiples of 8. + +4) Index comparison (Stage 4) + +python evaluate_enron_emails.py --index data/enron_index_hnsw.leann --stage 4 --complexity 88 --max-queries 100 --output results.json + +5) Generation evaluation (Stage 5) + +python evaluate_enron_emails.py --index data/enron_index_hnsw.leann --stage 5 --complexity 88 --llm-backend hf --model-name Qwen/Qwen3-8B + +6) Combined index + generation evaluation (Stages 4+5, recommended) + +python evaluate_enron_emails.py --index data/enron_index_hnsw.leann --stage 45 --complexity 88 --llm-backend hf + +Notes: +- Minimal CLI: you can run from repo root with only `--index`, defaults match financebench/laion patterns: + - `--stage` defaults to `all` (runs 2, 3, 4, 5) + - `--baseline-dir` defaults to `baseline` + - `--queries` defaults to `data/evaluation_queries.jsonl` (or falls back to the index directory) + - `--llm-backend` defaults to `hf` (HuggingFace), can use `vllm` + - `--model-name` defaults to `Qwen/Qwen3-8B` +- Fail-fast behavior: no silent fallbacks. If compact index cannot run with recompute, it errors out. +- Stage 5 requires Stage 4 retrieval results. Use `--stage 45` to run both efficiently. + +Optional flags: +- --queries data/evaluation_queries.jsonl (custom queries file) +- --baseline-dir baseline (where FAISS baseline lives) +- --complexity 88 (LEANN complexity parameter, optimal for 90% recall) +- --llm-backend hf|vllm (LLM backend for generation) +- --model-name Qwen/Qwen3-8B (LLM model for generation) +- --max-queries 1000 (limit number of queries for evaluation) + +## Files Produced +- data/enron_passages_preview.jsonl: Small preview of passages used (for inspection) +- data/enron_index_hnsw.leann.*: LEANN index files +- baseline/faiss_flat.index + baseline/metadata.pkl: FAISS baseline with passage IDs +- data/evaluation_queries.jsonl: Query file (id + query; includes GT IDs for reference) + +## Notes +- Evaluates both retrieval Recall@3 and generation timing with Qwen3-8B thinking model. +- The emails CSV must contain a column named "message" (raw RFC822 email) and a column named "file" for source identifier. Message-ID headers are parsed as canonical message IDs when present. +- Qwen3-8B requires special handling for thinking models with chat templates and tag processing. + +## Stages Summary + +- Stage 2 (Recall@3): + - Compares LEANN vs FAISS Flat baseline on Recall@3. + - Compact index runs with `recompute_embeddings=True`. + +- Stage 3 (Binary Search for Complexity): + - Builds a non-compact index (`_noncompact.leann`) and runs binary search with `recompute_embeddings=False` to find the minimal complexity achieving target Recall@3 (default 90%). + +- Stage 4 (Index Comparison): + - Reports .index-only sizes for compact vs non-compact. + - Measures timings on queries by default: non-compact (no recompute) vs compact (with recompute). + - Stores retrieval results for Stage 5 generation evaluation. + - Fails fast if compact recompute cannot run. + - If `--complexity` is not provided, the script tries to use the best complexity from Stage 3: + - First from the current run (when running `--stage all`), otherwise + - From `enron_stage3_results.json` saved next to the index during the last Stage 3 run. + - If neither exists, Stage 4 will error and ask you to run Stage 3 or pass `--complexity`. + +- Stage 5 (Generation Evaluation): + - Uses Qwen3-8B thinking model for RAG generation on retrieved documents from Stage 4. + - Supports HuggingFace (`hf`) and vLLM (`vllm`) backends. + - Measures generation timing separately from search timing. + - Requires Stage 4 results (no additional searching performed). + +## Example Results + +These are sample results obtained on Enron data using all-mpnet-base-v2 and Qwen3-8B. + +- Stage 3 (Binary Search): + - Minimal complexity achieving 90% Recall@3: 88 + - Sampled points: + - C=8 β†’ 59.9% Recall@3 + - C=72 β†’ 89.4% Recall@3 + - C=88 β†’ 90.2% Recall@3 + - C=96 β†’ 90.7% Recall@3 + - C=112 β†’ 91.1% Recall@3 + - C=136 β†’ 91.3% Recall@3 + - C=256 β†’ 92.0% Recall@3 + +- Stage 4 (Index Sizes, .index only): + - Compact: ~2.2 MB + - Non-compact: ~82.0 MB + - Storage saving by compact: ~97.3% + +- Stage 4 (Search Timing, 988 queries, complexity=88): + - Non-compact (no recompute): ~0.0075 s avg per query + - Compact (with recompute): ~1.981 s avg per query + - Speed ratio (non-compact/compact): ~0.0038x + +- Stage 5 (RAG Generation, 988 queries, Qwen3-8B): + - Average generation time: ~22.302 s per query + - Total queries processed: 988 + - LLM backend: HuggingFace transformers + - Model: Qwen/Qwen3-8B (thinking model with processing) + +Full JSON output is saved by the script (see `--output`), e.g.: +`benchmarks/enron_emails/results_enron_stage45.json`. diff --git a/benchmarks/enron_emails/data/.gitignore b/benchmarks/enron_emails/data/.gitignore new file mode 100644 index 0000000..c4ffae9 --- /dev/null +++ b/benchmarks/enron_emails/data/.gitignore @@ -0,0 +1 @@ +downloads/ diff --git a/benchmarks/enron_emails/evaluate_enron_emails.py b/benchmarks/enron_emails/evaluate_enron_emails.py new file mode 100644 index 0000000..c6e8518 --- /dev/null +++ b/benchmarks/enron_emails/evaluate_enron_emails.py @@ -0,0 +1,614 @@ +""" +Enron Emails Benchmark Evaluation - Retrieval Recall@3 (Stages 2/3/4) +Follows the style of FinanceBench/LAION: Stage 2 recall vs FAISS baseline, +Stage 3 complexity sweep to target recall, Stage 4 index comparison. +On errors, fail fast without fallbacks. +""" + +import argparse +import json +import logging +import os +import pickle +from pathlib import Path + +import numpy as np +from leann import LeannBuilder, LeannSearcher +from leann_backend_hnsw import faiss + +from ..llm_utils import generate_hf, generate_vllm, load_hf_model, load_vllm_model + +# Setup logging to reduce verbose output +logging.basicConfig(level=logging.WARNING) +logging.getLogger("leann.api").setLevel(logging.WARNING) +logging.getLogger("leann_backend_hnsw").setLevel(logging.WARNING) + + +class RecallEvaluator: + """Stage 2: Evaluate Recall@3 (LEANN vs FAISS)""" + + def __init__(self, index_path: str, baseline_dir: str): + self.index_path = index_path + self.baseline_dir = baseline_dir + self.searcher = LeannSearcher(index_path) + + baseline_index_path = os.path.join(baseline_dir, "faiss_flat.index") + metadata_path = os.path.join(baseline_dir, "metadata.pkl") + + self.faiss_index = faiss.read_index(baseline_index_path) + with open(metadata_path, "rb") as f: + self.passage_ids = pickle.load(f) + + print(f"πŸ“š Loaded FAISS flat baseline with {self.faiss_index.ntotal} vectors") + + # No fallbacks here; if embedding server is needed but fails, the caller will see the error. + + def evaluate_recall_at_3( + self, queries: list[str], complexity: int = 64, recompute_embeddings: bool = True + ) -> float: + """Evaluate recall@3 using FAISS Flat as ground truth""" + from leann.api import compute_embeddings + + recompute_str = "with recompute" if recompute_embeddings else "no recompute" + print(f"πŸ” Evaluating recall@3 with complexity={complexity} ({recompute_str})...") + + total_recall = 0.0 + for i, query in enumerate(queries): + # Compute query embedding with the same model/mode as the index + q_emb = compute_embeddings( + [query], + self.searcher.embedding_model, + mode=self.searcher.embedding_mode, + use_server=False, + ).astype(np.float32) + + # Search FAISS Flat ground truth + n = q_emb.shape[0] + k = 3 + distances = np.zeros((n, k), dtype=np.float32) + labels = np.zeros((n, k), dtype=np.int64) + self.faiss_index.search( + n, + faiss.swig_ptr(q_emb), + k, + faiss.swig_ptr(distances), + faiss.swig_ptr(labels), + ) + + baseline_ids = {self.passage_ids[idx] for idx in labels[0]} + + # Search with LEANN (may require embedding server depending on index configuration) + results = self.searcher.search( + query, + top_k=3, + complexity=complexity, + recompute_embeddings=recompute_embeddings, + ) + test_ids = {r.id for r in results} + + intersection = test_ids.intersection(baseline_ids) + recall = len(intersection) / 3.0 + total_recall += recall + + if i < 3: + print(f" Q{i + 1}: '{query[:60]}...' -> Recall@3: {recall:.3f}") + print(f" FAISS: {list(baseline_ids)}") + print(f" LEANN: {list(test_ids)}") + print(f" ∩: {list(intersection)}") + + avg = total_recall / max(1, len(queries)) + print(f"πŸ“Š Average Recall@3: {avg:.3f} ({avg * 100:.1f}%)") + return avg + + def cleanup(self): + if hasattr(self, "searcher"): + self.searcher.cleanup() + + +class EnronEvaluator: + def __init__(self, index_path: str): + self.index_path = index_path + self.searcher = LeannSearcher(index_path) + + def load_queries(self, queries_file: str) -> list[str]: + queries: list[str] = [] + with open(queries_file, encoding="utf-8") as f: + for line in f: + if not line.strip(): + continue + data = json.loads(line) + if "query" in data: + queries.append(data["query"]) + print(f"πŸ“Š Loaded {len(queries)} queries from {queries_file}") + return queries + + def cleanup(self): + if self.searcher: + self.searcher.cleanup() + + def analyze_index_sizes(self) -> dict: + """Analyze index sizes (.index only), similar to LAION bench.""" + + print("πŸ“ Analyzing index sizes (.index only)...") + index_path = Path(self.index_path) + index_dir = index_path.parent + index_name = index_path.stem + + sizes: dict[str, float] = {} + index_file = index_dir / f"{index_name}.index" + meta_file = index_dir / f"{index_path.name}.meta.json" + passages_file = index_dir / f"{index_path.name}.passages.jsonl" + passages_idx_file = index_dir / f"{index_path.name}.passages.idx" + + sizes["index_only_mb"] = ( + index_file.stat().st_size / (1024 * 1024) if index_file.exists() else 0.0 + ) + sizes["metadata_mb"] = ( + meta_file.stat().st_size / (1024 * 1024) if meta_file.exists() else 0.0 + ) + sizes["passages_text_mb"] = ( + passages_file.stat().st_size / (1024 * 1024) if passages_file.exists() else 0.0 + ) + sizes["passages_index_mb"] = ( + passages_idx_file.stat().st_size / (1024 * 1024) if passages_idx_file.exists() else 0.0 + ) + + print(f" πŸ“ .index size: {sizes['index_only_mb']:.1f} MB") + return sizes + + def create_non_compact_index_for_comparison(self, non_compact_index_path: str) -> dict: + """Create a non-compact index for comparison using current passages and embeddings.""" + + current_index_path = Path(self.index_path) + current_index_dir = current_index_path.parent + current_index_name = current_index_path.name + + # Read metadata to get passage source and embedding model + meta_path = current_index_dir / f"{current_index_name}.meta.json" + with open(meta_path, encoding="utf-8") as f: + meta = json.load(f) + + passage_source = meta["passage_sources"][0] + passage_file = passage_source["path"] + + # Convert relative path to absolute + if not Path(passage_file).is_absolute(): + passage_file = current_index_dir / Path(passage_file).name + + # Load all passages and ids + ids: list[str] = [] + texts: list[str] = [] + with open(passage_file, encoding="utf-8") as f: + for line in f: + if line.strip(): + data = json.loads(line) + ids.append(str(data["id"])) + texts.append(data["text"]) + + # Compute embeddings using the same method as LEANN + from leann.api import compute_embeddings + + embeddings = compute_embeddings( + texts, + meta["embedding_model"], + mode=meta.get("embedding_mode", "sentence-transformers"), + use_server=False, + ).astype(np.float32) + + # Build non-compact index with same passages and embeddings + builder = LeannBuilder( + backend_name="hnsw", + embedding_model=meta["embedding_model"], + embedding_mode=meta.get("embedding_mode", "sentence-transformers"), + is_recompute=False, + is_compact=False, + **{ + k: v + for k, v in meta.get("backend_kwargs", {}).items() + if k not in ["is_recompute", "is_compact"] + }, + ) + + # Persist a pickle for build_index_from_embeddings + pkl_path = current_index_dir / f"{Path(non_compact_index_path).stem}_embeddings.pkl" + with open(pkl_path, "wb") as pf: + pickle.dump((ids, embeddings), pf) + + print( + f"πŸ”¨ Building non-compact index at {non_compact_index_path} from precomputed embeddings..." + ) + builder.build_index_from_embeddings(non_compact_index_path, str(pkl_path)) + + # Analyze the non-compact index size + temp_evaluator = EnronEvaluator(non_compact_index_path) + non_compact_sizes = temp_evaluator.analyze_index_sizes() + non_compact_sizes["index_type"] = "non_compact" + + return non_compact_sizes + + def compare_index_performance( + self, non_compact_path: str, compact_path: str, test_queries: list[str], complexity: int + ) -> dict: + """Compare search speed for non-compact vs compact indexes.""" + import time + + results: dict = { + "non_compact": {"search_times": []}, + "compact": {"search_times": []}, + "avg_search_times": {}, + "speed_ratio": 0.0, + "retrieval_results": [], # Store retrieval results for Stage 5 + } + + print("⚑ Comparing search performance between indexes...") + # Non-compact (no recompute) + print(" πŸ” Testing non-compact index (no recompute)...") + non_compact_searcher = LeannSearcher(non_compact_path) + for q in test_queries: + t0 = time.time() + _ = non_compact_searcher.search( + q, top_k=3, complexity=complexity, recompute_embeddings=False + ) + results["non_compact"]["search_times"].append(time.time() - t0) + + # Compact (with recompute). Fail fast if it cannot run. + print(" πŸ” Testing compact index (with recompute)...") + compact_searcher = LeannSearcher(compact_path) + for q in test_queries: + t0 = time.time() + docs = compact_searcher.search( + q, top_k=3, complexity=complexity, recompute_embeddings=True + ) + results["compact"]["search_times"].append(time.time() - t0) + + # Store retrieval results for Stage 5 + results["retrieval_results"].append( + {"query": q, "retrieved_docs": [{"id": doc.id, "text": doc.text} for doc in docs]} + ) + compact_searcher.cleanup() + + if results["non_compact"]["search_times"]: + results["avg_search_times"]["non_compact"] = sum( + results["non_compact"]["search_times"] + ) / len(results["non_compact"]["search_times"]) + if results["compact"]["search_times"]: + results["avg_search_times"]["compact"] = sum(results["compact"]["search_times"]) / len( + results["compact"]["search_times"] + ) + if results["avg_search_times"].get("compact", 0) > 0: + results["speed_ratio"] = ( + results["avg_search_times"]["non_compact"] / results["avg_search_times"]["compact"] + ) + else: + results["speed_ratio"] = 0.0 + + non_compact_searcher.cleanup() + return results + + def evaluate_complexity( + self, + recall_eval: "RecallEvaluator", + queries: list[str], + target: float = 0.90, + c_min: int = 8, + c_max: int = 256, + max_iters: int = 10, + recompute: bool = False, + ) -> dict: + """Binary search minimal complexity achieving target recall (monotonic assumption).""" + + def round_c(x: int) -> int: + # snap to multiple of 8 like other benches typically do + return max(1, int((x + 7) // 8) * 8) + + metrics: list[dict] = [] + + lo = round_c(c_min) + hi = round_c(c_max) + + print( + f"πŸ§ͺ Binary search complexity in [{lo}, {hi}] for target Recall@3>={int(target * 100)}%..." + ) + + # Ensure upper bound can reach target; expand if needed (up to a cap) + r_lo = recall_eval.evaluate_recall_at_3( + queries, complexity=lo, recompute_embeddings=recompute + ) + metrics.append({"complexity": lo, "recall_at_3": r_lo}) + r_hi = recall_eval.evaluate_recall_at_3( + queries, complexity=hi, recompute_embeddings=recompute + ) + metrics.append({"complexity": hi, "recall_at_3": r_hi}) + + cap = 1024 + while r_hi < target and hi < cap: + lo = hi + r_lo = r_hi + hi = round_c(hi * 2) + r_hi = recall_eval.evaluate_recall_at_3( + queries, complexity=hi, recompute_embeddings=recompute + ) + metrics.append({"complexity": hi, "recall_at_3": r_hi}) + + if r_hi < target: + print(f"⚠️ Max complexity {hi} did not reach target recall {target:.2f}.") + print("πŸ“ˆ Observations:") + for m in metrics: + print(f" C={m['complexity']:>4} -> Recall@3={m['recall_at_3'] * 100:.1f}%") + return {"metrics": metrics, "best_complexity": None, "target_recall": target} + + # Binary search within [lo, hi] + best = hi + iters = 0 + while lo < hi and iters < max_iters: + mid = round_c((lo + hi) // 2) + r_mid = recall_eval.evaluate_recall_at_3( + queries, complexity=mid, recompute_embeddings=recompute + ) + metrics.append({"complexity": mid, "recall_at_3": r_mid}) + if r_mid >= target: + best = mid + hi = mid + else: + lo = mid + 8 # move past mid, respecting multiple-of-8 step + iters += 1 + + print("πŸ“ˆ Binary search results (sampled points):") + # Print unique complexity entries ordered by complexity + for m in sorted( + {m["complexity"]: m for m in metrics}.values(), key=lambda x: x["complexity"] + ): + print(f" C={m['complexity']:>4} -> Recall@3={m['recall_at_3'] * 100:.1f}%") + print(f"βœ… Minimal complexity achieving {int(target * 100)}% recall: {best}") + return {"metrics": metrics, "best_complexity": best, "target_recall": target} + + +def main(): + parser = argparse.ArgumentParser(description="Enron Emails Benchmark Evaluation") + parser.add_argument("--index", required=True, help="Path to LEANN index") + parser.add_argument( + "--queries", default="data/evaluation_queries.jsonl", help="Path to evaluation queries" + ) + parser.add_argument( + "--stage", + choices=["2", "3", "4", "5", "all", "45"], + default="all", + help="Which stage to run (2=recall, 3=complexity, 4=index comparison, 5=generation)", + ) + parser.add_argument("--complexity", type=int, default=None, help="LEANN search complexity") + parser.add_argument("--baseline-dir", default="baseline", help="Baseline output directory") + parser.add_argument( + "--max-queries", type=int, help="Limit number of queries to evaluate", default=1000 + ) + parser.add_argument( + "--target-recall", type=float, default=0.90, help="Target Recall@3 for Stage 3" + ) + parser.add_argument("--output", help="Save results to JSON file") + parser.add_argument("--llm-backend", choices=["hf", "vllm"], default="hf", help="LLM backend") + parser.add_argument("--model-name", default="Qwen/Qwen3-8B", help="Model name") + + args = parser.parse_args() + + # Resolve queries file: if default path not found, fall back to index's directory + if not os.path.exists(args.queries): + from pathlib import Path + + idx_dir = Path(args.index).parent + fallback_q = idx_dir / "evaluation_queries.jsonl" + if fallback_q.exists(): + args.queries = str(fallback_q) + + baseline_index_path = os.path.join(args.baseline_dir, "faiss_flat.index") + if not os.path.exists(baseline_index_path): + print(f"❌ FAISS baseline not found at {baseline_index_path}") + print("πŸ’‘ Please run setup_enron_emails.py first to build the baseline") + raise SystemExit(1) + + results_out: dict = {} + + if args.stage in ("2", "all"): + print("πŸš€ Starting Stage 2: Recall@3 evaluation") + evaluator = RecallEvaluator(args.index, args.baseline_dir) + + enron_eval = EnronEvaluator(args.index) + queries = enron_eval.load_queries(args.queries) + queries = queries[:10] + print(f"πŸ§ͺ Using first {len(queries)} queries") + + complexity = args.complexity or 64 + r = evaluator.evaluate_recall_at_3(queries, complexity) + results_out["stage2"] = {"complexity": complexity, "recall_at_3": r} + evaluator.cleanup() + enron_eval.cleanup() + print("βœ… Stage 2 completed!\n") + + if args.stage in ("3", "all"): + print("πŸš€ Starting Stage 3: Binary search for target recall (no recompute)") + enron_eval = EnronEvaluator(args.index) + queries = enron_eval.load_queries(args.queries) + queries = queries[: args.max_queries] + print(f"πŸ§ͺ Using first {len(queries)} queries") + + # Build non-compact index for fast binary search (recompute_embeddings=False) + from pathlib import Path + + index_path = Path(args.index) + non_compact_index_path = str(index_path.parent / f"{index_path.stem}_noncompact.leann") + enron_eval.create_non_compact_index_for_comparison(non_compact_index_path) + + # Use non-compact evaluator for binary search with recompute=False + evaluator_nc = RecallEvaluator(non_compact_index_path, args.baseline_dir) + sweep = enron_eval.evaluate_complexity( + evaluator_nc, queries, target=args.target_recall, recompute=False + ) + results_out["stage3"] = sweep + # Persist default stage 3 results near the index for Stage 4 auto-pickup + from pathlib import Path + + default_stage3_path = Path(args.index).parent / "enron_stage3_results.json" + with open(default_stage3_path, "w", encoding="utf-8") as f: + json.dump({"stage3": sweep}, f, indent=2) + print(f"πŸ“ Saved Stage 3 summary to {default_stage3_path}") + evaluator_nc.cleanup() + enron_eval.cleanup() + print("βœ… Stage 3 completed!\n") + + if args.stage in ("4", "all", "45"): + print("πŸš€ Starting Stage 4: Index size + performance comparison") + evaluator = RecallEvaluator(args.index, args.baseline_dir) + enron_eval = EnronEvaluator(args.index) + queries = enron_eval.load_queries(args.queries) + test_q = queries[: min(args.max_queries, len(queries))] + + current_sizes = enron_eval.analyze_index_sizes() + # Build non-compact index for comparison (no fallback) + from pathlib import Path + + index_path = Path(args.index) + non_compact_path = str(index_path.parent / f"{index_path.stem}_noncompact.leann") + non_compact_sizes = enron_eval.create_non_compact_index_for_comparison(non_compact_path) + nc_eval = EnronEvaluator(non_compact_path) + + if ( + current_sizes.get("index_only_mb", 0) > 0 + and non_compact_sizes.get("index_only_mb", 0) > 0 + ): + storage_saving_percent = max( + 0.0, + 100.0 * (1.0 - current_sizes["index_only_mb"] / non_compact_sizes["index_only_mb"]), + ) + else: + storage_saving_percent = 0.0 + + if args.complexity is None: + # Prefer in-session Stage 3 result + if "stage3" in results_out and results_out["stage3"].get("best_complexity") is not None: + complexity = results_out["stage3"]["best_complexity"] + print(f"πŸ“₯ Using best complexity from Stage 3 in-session: {complexity}") + else: + # Try to load last saved Stage 3 result near index + default_stage3_path = Path(args.index).parent / "enron_stage3_results.json" + if default_stage3_path.exists(): + with open(default_stage3_path, encoding="utf-8") as f: + prev = json.load(f) + complexity = prev.get("stage3", {}).get("best_complexity") + if complexity is None: + raise SystemExit( + "❌ Stage 4: No --complexity and no best_complexity found in saved Stage 3 results" + ) + print(f"πŸ“₯ Using best complexity from saved Stage 3: {complexity}") + else: + raise SystemExit( + "❌ Stage 4 requires --complexity if Stage 3 hasn't been run. Run stage 3 first or pass --complexity." + ) + else: + complexity = args.complexity + + comp = enron_eval.compare_index_performance( + non_compact_path, args.index, test_q, complexity=complexity + ) + results_out["stage4"] = { + "current_index": current_sizes, + "non_compact_index": non_compact_sizes, + "storage_saving_percent": storage_saving_percent, + "performance_comparison": comp, + } + nc_eval.cleanup() + evaluator.cleanup() + enron_eval.cleanup() + print("βœ… Stage 4 completed!\n") + + if args.stage in ("5", "all"): + print("πŸš€ Starting Stage 5: Generation evaluation with Qwen3-8B") + + # Check if Stage 4 results exist + if "stage4" not in results_out or "performance_comparison" not in results_out["stage4"]: + print("❌ Stage 5 requires Stage 4 retrieval results") + print("πŸ’‘ Run Stage 4 first or use --stage all") + raise SystemExit(1) + + retrieval_results = results_out["stage4"]["performance_comparison"]["retrieval_results"] + if not retrieval_results: + print("❌ No retrieval results found from Stage 4") + raise SystemExit(1) + + print(f"πŸ“ Using {len(retrieval_results)} retrieval results from Stage 4") + + # Load LLM + try: + if args.llm_backend == "hf": + tokenizer, model = load_hf_model(args.model_name) + + def llm_func(prompt): + return generate_hf(tokenizer, model, prompt) + else: # vllm + llm, sampling_params = load_vllm_model(args.model_name) + + def llm_func(prompt): + return generate_vllm(llm, sampling_params, prompt) + + # Run generation using stored retrieval results + import time + + from llm_utils import create_prompt + + generation_times = [] + responses = [] + + print("πŸ€– Running generation on pre-retrieved results...") + for i, item in enumerate(retrieval_results): + query = item["query"] + retrieved_docs = item["retrieved_docs"] + + # Prepare context from retrieved docs + context = "\n\n".join([doc["text"] for doc in retrieved_docs]) + prompt = create_prompt(context, query, "emails") + + # Time generation only + gen_start = time.time() + response = llm_func(prompt) + gen_time = time.time() - gen_start + + generation_times.append(gen_time) + responses.append(response) + + if i < 3: + print(f" Q{i + 1}: Gen={gen_time:.3f}s") + + avg_gen_time = sum(generation_times) / len(generation_times) + + print("\nπŸ“Š Generation Results:") + print(f" Total Queries: {len(retrieval_results)}") + print(f" Avg Generation Time: {avg_gen_time:.3f}s") + print(" (Search time from Stage 4)") + + results_out["stage5"] = { + "total_queries": len(retrieval_results), + "avg_generation_time": avg_gen_time, + "generation_times": generation_times, + "responses": responses, + } + + # Show sample results + print("\nπŸ“ Sample Results:") + for i in range(min(3, len(retrieval_results))): + query = retrieval_results[i]["query"] + response = responses[i] + print(f" Q{i + 1}: {query[:60]}...") + print(f" A{i + 1}: {response[:100]}...") + print() + + except Exception as e: + print(f"❌ Generation evaluation failed: {e}") + print("πŸ’‘ Make sure transformers/vllm is installed and model is available") + + print("βœ… Stage 5 completed!\n") + + if args.output and results_out: + with open(args.output, "w", encoding="utf-8") as f: + json.dump(results_out, f, indent=2) + print(f"πŸ“ Saved results to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/enron_emails/setup_enron_emails.py b/benchmarks/enron_emails/setup_enron_emails.py new file mode 100644 index 0000000..ca88748 --- /dev/null +++ b/benchmarks/enron_emails/setup_enron_emails.py @@ -0,0 +1,359 @@ +""" +Enron Emails Benchmark Setup Script +Prepares passages from emails.csv, builds LEANN index, and FAISS Flat baseline +""" + +import argparse +import csv +import json +import os +import re +from collections.abc import Iterable +from email import message_from_string +from email.policy import default +from pathlib import Path +from typing import Optional + +from leann import LeannBuilder + + +class EnronSetup: + def __init__(self, data_dir: str = "data"): + self.data_dir = Path(data_dir) + self.data_dir.mkdir(parents=True, exist_ok=True) + + self.passages_preview = self.data_dir / "enron_passages_preview.jsonl" + self.index_path = self.data_dir / "enron_index_hnsw.leann" + self.queries_file = self.data_dir / "evaluation_queries.jsonl" + self.downloads_dir = self.data_dir / "downloads" + self.downloads_dir.mkdir(parents=True, exist_ok=True) + + # ---------------------------- + # Dataset acquisition + # ---------------------------- + def ensure_emails_csv(self, emails_csv: Optional[str]) -> str: + """Return a path to emails.csv, downloading from Kaggle if needed.""" + if emails_csv: + p = Path(emails_csv) + if not p.exists(): + raise FileNotFoundError(f"emails.csv not found: {emails_csv}") + return str(p) + + print( + "πŸ“₯ Trying to download Enron emails.csv from Kaggle (wcukierski/enron-email-dataset)..." + ) + try: + from kaggle.api.kaggle_api_extended import KaggleApi + + api = KaggleApi() + api.authenticate() + api.dataset_download_files( + "wcukierski/enron-email-dataset", path=str(self.downloads_dir), unzip=True + ) + candidate = self.downloads_dir / "emails.csv" + if candidate.exists(): + print(f"βœ… Downloaded emails.csv: {candidate}") + return str(candidate) + else: + raise FileNotFoundError( + f"emails.csv was not found in {self.downloads_dir} after Kaggle download" + ) + except Exception as e: + print( + "❌ Could not download via Kaggle automatically. Provide --emails-csv or configure Kaggle API." + ) + print( + " Set KAGGLE_USERNAME and KAGGLE_KEY env vars, or place emails.csv locally and pass --emails-csv." + ) + raise e + + # ---------------------------- + # Data preparation + # ---------------------------- + @staticmethod + def _extract_message_id(raw_email: str) -> str: + msg = message_from_string(raw_email, policy=default) + val = msg.get("Message-ID", "") + if val.startswith("<") and val.endswith(">"): + val = val[1:-1] + return val or "" + + @staticmethod + def _split_header_body(raw_email: str) -> tuple[str, str]: + parts = raw_email.split("\n\n", 1) + if len(parts) == 2: + return parts[0].strip(), parts[1].strip() + # Heuristic fallback + first_lines = raw_email.splitlines() + if first_lines and ":" in first_lines[0]: + return raw_email.strip(), "" + return "", raw_email.strip() + + @staticmethod + def _split_fixed_words(text: str, chunk_words: int, keep_last: bool) -> list[str]: + text = (text or "").strip() + if not text: + return [] + if chunk_words <= 0: + return [text] + words = text.split() + if not words: + return [] + limit = len(words) + if not keep_last: + limit = (len(words) // chunk_words) * chunk_words + if limit == 0: + return [] + chunks = [" ".join(words[i : i + chunk_words]) for i in range(0, limit, chunk_words)] + return [c for c in (s.strip() for s in chunks) if c] + + def _iter_passages_from_csv( + self, + emails_csv: Path, + chunk_words: int = 256, + keep_last_header: bool = True, + keep_last_body: bool = True, + max_emails: int | None = None, + ) -> Iterable[dict]: + with open(emails_csv, encoding="utf-8") as f: + reader = csv.DictReader(f) + count = 0 + for i, row in enumerate(reader): + if max_emails is not None and count >= max_emails: + break + + raw_message = row.get("message", "") + email_file_id = row.get("file", "") + + if not raw_message.strip(): + continue + + message_id = self._extract_message_id(raw_message) + if not message_id: + # Fallback ID based on CSV position and file path + safe_file = re.sub(r"[^A-Za-z0-9_.-]", "_", email_file_id) + message_id = f"enron_{i}_{safe_file}" + + header, body = self._split_header_body(raw_message) + + # Header chunks + for chunk in self._split_fixed_words(header, chunk_words, keep_last_header): + yield { + "text": chunk, + "metadata": { + "message_id": message_id, + "is_header": True, + "email_file_id": email_file_id, + }, + } + + # Body chunks + for chunk in self._split_fixed_words(body, chunk_words, keep_last_body): + yield { + "text": chunk, + "metadata": { + "message_id": message_id, + "is_header": False, + "email_file_id": email_file_id, + }, + } + + count += 1 + + # ---------------------------- + # Build LEANN index and FAISS baseline + # ---------------------------- + def build_leann_index( + self, + emails_csv: Optional[str], + backend: str = "hnsw", + embedding_model: str = "sentence-transformers/all-mpnet-base-v2", + chunk_words: int = 256, + max_emails: int | None = None, + ) -> str: + emails_csv_path = self.ensure_emails_csv(emails_csv) + print(f"πŸ—οΈ Building LEANN index from {emails_csv_path}...") + + builder = LeannBuilder( + backend_name=backend, + embedding_model=embedding_model, + embedding_mode="sentence-transformers", + graph_degree=32, + complexity=64, + is_recompute=True, + is_compact=True, + num_threads=4, + ) + + # Stream passages and add to builder + preview_written = 0 + with open(self.passages_preview, "w", encoding="utf-8") as preview_out: + for p in self._iter_passages_from_csv( + Path(emails_csv_path), chunk_words=chunk_words, max_emails=max_emails + ): + builder.add_text(p["text"], metadata=p["metadata"]) + if preview_written < 200: + preview_out.write(json.dumps({"text": p["text"][:200], **p["metadata"]}) + "\n") + preview_written += 1 + + print(f"πŸ”¨ Building index at {self.index_path}...") + builder.build_index(str(self.index_path)) + print("βœ… LEANN index built!") + return str(self.index_path) + + def build_faiss_flat_baseline(self, index_path: str, output_dir: str = "baseline") -> str: + print("πŸ”¨ Building FAISS Flat baseline from LEANN passages...") + + import pickle + + import numpy as np + from leann.api import compute_embeddings + from leann_backend_hnsw import faiss + + os.makedirs(output_dir, exist_ok=True) + baseline_path = os.path.join(output_dir, "faiss_flat.index") + metadata_path = os.path.join(output_dir, "metadata.pkl") + + if os.path.exists(baseline_path) and os.path.exists(metadata_path): + print(f"βœ… Baseline already exists at {baseline_path}") + return baseline_path + + # Read meta for passage source and embedding model + meta_path = f"{index_path}.meta.json" + with open(meta_path, encoding="utf-8") as f: + meta = json.load(f) + + embedding_model = meta["embedding_model"] + passage_source = meta["passage_sources"][0] + passage_file = passage_source["path"] + + if not os.path.isabs(passage_file): + index_dir = os.path.dirname(index_path) + passage_file = os.path.join(index_dir, os.path.basename(passage_file)) + + # Load passages from builder output so IDs match LEANN + passages: list[str] = [] + passage_ids: list[str] = [] + with open(passage_file, encoding="utf-8") as f: + for line in f: + if not line.strip(): + continue + data = json.loads(line) + passages.append(data["text"]) + passage_ids.append(data["id"]) # builder-assigned ID + + print(f"πŸ“„ Loaded {len(passages)} passages for baseline") + print(f"πŸ€– Embedding model: {embedding_model}") + + embeddings = compute_embeddings( + passages, + embedding_model, + mode="sentence-transformers", + use_server=False, + ) + + # Build FAISS IndexFlatIP + dim = embeddings.shape[1] + index = faiss.IndexFlatIP(dim) + emb_f32 = embeddings.astype(np.float32) + index.add(emb_f32.shape[0], faiss.swig_ptr(emb_f32)) + + faiss.write_index(index, baseline_path) + with open(metadata_path, "wb") as pf: + pickle.dump(passage_ids, pf) + + print(f"βœ… FAISS baseline saved: {baseline_path}") + print(f"βœ… Metadata saved: {metadata_path}") + print(f"πŸ“Š Total vectors: {index.ntotal}") + return baseline_path + + # ---------------------------- + # Queries (optional): prepare evaluation queries file + # ---------------------------- + def prepare_queries(self, min_realism: float = 0.85) -> Path: + print( + "πŸ“ Preparing evaluation queries from HuggingFace dataset corbt/enron_emails_sample_questions ..." + ) + try: + from datasets import load_dataset + + ds = load_dataset("corbt/enron_emails_sample_questions", split="train") + except Exception as e: + print(f"⚠️ Failed to load dataset: {e}") + return self.queries_file + + kept = 0 + with open(self.queries_file, "w", encoding="utf-8") as out: + for i, item in enumerate(ds): + how_realistic = float(item.get("how_realistic", 0.0)) + if how_realistic < min_realism: + continue + qid = str(item.get("id", f"enron_q_{i}")) + query = item.get("question", "") + if not query: + continue + record = { + "id": qid, + "query": query, + # For reference only, not used in recall metric below + "gt_message_ids": item.get("message_ids", []), + } + out.write(json.dumps(record) + "\n") + kept += 1 + print(f"βœ… Wrote {kept} queries to {self.queries_file}") + return self.queries_file + + +def main(): + parser = argparse.ArgumentParser(description="Setup Enron Emails Benchmark") + parser.add_argument( + "--emails-csv", + help="Path to emails.csv (Enron dataset). If omitted, attempt Kaggle download.", + ) + parser.add_argument("--data-dir", default="data", help="Data directory") + parser.add_argument("--backend", choices=["hnsw", "diskann"], default="hnsw") + parser.add_argument( + "--embedding-model", + default="sentence-transformers/all-mpnet-base-v2", + help="Embedding model for LEANN", + ) + parser.add_argument("--chunk-words", type=int, default=256, help="Fixed word chunk size") + parser.add_argument("--max-emails", type=int, help="Limit number of emails to process") + parser.add_argument("--skip-queries", action="store_true", help="Skip creating queries file") + parser.add_argument("--skip-build", action="store_true", help="Skip building LEANN index") + + args = parser.parse_args() + + setup = EnronSetup(args.data_dir) + + # Build index + if not args.skip_build: + index_path = setup.build_leann_index( + emails_csv=args.emails_csv, + backend=args.backend, + embedding_model=args.embedding_model, + chunk_words=args.chunk_words, + max_emails=args.max_emails, + ) + + # Build FAISS baseline from the same passages & embeddings + setup.build_faiss_flat_baseline(index_path) + else: + print("⏭️ Skipping LEANN index build and baseline") + + # Queries file (optional) + if not args.skip_queries: + setup.prepare_queries() + else: + print("⏭️ Skipping query preparation") + + print("\nπŸŽ‰ Enron Emails setup completed!") + print(f"πŸ“ Data directory: {setup.data_dir.absolute()}") + print("Next steps:") + print( + "1) Evaluate recall: python evaluate_enron_emails.py --index data/enron_index_hnsw.leann --stage 2" + ) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/faiss_only.py b/benchmarks/faiss_only.py new file mode 100644 index 0000000..c501c10 --- /dev/null +++ b/benchmarks/faiss_only.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Test only Faiss HNSW""" + +import os +import sys +import time + +import psutil + + +def get_memory_usage(): + process = psutil.Process() + return process.memory_info().rss / 1024 / 1024 + + +class MemoryTracker: + def __init__(self, name: str): + self.name = name + self.start_mem = get_memory_usage() + self.stages = [] + + def checkpoint(self, stage: str): + current_mem = get_memory_usage() + diff = current_mem - self.start_mem + print(f"[{self.name} - {stage}] Memory: {current_mem:.1f} MB (+{diff:.1f} MB)") + self.stages.append((stage, current_mem)) + return current_mem + + def summary(self): + peak_mem = max(mem for _, mem in self.stages) + print(f"Peak Memory: {peak_mem:.1f} MB") + return peak_mem + + +def main(): + try: + import faiss + except ImportError: + print("Faiss is not installed.") + print( + "Please install it with `uv pip install faiss-cpu` and you can then run this script again" + ) + sys.exit(1) + + from llama_index.core import ( + Settings, + SimpleDirectoryReader, + StorageContext, + VectorStoreIndex, + ) + from llama_index.core.node_parser import SentenceSplitter + from llama_index.embeddings.huggingface import HuggingFaceEmbedding + from llama_index.vector_stores.faiss import FaissVectorStore + + tracker = MemoryTracker("Faiss HNSW") + tracker.checkpoint("Initial") + + embed_model = HuggingFaceEmbedding(model_name="facebook/contriever") + Settings.embed_model = embed_model + tracker.checkpoint("After embedding model setup") + + d = 768 + faiss_index = faiss.IndexHNSWFlat(d, 32) + faiss_index.hnsw.efConstruction = 64 + tracker.checkpoint("After Faiss index creation") + + documents = SimpleDirectoryReader( + "data", + recursive=True, + encoding="utf-8", + required_exts=[".pdf", ".txt", ".md"], + ).load_data() + tracker.checkpoint("After document loading") + + # Parse into chunks using the same splitter as LEANN + node_parser = SentenceSplitter( + chunk_size=256, chunk_overlap=20, separator=" ", paragraph_separator="\n\n" + ) + + tracker.checkpoint("After text splitter setup") + + # Check if index already exists and try to load it + index_loaded = False + if os.path.exists("./storage_faiss"): + print("Loading existing Faiss HNSW index...") + try: + # Use the correct Faiss loading pattern from the example + vector_store = FaissVectorStore.from_persist_dir("./storage_faiss") + storage_context = StorageContext.from_defaults( + vector_store=vector_store, persist_dir="./storage_faiss" + ) + from llama_index.core import load_index_from_storage + + index = load_index_from_storage(storage_context=storage_context) + print("Index loaded from ./storage_faiss") + tracker.checkpoint("After loading existing index") + index_loaded = True + except Exception as e: + print(f"Failed to load existing index: {e}") + print("Cleaning up corrupted index and building new one...") + # Clean up corrupted index + import shutil + + if os.path.exists("./storage_faiss"): + shutil.rmtree("./storage_faiss") + + if not index_loaded: + print("Building new Faiss HNSW index...") + + # Use the correct Faiss building pattern from the example + vector_store = FaissVectorStore(faiss_index=faiss_index) + storage_context = StorageContext.from_defaults(vector_store=vector_store) + index = VectorStoreIndex.from_documents( + documents, storage_context=storage_context, transformations=[node_parser] + ) + tracker.checkpoint("After index building") + + # Save index to disk using the correct pattern + index.storage_context.persist(persist_dir="./storage_faiss") + tracker.checkpoint("After index saving") + + # Measure runtime memory overhead + print("\nMeasuring runtime memory overhead...") + runtime_start_mem = get_memory_usage() + print(f"Before load memory: {runtime_start_mem:.1f} MB") + tracker.checkpoint("Before load memory") + + query_engine = index.as_query_engine(similarity_top_k=20) + queries = [ + "δ»€δΉˆζ˜―η›˜ε€ε€§ζ¨‘εž‹δ»₯εŠη›˜ε€εΌ€ε‘θΏ‡η¨‹δΈ­ι‡εˆ°δΊ†δ»€δΉˆι˜΄ζš—ι’,δ»»εŠ‘δ»€δΈ€θˆ¬εœ¨δ»€δΉˆεŸŽεΈ‚ι’ε‘", + "What is LEANN and how does it work?", + "εŽδΈΊθ―ΊδΊšζ–ΉθˆŸεžιͺŒε€ηš„主要研穢内εΉ", + ] + + for i, query in enumerate(queries): + start_time = time.time() + _ = query_engine.query(query) + query_time = time.time() - start_time + print(f"Query {i + 1} time: {query_time:.3f}s") + tracker.checkpoint(f"After query {i + 1}") + + runtime_end_mem = get_memory_usage() + runtime_overhead = runtime_end_mem - runtime_start_mem + + peak_memory = tracker.summary() + print(f"Peak Memory: {peak_memory:.1f} MB") + print(f"Runtime Memory Overhead: {runtime_overhead:.1f} MB") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/financebench/README.md b/benchmarks/financebench/README.md new file mode 100644 index 0000000..aefe9d6 --- /dev/null +++ b/benchmarks/financebench/README.md @@ -0,0 +1,115 @@ +# FinanceBench Benchmark for LEANN-RAG + +FinanceBench is a benchmark for evaluating retrieval-augmented generation (RAG) systems on financial document question-answering tasks. + +## Dataset + +- **Source**: [PatronusAI/financebench](https://huggingface.co/datasets/PatronusAI/financebench) +- **Questions**: 150 financial Q&A examples +- **Documents**: 368 PDF files (10-K, 10-Q, 8-K, earnings reports) +- **Companies**: Major public companies (3M, Apple, Microsoft, Amazon, etc.) +- **Paper**: [FinanceBench: A New Benchmark for Financial Question Answering](https://arxiv.org/abs/2311.11944) + +## Structure + +``` +benchmarks/financebench/ +β”œβ”€β”€ setup_financebench.py # Downloads PDFs and builds index +β”œβ”€β”€ evaluate_financebench.py # Intelligent evaluation script +β”œβ”€β”€ data/ +β”‚ β”œβ”€β”€ financebench_merged.jsonl # Q&A dataset +β”‚ β”œβ”€β”€ pdfs/ # Downloaded financial documents +β”‚ └── index/ # LEANN indexes +β”‚ └── financebench_full_hnsw.leann +└── README.md +``` + +## Usage + +### 1. Setup (Download & Build Index) + +```bash +cd benchmarks/financebench +python setup_financebench.py +``` + +This will: +- Download the 150 Q&A examples +- Download all 368 PDF documents (parallel processing) +- Build a LEANN index from 53K+ text chunks +- Verify setup with test query + +### 2. Evaluation + +```bash +# Basic retrieval evaluation +python evaluate_financebench.py --index data/index/financebench_full_hnsw.leann + + +# RAG generation evaluation with Qwen3-8B +python evaluate_financebench.py --index data/index/financebench_full_hnsw.leann --stage 4 --complexity 64 --llm-backend hf --model-name Qwen/Qwen3-8B --output results_qwen3.json +``` + +## Evaluation Methods + +### Retrieval Evaluation +Uses intelligent matching with three strategies: +1. **Exact text overlap** - Direct substring matches +2. **Number matching** - Key financial figures ($1,577, 1.2B, etc.) +3. **Semantic similarity** - Word overlap with 20% threshold + +### QA Evaluation +LLM-based answer evaluation using GPT-4o: +- Handles numerical rounding and equivalent representations +- Considers fractions, percentages, and decimal equivalents +- Evaluates semantic meaning rather than exact text match + +## Benchmark Results + +### LEANN-RAG Performance (sentence-transformers/all-mpnet-base-v2) + +**Retrieval Metrics:** +- **Question Coverage**: 100.0% (all questions retrieve relevant docs) +- **Exact Match Rate**: 0.7% (substring overlap with evidence) +- **Number Match Rate**: 120.7% (key financial figures matched)* +- **Semantic Match Rate**: 4.7% (word overlap β‰₯20%) +- **Average Search Time**: 0.097s + +**QA Metrics:** +- **Accuracy**: 42.7% (LLM-evaluated answer correctness) +- **Average QA Time**: 4.71s (end-to-end response time) + +**System Performance:** +- **Index Size**: 53,985 chunks from 368 PDFs +- **Build Time**: ~5-10 minutes with sentence-transformers/all-mpnet-base-v2 + +*Note: Number match rate >100% indicates multiple retrieved documents contain the same financial figures, which is expected behavior for financial data appearing across multiple document sections. + +### LEANN-RAG Generation Performance (Qwen3-8B) + +- **Stage 4 (Index Comparison):** + - Compact Index: 5.0 MB + - Non-compact Index: 172.2 MB + - **Storage Saving**: 97.1% +- **Search Performance**: + - Non-compact (no recompute): 0.009s avg per query + - Compact (with recompute): 2.203s avg per query + - Speed ratio: 0.004x + +**Generation Evaluation (20 queries, complexity=64):** +- **Average Search Time**: 1.638s per query +- **Average Generation Time**: 45.957s per query +- **LLM Backend**: HuggingFace transformers +- **Model**: Qwen/Qwen3-8B (thinking model with processing) +- **Total Questions Processed**: 20 + +## Options + +```bash +# Use different backends +python setup_financebench.py --backend diskann +python evaluate_financebench.py --index data/index/financebench_full_diskann.leann + +# Use different embedding models +python setup_financebench.py --embedding-model facebook/contriever +``` diff --git a/benchmarks/financebench/evaluate_financebench.py b/benchmarks/financebench/evaluate_financebench.py new file mode 100755 index 0000000..8c759d1 --- /dev/null +++ b/benchmarks/financebench/evaluate_financebench.py @@ -0,0 +1,925 @@ +""" +FinanceBench Evaluation Script - Modular Recall-based Evaluation +""" + +import argparse +import json +import logging +import os +import pickle +import time +from pathlib import Path +from typing import Optional + +import numpy as np +import openai +from leann import LeannChat, LeannSearcher +from leann_backend_hnsw import faiss + +from ..llm_utils import evaluate_rag, generate_hf, generate_vllm, load_hf_model, load_vllm_model + +# Setup logging to reduce verbose output +logging.basicConfig(level=logging.WARNING) +logging.getLogger("leann.api").setLevel(logging.WARNING) +logging.getLogger("leann_backend_hnsw").setLevel(logging.WARNING) + + +class RecallEvaluator: + """Stage 2: Evaluate Recall@3 (searcher vs baseline)""" + + def __init__(self, index_path: str, baseline_dir: str): + self.index_path = index_path + self.baseline_dir = baseline_dir + self.searcher = LeannSearcher(index_path) + + # Load FAISS flat baseline + baseline_index_path = os.path.join(baseline_dir, "faiss_flat.index") + metadata_path = os.path.join(baseline_dir, "metadata.pkl") + + self.faiss_index = faiss.read_index(baseline_index_path) + with open(metadata_path, "rb") as f: + self.passage_ids = pickle.load(f) + print(f"πŸ“š Loaded FAISS flat baseline with {self.faiss_index.ntotal} vectors") + + def evaluate_recall_at_3( + self, queries: list[str], complexity: int = 64, recompute_embeddings: bool = True + ) -> float: + """Evaluate recall@3 for given queries at specified complexity""" + recompute_str = "with recompute" if recompute_embeddings else "no recompute" + print(f"πŸ” Evaluating recall@3 with complexity={complexity} ({recompute_str})...") + + total_recall = 0.0 + num_queries = len(queries) + + for i, query in enumerate(queries): + # Get ground truth: search with FAISS flat + from leann.api import compute_embeddings + + query_embedding = compute_embeddings( + [query], + self.searcher.embedding_model, + mode=self.searcher.embedding_mode, + use_server=False, + ).astype(np.float32) + + # Search FAISS flat for ground truth using LEANN's modified faiss API + n = query_embedding.shape[0] # Number of queries + k = 3 # Number of nearest neighbors + distances = np.zeros((n, k), dtype=np.float32) + labels = np.zeros((n, k), dtype=np.int64) + + self.faiss_index.search( + n, + faiss.swig_ptr(query_embedding), + k, + faiss.swig_ptr(distances), + faiss.swig_ptr(labels), + ) + + # Extract the results + baseline_ids = {self.passage_ids[idx] for idx in labels[0]} + + # Search with LEANN at specified complexity + test_results = self.searcher.search( + query, + top_k=3, + complexity=complexity, + recompute_embeddings=recompute_embeddings, + ) + test_ids = {result.id for result in test_results} + + # Calculate recall@3 = |intersection| / |ground_truth| + intersection = test_ids.intersection(baseline_ids) + recall = len(intersection) / 3.0 # Ground truth size is 3 + total_recall += recall + + if i < 3: # Show first few examples + print(f" Query {i + 1}: '{query[:50]}...' -> Recall@3: {recall:.3f}") + print(f" FAISS ground truth: {list(baseline_ids)}") + print(f" LEANN results (C={complexity}, {recompute_str}): {list(test_ids)}") + print(f" Intersection: {list(intersection)}") + + avg_recall = total_recall / num_queries + print(f"πŸ“Š Average Recall@3: {avg_recall:.3f} ({avg_recall * 100:.1f}%)") + return avg_recall + + def cleanup(self): + """Cleanup resources""" + if hasattr(self, "searcher"): + self.searcher.cleanup() + + +class FinanceBenchEvaluator: + def __init__(self, index_path: str, openai_api_key: Optional[str] = None): + self.index_path = index_path + self.openai_client = openai.OpenAI(api_key=openai_api_key) if openai_api_key else None + + self.searcher = LeannSearcher(index_path) + self.chat = LeannChat(index_path) if openai_api_key else None + + def load_dataset(self, dataset_path: str = "data/financebench_merged.jsonl"): + """Load FinanceBench dataset""" + data = [] + with open(dataset_path, encoding="utf-8") as f: + for line in f: + if line.strip(): + data.append(json.loads(line)) + + print(f"πŸ“Š Loaded {len(data)} FinanceBench examples") + return data + + def analyze_index_sizes(self) -> dict: + """Analyze index sizes with and without embeddings""" + + print("πŸ“ Analyzing index sizes...") + + # Get all index-related files + index_path = Path(self.index_path) + index_dir = index_path.parent + index_name = index_path.stem # Remove .leann extension + + sizes = {} + total_with_embeddings = 0 + + # Core index files + index_file = index_dir / f"{index_name}.index" + meta_file = index_dir / f"{index_path.name}.meta.json" # Keep .leann for meta file + passages_file = index_dir / f"{index_path.name}.passages.jsonl" # Keep .leann for passages + passages_idx_file = index_dir / f"{index_path.name}.passages.idx" # Keep .leann for idx + + for file_path, name in [ + (index_file, "index"), + (meta_file, "metadata"), + (passages_file, "passages_text"), + (passages_idx_file, "passages_index"), + ]: + if file_path.exists(): + size_mb = file_path.stat().st_size / (1024 * 1024) + sizes[name] = size_mb + total_with_embeddings += size_mb + + else: + sizes[name] = 0 + + sizes["total_with_embeddings"] = total_with_embeddings + sizes["index_only_mb"] = sizes["index"] # Just the .index file for fair comparison + + print(f" πŸ“ Total index size: {total_with_embeddings:.1f} MB") + print(f" πŸ“ Index file only: {sizes['index']:.1f} MB") + + return sizes + + def create_compact_index_for_comparison(self, compact_index_path: str) -> dict: + """Create a compact index for comparison purposes""" + print("πŸ—οΈ Building compact index from existing passages...") + + # Load existing passages from current index + + from leann import LeannBuilder + + current_index_path = Path(self.index_path) + current_index_dir = current_index_path.parent + current_index_name = current_index_path.name + + # Read metadata to get passage source + meta_path = current_index_dir / f"{current_index_name}.meta.json" + with open(meta_path) as f: + import json + + meta = json.load(f) + + passage_source = meta["passage_sources"][0] + passage_file = passage_source["path"] + + # Convert relative path to absolute + if not Path(passage_file).is_absolute(): + passage_file = current_index_dir / Path(passage_file).name + + print(f"πŸ“„ Loading passages from {passage_file}...") + + # Build compact index with same passages + builder = LeannBuilder( + backend_name="hnsw", + embedding_model=meta["embedding_model"], + embedding_mode=meta.get("embedding_mode", "sentence-transformers"), + is_recompute=True, # Enable recompute (no stored embeddings) + is_compact=True, # Enable compact storage + **meta.get("backend_kwargs", {}), + ) + + # Load all passages + with open(passage_file, encoding="utf-8") as f: + for line in f: + if line.strip(): + data = json.loads(line) + builder.add_text(data["text"], metadata=data.get("metadata", {})) + + print(f"πŸ”¨ Building compact index at {compact_index_path}...") + builder.build_index(compact_index_path) + + # Analyze the compact index size + temp_evaluator = FinanceBenchEvaluator(compact_index_path) + compact_sizes = temp_evaluator.analyze_index_sizes() + compact_sizes["index_type"] = "compact" + + return compact_sizes + + def create_non_compact_index_for_comparison(self, non_compact_index_path: str) -> dict: + """Create a non-compact index for comparison purposes""" + print("πŸ—οΈ Building non-compact index from existing passages...") + + # Load existing passages from current index + + from leann import LeannBuilder + + current_index_path = Path(self.index_path) + current_index_dir = current_index_path.parent + current_index_name = current_index_path.name + + # Read metadata to get passage source + meta_path = current_index_dir / f"{current_index_name}.meta.json" + with open(meta_path) as f: + import json + + meta = json.load(f) + + passage_source = meta["passage_sources"][0] + passage_file = passage_source["path"] + + # Convert relative path to absolute + if not Path(passage_file).is_absolute(): + passage_file = current_index_dir / Path(passage_file).name + + print(f"πŸ“„ Loading passages from {passage_file}...") + + # Build non-compact index with same passages + builder = LeannBuilder( + backend_name="hnsw", + embedding_model=meta["embedding_model"], + embedding_mode=meta.get("embedding_mode", "sentence-transformers"), + is_recompute=False, # Disable recompute (store embeddings) + is_compact=False, # Disable compact storage + **{ + k: v + for k, v in meta.get("backend_kwargs", {}).items() + if k not in ["is_recompute", "is_compact"] + }, + ) + + # Load all passages + with open(passage_file, encoding="utf-8") as f: + for line in f: + if line.strip(): + data = json.loads(line) + builder.add_text(data["text"], metadata=data.get("metadata", {})) + + print(f"πŸ”¨ Building non-compact index at {non_compact_index_path}...") + builder.build_index(non_compact_index_path) + + # Analyze the non-compact index size + temp_evaluator = FinanceBenchEvaluator(non_compact_index_path) + non_compact_sizes = temp_evaluator.analyze_index_sizes() + non_compact_sizes["index_type"] = "non_compact" + + return non_compact_sizes + + def compare_index_performance( + self, non_compact_path: str, compact_path: str, test_data: list, complexity: int + ) -> dict: + """Compare performance between non-compact and compact indexes""" + print("⚑ Comparing search performance between indexes...") + + import time + + from leann import LeannSearcher + + # Test queries + test_queries = [item["question"] for item in test_data[:5]] + + results = { + "non_compact": {"search_times": []}, + "compact": {"search_times": []}, + "avg_search_times": {}, + "speed_ratio": 0.0, + } + + # Test non-compact index (no recompute) + print(" πŸ” Testing non-compact index (no recompute)...") + non_compact_searcher = LeannSearcher(non_compact_path) + + for query in test_queries: + start_time = time.time() + _ = non_compact_searcher.search( + query, top_k=3, complexity=complexity, recompute_embeddings=False + ) + search_time = time.time() - start_time + results["non_compact"]["search_times"].append(search_time) + + # Test compact index (with recompute) + print(" πŸ” Testing compact index (with recompute)...") + compact_searcher = LeannSearcher(compact_path) + + for query in test_queries: + start_time = time.time() + _ = compact_searcher.search( + query, top_k=3, complexity=complexity, recompute_embeddings=True + ) + search_time = time.time() - start_time + results["compact"]["search_times"].append(search_time) + + # Calculate averages + results["avg_search_times"]["non_compact"] = sum( + results["non_compact"]["search_times"] + ) / len(results["non_compact"]["search_times"]) + results["avg_search_times"]["compact"] = sum(results["compact"]["search_times"]) / len( + results["compact"]["search_times"] + ) + + # Performance ratio + if results["avg_search_times"]["compact"] > 0: + results["speed_ratio"] = ( + results["avg_search_times"]["non_compact"] / results["avg_search_times"]["compact"] + ) + else: + results["speed_ratio"] = float("inf") + + print( + f" Non-compact (no recompute): {results['avg_search_times']['non_compact']:.3f}s avg" + ) + print(f" Compact (with recompute): {results['avg_search_times']['compact']:.3f}s avg") + print(f" Speed ratio: {results['speed_ratio']:.2f}x") + + # Cleanup + non_compact_searcher.cleanup() + compact_searcher.cleanup() + + return results + + def evaluate_timing_breakdown( + self, data: list[dict], max_samples: Optional[int] = None + ) -> dict: + """Evaluate timing breakdown and accuracy by hacking LeannChat.ask() for separated timing""" + if not self.chat or not self.openai_client: + print("⚠️ Skipping timing evaluation (no OpenAI API key provided)") + return { + "total_questions": 0, + "avg_search_time": 0.0, + "avg_generation_time": 0.0, + "avg_total_time": 0.0, + "accuracy": 0.0, + } + + print("πŸ”πŸ€– Evaluating timing breakdown and accuracy (search + generation)...") + + if max_samples: + data = data[:max_samples] + print(f"πŸ“ Using first {max_samples} samples for timing evaluation") + + search_times = [] + generation_times = [] + total_times = [] + correct_answers = 0 + + for i, item in enumerate(data): + question = item["question"] + ground_truth = item["answer"] + + try: + # Hack: Monkey-patch the ask method to capture internal timing + original_ask = self.chat.ask + captured_search_time = None + captured_generation_time = None + + def patched_ask(*args, **kwargs): + nonlocal captured_search_time, captured_generation_time + + # Time the search part + search_start = time.time() + results = self.chat.searcher.search(args[0], top_k=3, complexity=64) + captured_search_time = time.time() - search_start + + # Time the generation part + context = "\n\n".join([r.text for r in results]) + prompt = ( + "Here is some retrieved context that might help answer your question:\n\n" + f"{context}\n\n" + f"Question: {args[0]}\n\n" + "Please provide the best answer you can based on this context and your knowledge." + ) + + generation_start = time.time() + answer = self.chat.llm.ask(prompt) + captured_generation_time = time.time() - generation_start + + return answer + + # Apply the patch + self.chat.ask = patched_ask + + # Time the total QA + total_start = time.time() + generated_answer = self.chat.ask(question) + total_time = time.time() - total_start + + # Restore original method + self.chat.ask = original_ask + + # Store the timings + search_times.append(captured_search_time) + generation_times.append(captured_generation_time) + total_times.append(total_time) + + # Check accuracy using LLM as judge + is_correct = self._check_answer_accuracy(generated_answer, ground_truth, question) + if is_correct: + correct_answers += 1 + + status = "βœ…" if is_correct else "❌" + print( + f"Question {i + 1}/{len(data)}: {status} Search={captured_search_time:.3f}s, Gen={captured_generation_time:.3f}s, Total={total_time:.3f}s" + ) + print(f" GT: {ground_truth}") + print(f" Gen: {generated_answer[:100]}...") + + except Exception as e: + print(f" ❌ Error: {e}") + search_times.append(0.0) + generation_times.append(0.0) + total_times.append(0.0) + + accuracy = correct_answers / len(data) if data else 0.0 + + metrics = { + "total_questions": len(data), + "avg_search_time": sum(search_times) / len(search_times) if search_times else 0.0, + "avg_generation_time": sum(generation_times) / len(generation_times) + if generation_times + else 0.0, + "avg_total_time": sum(total_times) / len(total_times) if total_times else 0.0, + "accuracy": accuracy, + "correct_answers": correct_answers, + "search_times": search_times, + "generation_times": generation_times, + "total_times": total_times, + } + + return metrics + + def _check_answer_accuracy( + self, generated_answer: str, ground_truth: str, question: str + ) -> bool: + """Check if generated answer matches ground truth using LLM as judge""" + judge_prompt = f"""You are an expert judge evaluating financial question answering. + +Question: {question} + +Ground Truth Answer: {ground_truth} + +Generated Answer: {generated_answer} + +Task: Determine if the generated answer is factually correct compared to the ground truth. Focus on: +1. Numerical accuracy (exact values, units, currency) +2. Key financial concepts and terminology +3. Overall factual correctness + +For financial data, small formatting differences are OK (e.g., "$1,577" vs "1577 million" vs "$1.577 billion"), but the core numerical value must match. + +Respond with exactly one word: "CORRECT" if the generated answer is factually accurate, or "INCORRECT" if it's wrong or significantly different.""" + + try: + judge_response = self.openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": judge_prompt}], + max_tokens=10, + temperature=0, + ) + if not judge_response.choices or judge_response.choices[0].message is None: + raise ValueError("LLM returned empty or filtered response") + judgment = judge_response.choices[0].message.content.strip().upper() + return judgment == "CORRECT" + except Exception as e: + print(f" ⚠️ Judge error: {e}, falling back to string matching") + # Fallback to simple string matching + gen_clean = generated_answer.strip().lower().replace("$", "").replace(",", "") + gt_clean = ground_truth.strip().lower().replace("$", "").replace(",", "") + return gt_clean in gen_clean + + def _print_results(self, timing_metrics: dict): + """Print evaluation results""" + print("\n🎯 EVALUATION RESULTS") + print("=" * 50) + + # Index comparison analysis + if "current_index" in timing_metrics and "non_compact_index" in timing_metrics: + print("\nπŸ“ Index Comparison Analysis:") + current = timing_metrics["current_index"] + non_compact = timing_metrics["non_compact_index"] + + print(f" Compact index (current): {current.get('total_with_embeddings', 0):.1f} MB") + print( + f" Non-compact index (with embeddings): {non_compact.get('total_with_embeddings', 0):.1f} MB" + ) + print( + f" Storage saving by compact: {timing_metrics.get('storage_saving_percent', 0):.1f}%" + ) + + print(" Component breakdown (non-compact):") + print(f" - Main index: {non_compact.get('index', 0):.1f} MB") + print(f" - Passages text: {non_compact.get('passages_text', 0):.1f} MB") + print(f" - Passages index: {non_compact.get('passages_index', 0):.1f} MB") + print(f" - Metadata: {non_compact.get('metadata', 0):.1f} MB") + + # Performance comparison + if "performance_comparison" in timing_metrics: + perf = timing_metrics["performance_comparison"] + print("\n⚑ Performance Comparison:") + print( + f" Non-compact (no recompute): {perf.get('avg_search_times', {}).get('non_compact', 0):.3f}s avg" + ) + print( + f" Compact (with recompute): {perf.get('avg_search_times', {}).get('compact', 0):.3f}s avg" + ) + print(f" Speed ratio: {perf.get('speed_ratio', 0):.2f}x") + + # Legacy single index analysis (fallback) + if "total_with_embeddings" in timing_metrics and "current_index" not in timing_metrics: + print("\nπŸ“ Index Size Analysis:") + print(f" Total index size: {timing_metrics.get('total_with_embeddings', 0):.1f} MB") + + print("\nπŸ“Š Accuracy:") + print(f" Accuracy: {timing_metrics.get('accuracy', 0) * 100:.1f}%") + print( + f" Correct Answers: {timing_metrics.get('correct_answers', 0)}/{timing_metrics.get('total_questions', 0)}" + ) + + print("\nπŸ“Š Timing Breakdown:") + print(f" Total Questions: {timing_metrics.get('total_questions', 0)}") + print(f" Avg Search Time: {timing_metrics.get('avg_search_time', 0):.3f}s") + print(f" Avg Generation Time: {timing_metrics.get('avg_generation_time', 0):.3f}s") + print(f" Avg Total Time: {timing_metrics.get('avg_total_time', 0):.3f}s") + + if timing_metrics.get("avg_total_time", 0) > 0: + search_pct = ( + timing_metrics.get("avg_search_time", 0) + / timing_metrics.get("avg_total_time", 1) + * 100 + ) + gen_pct = ( + timing_metrics.get("avg_generation_time", 0) + / timing_metrics.get("avg_total_time", 1) + * 100 + ) + print("\nπŸ“ˆ Time Distribution:") + print(f" Search: {search_pct:.1f}%") + print(f" Generation: {gen_pct:.1f}%") + + def cleanup(self): + """Cleanup resources""" + if self.searcher: + self.searcher.cleanup() + + +def main(): + parser = argparse.ArgumentParser(description="Modular FinanceBench Evaluation") + parser.add_argument("--index", required=True, help="Path to LEANN index") + parser.add_argument("--dataset", default="data/financebench_merged.jsonl", help="Dataset path") + parser.add_argument( + "--stage", + choices=["2", "3", "4", "all"], + default="all", + help="Which stage to run (2=recall, 3=complexity, 4=generation)", + ) + parser.add_argument("--complexity", type=int, default=None, help="Complexity for search") + parser.add_argument("--baseline-dir", default="baseline", help="Baseline output directory") + parser.add_argument("--openai-api-key", help="OpenAI API key for generation evaluation") + parser.add_argument("--output", help="Save results to JSON file") + parser.add_argument( + "--llm-backend", choices=["openai", "hf", "vllm"], default="openai", help="LLM backend" + ) + parser.add_argument("--model-name", default="Qwen3-8B", help="Model name for HF/vLLM") + + args = parser.parse_args() + + try: + # Check if baseline exists + baseline_index_path = os.path.join(args.baseline_dir, "faiss_flat.index") + if not os.path.exists(baseline_index_path): + print(f"❌ FAISS baseline not found at {baseline_index_path}") + print("πŸ’‘ Please run setup_financebench.py first to build the baseline") + exit(1) + + if args.stage == "2" or args.stage == "all": + # Stage 2: Recall@3 evaluation + print("πŸš€ Starting Stage 2: Recall@3 evaluation") + + evaluator = RecallEvaluator(args.index, args.baseline_dir) + + # Load FinanceBench queries for testing + print("πŸ“– Loading FinanceBench dataset...") + queries = [] + with open(args.dataset, encoding="utf-8") as f: + for line in f: + if line.strip(): + data = json.loads(line) + queries.append(data["question"]) + + # Test with more queries for robust measurement + test_queries = queries[:2000] + print(f"πŸ§ͺ Testing with {len(test_queries)} queries") + + # Test with complexity 64 + complexity = 64 + recall = evaluator.evaluate_recall_at_3(test_queries, complexity) + print(f"πŸ“ˆ Recall@3 at complexity {complexity}: {recall * 100:.1f}%") + + evaluator.cleanup() + print("βœ… Stage 2 completed!\n") + + # Shared non-compact index path for Stage 3 and 4 + non_compact_index_path = args.index.replace(".leann", "_noncompact.leann") + complexity = args.complexity + + if args.stage == "3" or args.stage == "all": + # Stage 3: Binary search for 90% recall complexity (using non-compact index for speed) + print("πŸš€ Starting Stage 3: Binary search for 90% recall complexity") + print( + "πŸ’‘ Creating non-compact index for fast binary search with recompute_embeddings=False" + ) + + # Create non-compact index for binary search (will be reused in Stage 4) + print("πŸ—οΈ Creating non-compact index for binary search...") + evaluator = FinanceBenchEvaluator(args.index) + evaluator.create_non_compact_index_for_comparison(non_compact_index_path) + + # Use non-compact index for binary search + binary_search_evaluator = RecallEvaluator(non_compact_index_path, args.baseline_dir) + + # Load queries for testing + print("πŸ“– Loading FinanceBench dataset...") + queries = [] + with open(args.dataset, encoding="utf-8") as f: + for line in f: + if line.strip(): + data = json.loads(line) + queries.append(data["question"]) + + # Use more queries for robust measurement + test_queries = queries[:200] + print(f"πŸ§ͺ Testing with {len(test_queries)} queries") + + # Binary search for 90% recall complexity (without recompute for speed) + target_recall = 0.9 + min_complexity, max_complexity = 1, 32 + + print(f"πŸ” Binary search for {target_recall * 100}% recall complexity...") + print(f"Search range: {min_complexity} to {max_complexity}") + + best_complexity = None + best_recall = 0.0 + + while min_complexity <= max_complexity: + mid_complexity = (min_complexity + max_complexity) // 2 + + print( + f"\nπŸ§ͺ Testing complexity {mid_complexity} (no recompute, non-compact index)..." + ) + # Use recompute_embeddings=False on non-compact index for fast binary search + recall = binary_search_evaluator.evaluate_recall_at_3( + test_queries, mid_complexity, recompute_embeddings=False + ) + + print( + f" Complexity {mid_complexity}: Recall@3 = {recall:.3f} ({recall * 100:.1f}%)" + ) + + if recall >= target_recall: + best_complexity = mid_complexity + best_recall = recall + max_complexity = mid_complexity - 1 + print(" βœ… Target reached! Searching for lower complexity...") + else: + min_complexity = mid_complexity + 1 + print(" ❌ Below target. Searching for higher complexity...") + + if best_complexity is not None: + print("\n🎯 Optimal complexity found!") + print(f" Complexity: {best_complexity}") + print(f" Recall@3: {best_recall:.3f} ({best_recall * 100:.1f}%)") + + # Test a few complexities around the optimal one for verification + print("\nπŸ”¬ Verification test around optimal complexity:") + verification_complexities = [ + max(1, best_complexity - 2), + max(1, best_complexity - 1), + best_complexity, + best_complexity + 1, + best_complexity + 2, + ] + + for complexity in verification_complexities: + if complexity <= 512: # reasonable upper bound + recall = binary_search_evaluator.evaluate_recall_at_3( + test_queries, complexity, recompute_embeddings=False + ) + status = "βœ…" if recall >= target_recall else "❌" + print(f" {status} Complexity {complexity:3d}: {recall * 100:5.1f}%") + + # Now test the optimal complexity with compact index and recompute for comparison + print( + f"\nπŸ”„ Testing optimal complexity {best_complexity} on compact index WITH recompute..." + ) + compact_evaluator = RecallEvaluator(args.index, args.baseline_dir) + recall_with_recompute = compact_evaluator.evaluate_recall_at_3( + test_queries[:10], best_complexity, recompute_embeddings=True + ) + print( + f" βœ… Complexity {best_complexity} (compact index with recompute): {recall_with_recompute * 100:.1f}%" + ) + complexity = best_complexity + print( + f" πŸ“Š Recall difference: {abs(best_recall - recall_with_recompute) * 100:.2f}%" + ) + compact_evaluator.cleanup() + else: + print(f"\n❌ Could not find complexity achieving {target_recall * 100}% recall") + print("All tested complexities were below target.") + + # Cleanup evaluators (keep non-compact index for Stage 4) + binary_search_evaluator.cleanup() + evaluator.cleanup() + + print("βœ… Stage 3 completed! Non-compact index saved for Stage 4.\n") + + if args.stage == "4" or args.stage == "all": + # Stage 4: Comprehensive evaluation with dual index comparison + print("πŸš€ Starting Stage 4: Comprehensive evaluation with dual index comparison") + + # Use FinanceBench evaluator for QA evaluation + evaluator = FinanceBenchEvaluator( + args.index, args.openai_api_key if args.llm_backend == "openai" else None + ) + + print("πŸ“– Loading FinanceBench dataset...") + data = evaluator.load_dataset(args.dataset) + + # Step 1: Analyze current (compact) index + print("\nπŸ“ Analyzing current index (compact, pruned)...") + compact_size_metrics = evaluator.analyze_index_sizes() + compact_size_metrics["index_type"] = "compact" + + # Step 2: Use existing non-compact index or create if needed + from pathlib import Path + + if Path(non_compact_index_path).exists(): + print( + f"\nπŸ“ Using existing non-compact index from Stage 3: {non_compact_index_path}" + ) + temp_evaluator = FinanceBenchEvaluator(non_compact_index_path) + non_compact_size_metrics = temp_evaluator.analyze_index_sizes() + non_compact_size_metrics["index_type"] = "non_compact" + else: + print("\nπŸ—οΈ Creating non-compact index (with embeddings) for comparison...") + non_compact_size_metrics = evaluator.create_non_compact_index_for_comparison( + non_compact_index_path + ) + + # Step 3: Compare index sizes + print("\nπŸ“Š Index size comparison:") + print( + f" Compact index (current): {compact_size_metrics['total_with_embeddings']:.1f} MB" + ) + print( + f" Non-compact index: {non_compact_size_metrics['total_with_embeddings']:.1f} MB" + ) + print("\nπŸ“Š Index-only size comparison (.index file only):") + print(f" Compact index: {compact_size_metrics['index_only_mb']:.1f} MB") + print(f" Non-compact index: {non_compact_size_metrics['index_only_mb']:.1f} MB") + # Use index-only size for fair comparison (same as Enron emails) + storage_saving = ( + (non_compact_size_metrics["index_only_mb"] - compact_size_metrics["index_only_mb"]) + / non_compact_size_metrics["index_only_mb"] + * 100 + ) + print(f" Storage saving by compact: {storage_saving:.1f}%") + + # Step 4: Performance comparison between the two indexes + if complexity is None: + raise ValueError("Complexity is required for performance comparison") + + print("\n⚑ Performance comparison between indexes...") + performance_metrics = evaluator.compare_index_performance( + non_compact_index_path, args.index, data[:10], complexity=complexity + ) + + # Step 5: Generation evaluation + test_samples = 20 + print(f"\nπŸ§ͺ Testing with first {test_samples} samples for generation analysis") + + if args.llm_backend == "openai" and args.openai_api_key: + print("πŸ”πŸ€– Running OpenAI-based generation evaluation...") + evaluation_start = time.time() + timing_metrics = evaluator.evaluate_timing_breakdown(data[:test_samples]) + evaluation_time = time.time() - evaluation_start + else: + print( + f"πŸ”πŸ€– Running {args.llm_backend} generation evaluation with {args.model_name}..." + ) + try: + # Load LLM + if args.llm_backend == "hf": + tokenizer, model = load_hf_model(args.model_name) + + def llm_func(prompt): + return generate_hf(tokenizer, model, prompt) + else: # vllm + llm, sampling_params = load_vllm_model(args.model_name) + + def llm_func(prompt): + return generate_vllm(llm, sampling_params, prompt) + + # Simple generation evaluation + queries = [item["question"] for item in data[:test_samples]] + gen_results = evaluate_rag( + evaluator.searcher, + llm_func, + queries, + domain="finance", + complexity=complexity, + ) + + timing_metrics = { + "total_questions": len(queries), + "avg_search_time": gen_results["avg_search_time"], + "avg_generation_time": gen_results["avg_generation_time"], + "results": gen_results["results"], + } + evaluation_time = time.time() + + except Exception as e: + print(f"❌ Generation evaluation failed: {e}") + timing_metrics = { + "total_questions": 0, + "avg_search_time": 0, + "avg_generation_time": 0, + } + evaluation_time = 0 + + # Combine all metrics + combined_metrics = { + **timing_metrics, + "total_evaluation_time": evaluation_time, + "current_index": compact_size_metrics, + "non_compact_index": non_compact_size_metrics, + "performance_comparison": performance_metrics, + "storage_saving_percent": storage_saving, + } + + # Print results + print("\nπŸ“Š Generation Results:") + print(f" Total Questions: {timing_metrics.get('total_questions', 0)}") + print(f" Avg Search Time: {timing_metrics.get('avg_search_time', 0):.3f}s") + print(f" Avg Generation Time: {timing_metrics.get('avg_generation_time', 0):.3f}s") + + # Save results if requested + if args.output: + print(f"\nπŸ’Ύ Saving results to {args.output}...") + with open(args.output, "w") as f: + json.dump(combined_metrics, f, indent=2, default=str) + print(f"βœ… Results saved to {args.output}") + + evaluator.cleanup() + print("βœ… Stage 4 completed!\n") + + if args.stage == "all": + print("πŸŽ‰ All evaluation stages completed successfully!") + print("\nπŸ“‹ Summary:") + print(" Stage 2: βœ… Recall@3 evaluation completed") + print(" Stage 3: βœ… Optimal complexity found") + print(" Stage 4: βœ… Generation accuracy & timing evaluation completed") + print("\nπŸ”§ Recommended next steps:") + print(" - Use optimal complexity for best speed/accuracy balance") + print(" - Review accuracy and timing breakdown for performance optimization") + print(" - Run full evaluation on complete dataset if needed") + + # Clean up non-compact index after all stages complete + print("\n🧹 Cleaning up temporary non-compact index...") + from pathlib import Path + + if Path(non_compact_index_path).exists(): + temp_index_dir = Path(non_compact_index_path).parent + temp_index_name = Path(non_compact_index_path).name + for temp_file in temp_index_dir.glob(f"{temp_index_name}*"): + temp_file.unlink() + print(f"βœ… Cleaned up {non_compact_index_path}") + else: + print("πŸ“ No temporary index to clean up") + except KeyboardInterrupt: + print("\n⚠️ Evaluation interrupted by user") + exit(1) + except Exception as e: + print(f"\n❌ Stage {args.stage} failed: {e}") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/financebench/setup_financebench.py b/benchmarks/financebench/setup_financebench.py new file mode 100755 index 0000000..36bd0da --- /dev/null +++ b/benchmarks/financebench/setup_financebench.py @@ -0,0 +1,462 @@ +#!/usr/bin/env python3 +""" +FinanceBench Complete Setup Script +Downloads all PDFs and builds full LEANN datastore +""" + +import argparse +import os +import re +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path +from threading import Lock + +import pymupdf +import requests +from leann import LeannBuilder, LeannSearcher +from tqdm import tqdm + + +class FinanceBenchSetup: + def __init__(self, data_dir: str = "data"): + self.base_dir = Path(__file__).parent # benchmarks/financebench/ + self.data_dir = self.base_dir / data_dir + self.pdf_dir = self.data_dir / "pdfs" + self.dataset_file = self.data_dir / "financebench_merged.jsonl" + self.index_dir = self.data_dir / "index" + self.download_lock = Lock() + + def download_dataset(self): + """Download the main FinanceBench dataset""" + print("πŸ“Š Downloading FinanceBench dataset...") + self.data_dir.mkdir(parents=True, exist_ok=True) + + if self.dataset_file.exists(): + print(f"βœ… Dataset already exists: {self.dataset_file}") + return + + url = "https://huggingface.co/datasets/PatronusAI/financebench/raw/main/financebench_merged.jsonl" + response = requests.get(url, stream=True) + response.raise_for_status() + + with open(self.dataset_file, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + print(f"βœ… Dataset downloaded: {self.dataset_file}") + + def get_pdf_list(self): + """Get list of all PDF files from GitHub""" + print("πŸ“‹ Fetching PDF list from GitHub...") + + response = requests.get( + "https://api.github.com/repos/patronus-ai/financebench/contents/pdfs" + ) + response.raise_for_status() + pdf_files = response.json() + + print(f"Found {len(pdf_files)} PDF files") + return pdf_files + + def download_single_pdf(self, pdf_info, position): + """Download a single PDF file""" + pdf_name = pdf_info["name"] + pdf_path = self.pdf_dir / pdf_name + + # Skip if already downloaded + if pdf_path.exists() and pdf_path.stat().st_size > 0: + return f"βœ… {pdf_name} (cached)" + + try: + # Download PDF + response = requests.get(pdf_info["download_url"], timeout=60) + response.raise_for_status() + + # Write to file + with self.download_lock: + with open(pdf_path, "wb") as f: + f.write(response.content) + + return f"βœ… {pdf_name} ({len(response.content) // 1024}KB)" + + except Exception as e: + return f"❌ {pdf_name}: {e!s}" + + def download_all_pdfs(self, max_workers: int = 5): + """Download all PDF files with parallel processing""" + self.pdf_dir.mkdir(parents=True, exist_ok=True) + + pdf_files = self.get_pdf_list() + + print(f"πŸ“₯ Downloading {len(pdf_files)} PDFs with {max_workers} workers...") + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # Submit all download tasks + future_to_pdf = { + executor.submit(self.download_single_pdf, pdf_info, i): pdf_info["name"] + for i, pdf_info in enumerate(pdf_files) + } + + # Process completed downloads with progress bar + with tqdm(total=len(pdf_files), desc="Downloading PDFs") as pbar: + for future in as_completed(future_to_pdf): + result = future.result() + pbar.set_postfix_str(result.split()[-1] if "βœ…" in result else "Error") + pbar.update(1) + + # Verify downloads + downloaded_pdfs = list(self.pdf_dir.glob("*.pdf")) + print(f"βœ… Successfully downloaded {len(downloaded_pdfs)}/{len(pdf_files)} PDFs") + + # Show any failures + missing_pdfs = [] + for pdf_info in pdf_files: + pdf_path = self.pdf_dir / pdf_info["name"] + if not pdf_path.exists() or pdf_path.stat().st_size == 0: + missing_pdfs.append(pdf_info["name"]) + + if missing_pdfs: + print(f"⚠️ Failed to download {len(missing_pdfs)} PDFs:") + for pdf in missing_pdfs[:5]: # Show first 5 + print(f" - {pdf}") + if len(missing_pdfs) > 5: + print(f" ... and {len(missing_pdfs) - 5} more") + + def build_leann_index( + self, + backend: str = "hnsw", + embedding_model: str = "sentence-transformers/all-mpnet-base-v2", + ): + """Build LEANN index from all PDFs""" + print(f"πŸ—οΈ Building LEANN index with {backend} backend...") + + # Check if we have PDFs + pdf_files = list(self.pdf_dir.glob("*.pdf")) + if not pdf_files: + raise RuntimeError("No PDF files found! Run download first.") + + print(f"Found {len(pdf_files)} PDF files to process") + + start_time = time.time() + + # Initialize builder with standard compact configuration + builder = LeannBuilder( + backend_name=backend, + embedding_model=embedding_model, + embedding_mode="sentence-transformers", + graph_degree=32, + complexity=64, + is_recompute=True, # Enable recompute (no stored embeddings) + is_compact=True, # Enable compact storage (pruned) + num_threads=4, + ) + + # Process PDFs and extract text + total_chunks = 0 + failed_pdfs = [] + + for pdf_path in tqdm(pdf_files, desc="Processing PDFs"): + try: + chunks = self.extract_pdf_text(pdf_path) + for chunk in chunks: + builder.add_text(chunk["text"], metadata=chunk["metadata"]) + total_chunks += 1 + + except Exception as e: + print(f"❌ Failed to process {pdf_path.name}: {e}") + failed_pdfs.append(pdf_path.name) + continue + + # Build index in index directory + self.index_dir.mkdir(parents=True, exist_ok=True) + index_path = self.index_dir / f"financebench_full_{backend}.leann" + print(f"πŸ”¨ Building index: {index_path}") + builder.build_index(str(index_path)) + + build_time = time.time() - start_time + + print("βœ… Index built successfully!") + print(f" πŸ“ Index path: {index_path}") + print(f" πŸ“Š Total chunks: {total_chunks:,}") + print(f" πŸ“„ Processed PDFs: {len(pdf_files) - len(failed_pdfs)}/{len(pdf_files)}") + print(f" ⏱️ Build time: {build_time:.1f}s") + + if failed_pdfs: + print(f" ⚠️ Failed PDFs: {failed_pdfs}") + + return str(index_path) + + def build_faiss_flat_baseline(self, index_path: str, output_dir: str = "baseline"): + """Build FAISS flat baseline using the same embeddings as LEANN index""" + print("πŸ”¨ Building FAISS Flat baseline...") + + import os + import pickle + + import numpy as np + from leann.api import compute_embeddings + from leann_backend_hnsw import faiss + + os.makedirs(output_dir, exist_ok=True) + baseline_path = os.path.join(output_dir, "faiss_flat.index") + metadata_path = os.path.join(output_dir, "metadata.pkl") + + if os.path.exists(baseline_path) and os.path.exists(metadata_path): + print(f"βœ… Baseline already exists at {baseline_path}") + return baseline_path + + # Read metadata from the built index + meta_path = f"{index_path}.meta.json" + with open(meta_path) as f: + import json + + meta = json.loads(f.read()) + + embedding_model = meta["embedding_model"] + passage_source = meta["passage_sources"][0] + passage_file = passage_source["path"] + + # Convert relative path to absolute + if not os.path.isabs(passage_file): + index_dir = os.path.dirname(index_path) + passage_file = os.path.join(index_dir, os.path.basename(passage_file)) + + print(f"πŸ“Š Loading passages from {passage_file}...") + print(f"πŸ€– Using embedding model: {embedding_model}") + + # Load all passages for baseline + passages = [] + passage_ids = [] + with open(passage_file, encoding="utf-8") as f: + for line in f: + if line.strip(): + data = json.loads(line) + passages.append(data["text"]) + passage_ids.append(data["id"]) + + print(f"πŸ“„ Loaded {len(passages)} passages") + + # Compute embeddings using the same method as LEANN + print("🧠Computing embeddings...") + embeddings = compute_embeddings( + passages, + embedding_model, + mode="sentence-transformers", + use_server=False, + ) + + print(f"πŸ“ Embedding shape: {embeddings.shape}") + + # Build FAISS flat index + print("πŸ—οΈ Building FAISS IndexFlatIP...") + dimension = embeddings.shape[1] + index = faiss.IndexFlatIP(dimension) + + # Add embeddings to flat index + embeddings_f32 = embeddings.astype(np.float32) + index.add(embeddings_f32.shape[0], faiss.swig_ptr(embeddings_f32)) + + # Save index and metadata + faiss.write_index(index, baseline_path) + with open(metadata_path, "wb") as f: + pickle.dump(passage_ids, f) + + print(f"βœ… FAISS baseline saved to {baseline_path}") + print(f"βœ… Metadata saved to {metadata_path}") + print(f"πŸ“Š Total vectors: {index.ntotal}") + + return baseline_path + + def extract_pdf_text(self, pdf_path: Path) -> list[dict]: + """Extract and chunk text from a PDF file""" + chunks = [] + doc = pymupdf.open(pdf_path) + + for page_num in range(len(doc)): + page = doc[page_num] + text = page.get_text() # type: ignore + + if not text.strip(): + continue + + # Create metadata + metadata = { + "source_file": pdf_path.name, + "page_number": page_num + 1, + "document_type": "10K" if "10K" in pdf_path.name else "10Q", + "company": pdf_path.name.split("_")[0], + "doc_period": self.extract_year_from_filename(pdf_path.name), + } + + # Use recursive character splitting like LangChain + if len(text.split()) > 500: + # Split by double newlines (paragraphs) + paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()] + + current_chunk = "" + for para in paragraphs: + # If adding this paragraph would make chunk too long, save current chunk + if current_chunk and len((current_chunk + " " + para).split()) > 300: + if current_chunk.strip(): + chunks.append( + { + "text": current_chunk.strip(), + "metadata": { + **metadata, + "chunk_id": f"page_{page_num + 1}_chunk_{len(chunks)}", + }, + } + ) + current_chunk = para + else: + current_chunk = (current_chunk + " " + para).strip() + + # Add the last chunk + if current_chunk.strip(): + chunks.append( + { + "text": current_chunk.strip(), + "metadata": { + **metadata, + "chunk_id": f"page_{page_num + 1}_chunk_{len(chunks)}", + }, + } + ) + else: + # Page is short enough, use as single chunk + chunks.append( + { + "text": text.strip(), + "metadata": {**metadata, "chunk_id": f"page_{page_num + 1}"}, + } + ) + + doc.close() + return chunks + + def extract_year_from_filename(self, filename: str) -> str: + """Extract year from PDF filename""" + # Try to find 4-digit year in filename + + match = re.search(r"(\d{4})", filename) + return match.group(1) if match else "unknown" + + def verify_setup(self, index_path: str): + """Verify the setup by testing a simple query""" + print("πŸ§ͺ Verifying setup with test query...") + + try: + searcher = LeannSearcher(index_path) + + # Test query + test_query = "What is the capital expenditure for 3M in 2018?" + results = searcher.search(test_query, top_k=3) + + print(f"βœ… Test query successful! Found {len(results)} results:") + for i, result in enumerate(results, 1): + company = result.metadata.get("company", "Unknown") + year = result.metadata.get("doc_period", "Unknown") + page = result.metadata.get("page_number", "Unknown") + print(f" {i}. {company} {year} (page {page}) - Score: {result.score:.3f}") + print(f" {result.text[:100]}...") + + searcher.cleanup() + print("βœ… Setup verification completed successfully!") + + except Exception as e: + print(f"❌ Setup verification failed: {e}") + raise + + +def main(): + parser = argparse.ArgumentParser(description="Setup FinanceBench with full PDF datastore") + parser.add_argument("--data-dir", default="data", help="Data directory") + parser.add_argument( + "--backend", choices=["hnsw", "diskann"], default="hnsw", help="LEANN backend" + ) + parser.add_argument( + "--embedding-model", + default="sentence-transformers/all-mpnet-base-v2", + help="Embedding model", + ) + parser.add_argument("--max-workers", type=int, default=5, help="Parallel download workers") + parser.add_argument("--skip-download", action="store_true", help="Skip PDF download") + parser.add_argument("--skip-build", action="store_true", help="Skip index building") + parser.add_argument( + "--build-baseline-only", + action="store_true", + help="Only build FAISS baseline from existing index", + ) + + args = parser.parse_args() + + print("🏦 FinanceBench Complete Setup") + print("=" * 50) + + setup = FinanceBenchSetup(args.data_dir) + + try: + if args.build_baseline_only: + # Only build baseline from existing index + index_path = setup.index_dir / f"financebench_full_{args.backend}" + index_file = f"{index_path}.index" + meta_file = f"{index_path}.leann.meta.json" + + if not os.path.exists(index_file) or not os.path.exists(meta_file): + print("❌ Index files not found:") + print(f" Index: {index_file}") + print(f" Meta: {meta_file}") + print("πŸ’‘ Run without --build-baseline-only to build the index first") + exit(1) + + print(f"πŸ”¨ Building baseline from existing index: {index_path}") + baseline_path = setup.build_faiss_flat_baseline(str(index_path)) + print(f"βœ… Baseline built at {baseline_path}") + return + + # Step 1: Download dataset + setup.download_dataset() + + # Step 2: Download PDFs + if not args.skip_download: + setup.download_all_pdfs(max_workers=args.max_workers) + else: + print("⏭️ Skipping PDF download") + + # Step 3: Build LEANN index + if not args.skip_build: + index_path = setup.build_leann_index( + backend=args.backend, embedding_model=args.embedding_model + ) + + # Step 4: Build FAISS flat baseline + print("\nπŸ”¨ Building FAISS flat baseline...") + baseline_path = setup.build_faiss_flat_baseline(index_path) + print(f"βœ… Baseline built at {baseline_path}") + + # Step 5: Verify setup + setup.verify_setup(index_path) + else: + print("⏭️ Skipping index building") + + print("\nπŸŽ‰ FinanceBench setup completed!") + print(f"πŸ“ Data directory: {setup.data_dir.absolute()}") + print("\nNext steps:") + print( + "1. Run evaluation: python evaluate_financebench.py --index data/index/financebench_full_hnsw.leann" + ) + print( + "2. Or test manually: python -c \"from leann import LeannSearcher; s = LeannSearcher('data/index/financebench_full_hnsw.leann'); print(s.search('3M capital expenditure 2018'))\"" + ) + + except KeyboardInterrupt: + print("\n⚠️ Setup interrupted by user") + exit(1) + except Exception as e: + print(f"\n❌ Setup failed: {e}") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/financebench/verify_recall.py b/benchmarks/financebench/verify_recall.py new file mode 100644 index 0000000..c4f77cb --- /dev/null +++ b/benchmarks/financebench/verify_recall.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "faiss-cpu", +# "numpy", +# "sentence-transformers", +# "torch", +# "tqdm", +# ] +# /// + +""" +Independent recall verification script using standard FAISS. +Creates two indexes (HNSW and Flat) and compares recall@3 at different complexities. +""" + +import json +import time +from pathlib import Path + +import faiss +import numpy as np +from sentence_transformers import SentenceTransformer +from tqdm import tqdm + + +def compute_embeddings_direct(chunks: list[str], model_name: str) -> np.ndarray: + """ + Direct embedding computation using sentence-transformers. + Copied logic to avoid dependency issues. + """ + print(f"Loading model: {model_name}") + model = SentenceTransformer(model_name) + + print(f"Computing embeddings for {len(chunks)} chunks...") + embeddings = model.encode( + chunks, + show_progress_bar=True, + batch_size=32, + convert_to_numpy=True, + normalize_embeddings=False, + ) + + return embeddings.astype(np.float32) + + +def load_financebench_queries(dataset_path: str, max_queries: int = 200) -> list[str]: + """Load FinanceBench queries from dataset""" + queries = [] + with open(dataset_path, encoding="utf-8") as f: + for line in f: + if line.strip(): + data = json.loads(line) + queries.append(data["question"]) + if len(queries) >= max_queries: + break + return queries + + +def load_passages_from_leann_index(index_path: str) -> tuple[list[str], list[str]]: + """Load passages from LEANN index structure""" + meta_path = f"{index_path}.meta.json" + with open(meta_path) as f: + meta = json.load(f) + + passage_source = meta["passage_sources"][0] + passage_file = passage_source["path"] + + # Convert relative path to absolute + if not Path(passage_file).is_absolute(): + index_dir = Path(index_path).parent + passage_file = index_dir / Path(passage_file).name + + print(f"Loading passages from {passage_file}") + + passages = [] + passage_ids = [] + with open(passage_file, encoding="utf-8") as f: + for line in tqdm(f, desc="Loading passages"): + if line.strip(): + data = json.loads(line) + passages.append(data["text"]) + passage_ids.append(data["id"]) + + print(f"Loaded {len(passages)} passages") + return passages, passage_ids + + +def build_faiss_indexes(embeddings: np.ndarray) -> tuple[faiss.Index, faiss.Index]: + """Build FAISS indexes: Flat (ground truth) and HNSW""" + dimension = embeddings.shape[1] + + # Build Flat index (ground truth) + print("Building FAISS IndexFlatIP (ground truth)...") + flat_index = faiss.IndexFlatIP(dimension) + flat_index.add(embeddings) + + # Build HNSW index + print("Building FAISS IndexHNSWFlat...") + M = 32 # Same as LEANN default + hnsw_index = faiss.IndexHNSWFlat(dimension, M, faiss.METRIC_INNER_PRODUCT) + hnsw_index.hnsw.efConstruction = 200 # Same as LEANN default + hnsw_index.add(embeddings) + + print(f"Built indexes with {flat_index.ntotal} vectors, dimension {dimension}") + return flat_index, hnsw_index + + +def evaluate_recall_at_k( + query_embeddings: np.ndarray, + flat_index: faiss.Index, + hnsw_index: faiss.Index, + passage_ids: list[str], + k: int = 3, + ef_search: int = 64, +) -> float: + """Evaluate recall@k comparing HNSW vs Flat""" + + # Set search parameters for HNSW + hnsw_index.hnsw.efSearch = ef_search + + total_recall = 0.0 + num_queries = query_embeddings.shape[0] + + for i in range(num_queries): + query = query_embeddings[i : i + 1] # Keep 2D shape + + # Get ground truth from Flat index (standard FAISS API) + flat_distances, flat_indices = flat_index.search(query, k) + ground_truth_ids = {passage_ids[idx] for idx in flat_indices[0]} + + # Get results from HNSW index (standard FAISS API) + hnsw_distances, hnsw_indices = hnsw_index.search(query, k) + hnsw_ids = {passage_ids[idx] for idx in hnsw_indices[0]} + + # Calculate recall + intersection = ground_truth_ids.intersection(hnsw_ids) + recall = len(intersection) / k + total_recall += recall + + if i < 3: # Show first few examples + print(f" Query {i + 1}: Recall@{k} = {recall:.3f}") + print(f" Flat: {list(ground_truth_ids)}") + print(f" HNSW: {list(hnsw_ids)}") + print(f" Intersection: {list(intersection)}") + + avg_recall = total_recall / num_queries + return avg_recall + + +def main(): + # Configuration + dataset_path = "data/financebench_merged.jsonl" + index_path = "data/index/financebench_full_hnsw.leann" + embedding_model = "sentence-transformers/all-mpnet-base-v2" + + print("πŸ” FAISS Recall Verification") + print("=" * 50) + + # Check if files exist + if not Path(dataset_path).exists(): + print(f"❌ Dataset not found: {dataset_path}") + return + if not Path(f"{index_path}.meta.json").exists(): + print(f"❌ Index metadata not found: {index_path}.meta.json") + return + + # Load data + print("πŸ“– Loading FinanceBench queries...") + queries = load_financebench_queries(dataset_path, max_queries=50) + print(f"Loaded {len(queries)} queries") + + print("πŸ“„ Loading passages from LEANN index...") + passages, passage_ids = load_passages_from_leann_index(index_path) + + # Compute embeddings + print("🧠Computing passage embeddings...") + passage_embeddings = compute_embeddings_direct(passages, embedding_model) + + print("🧠Computing query embeddings...") + query_embeddings = compute_embeddings_direct(queries, embedding_model) + + # Build FAISS indexes + print("πŸ—οΈ Building FAISS indexes...") + flat_index, hnsw_index = build_faiss_indexes(passage_embeddings) + + # Test different efSearch values (equivalent to LEANN complexity) + print("\nπŸ“Š Evaluating Recall@3 at different efSearch values...") + ef_search_values = [16, 32, 64, 128, 256] + + for ef_search in ef_search_values: + print(f"\nπŸ§ͺ Testing efSearch = {ef_search}") + start_time = time.time() + + recall = evaluate_recall_at_k( + query_embeddings, flat_index, hnsw_index, passage_ids, k=3, ef_search=ef_search + ) + + elapsed = time.time() - start_time + print( + f"πŸ“ˆ efSearch {ef_search}: Recall@3 = {recall:.3f} ({recall * 100:.1f}%) in {elapsed:.2f}s" + ) + + print("\nβœ… Verification completed!") + print("\nπŸ“‹ Summary:") + print(" - Built independent FAISS Flat and HNSW indexes") + print(" - Compared recall@3 at different efSearch values") + print(" - Used same embedding model as LEANN") + print(" - This validates LEANN's recall measurements") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/flashlib_ivf_vs_faiss_ivf.py b/benchmarks/flashlib_ivf_vs_faiss_ivf.py new file mode 100644 index 0000000..1a515ad --- /dev/null +++ b/benchmarks/flashlib_ivf_vs_faiss_ivf.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +""" +FlashLib IVF (GPU) vs FAISS IVF (CPU) head-to-head for LEANN. + +This is the apples-to-apples *approximate* comparison: both backends are IVF-Flat +(inverted file) indexes that coarse-quantize the corpus into ``nlist`` cells and, at +search time, scan only the ``nprobe`` nearest cells. At a fixed ``(nlist, nprobe)`` +the two probe (almost) the same candidate set, so recall is comparable - the only +difference is GPU vs CPU kernels. (Contrast with ``flashlib_vs_hnsw_speed_comparison.py``, +which compares *exact* GPU k-NN against exact CPU flat search.) + +Backends compared, per corpus size, at a shared ``nlist`` and across an ``nprobe`` sweep: + +- ``flashlib_ivf (GPU)`` -> the LEANN ``flashlib_ivf`` backend + (``packages/leann-backend-flashlib-ivf``): FlashLib ``flash_ivf_flat`` on CUDA tensors. +- ``ivf (CPU)`` -> the LEANN ``ivf`` backend + (``packages/leann-backend-ivf``): FAISS ``IndexIVFFlat`` on CPU. + +Both are driven through the LEANN backend registry (the real builders/searchers), so +this measures what each backend actually does. Distance metric is cosine (vectors are +L2-normalized; FlashLib IVF ranks by squared-L2, FAISS by inner product - equivalent +on normalized vectors). + +Metrics per (size, nprobe): single-query latency (median ms), batched throughput +(queries/s), recall@k vs exact ground truth (for BOTH backends), and the GPU/CPU +speedup. Build time and index size are reported once per (size, nlist). + +Data is a mixture-of-Gaussians (clustered + L2-normalized) to mimic the local +structure of real embeddings, so IVF coarse quantization behaves realistically. + +Requirements: a CUDA GPU, ``flashlib``, ``torch``+CUDA, ``faiss-cpu``, ``leann-core``, +``leann-backend-ivf`` and ``leann-backend-flashlib-ivf``. + +Examples: + + # laptop-like CPU budget (8 threads) for the FAISS baseline + python benchmarks/flashlib_ivf_vs_faiss_ivf.py --sizes 100000 1000000 --cpu-threads 8 + + # single 1M run, custom nlist and nprobe sweep + python benchmarks/flashlib_ivf_vs_faiss_ivf.py --sizes 1000000 \ + --nlist 4096 --nprobe-sweep 1 8 32 128 + +Note: importing ``leann`` pulls in ``leann_backend_hnsw`` (LEANN's API imports it at +module load). From a source checkout whose compiled HNSW backend is not installed +(e.g. glibc < 2.35), put the pure-Python package on the path first: + + PYTHONPATH=packages/leann-backend-hnsw python benchmarks/flashlib_ivf_vs_faiss_ivf.py +""" + +# ruff: noqa: E402 (BLAS env vars must be set before importing numpy / faiss) +import os +import sys + + +def _argv_value(flag: str, default: str) -> str: + """Read ``--flag value`` from argv before argparse, so we can pin BLAS thread + counts BEFORE numpy/faiss import (their thread pools are fixed at import time).""" + if flag in sys.argv: + i = sys.argv.index(flag) + if i + 1 < len(sys.argv): + return sys.argv[i + 1] + return default + + +# FAISS CPU search latency is governed by the BLAS thread pool, which is read at +# import time - so pin it here, before importing numpy/faiss, to the requested CPU +# budget. ``--cpu-threads 0`` means "all cores" (capped at 32: 192-thread OpenBLAS +# both crashes with "too many memory regions" and yields no benefit here). +_cpu_threads = int(_argv_value("--cpu-threads", "0")) +_blas = str(_cpu_threads) if _cpu_threads > 0 else str(min(os.cpu_count() or 1, 32)) +for _v in ("OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS", "NUMEXPR_NUM_THREADS", "OMP_NUM_THREADS"): + os.environ[_v] = _blas + +import argparse +import gc +import json +import math +import tempfile +import time +from pathlib import Path +from typing import Any + +import numpy as np + + +def _fail(msg: str) -> None: + print(f"\n[ERROR] {msg}") + sys.exit(1) + + +def _normalize(x: np.ndarray) -> np.ndarray: + norms = np.linalg.norm(x, axis=1, keepdims=True) + norms[norms == 0] = 1.0 + return np.ascontiguousarray(x / norms) + + +def make_clustered_data(n_db: int, n_query: int, dim: int, seed: int, cluster_std: float): + """Mixture-of-Gaussians, L2-normalized: a stand-in for real embeddings that have + local cluster structure (so IVF coarse quantization is representative).""" + rng = np.random.default_rng(seed) + n_clusters = max(16, min(n_db // 100, 8192)) + centers = rng.standard_normal((n_clusters, dim), dtype=np.float32) + centers /= np.linalg.norm(centers, axis=1, keepdims=True) + + assign = rng.integers(0, n_clusters, size=n_db) + db = centers[assign] + cluster_std * rng.standard_normal((n_db, dim)).astype(np.float32) + + q_assign = rng.integers(0, n_clusters, size=n_query) + queries = centers[q_assign] + cluster_std * rng.standard_normal((n_query, dim)).astype( + np.float32 + ) + return _normalize(db.astype(np.float32)), _normalize(queries.astype(np.float32)) + + +def exact_ground_truth(db: np.ndarray, queries: np.ndarray, top_k: int): + """Exact top-k by cosine (== inner product on normalized vectors), on GPU, + chunked over queries to bound memory.""" + import torch + + db_t = torch.from_numpy(db).cuda() + q_t = torch.from_numpy(queries).cuda() + out = np.empty((queries.shape[0], top_k), dtype=np.int64) + step = 256 + for i in range(0, q_t.shape[0], step): + scores = q_t[i : i + step] @ db_t.T + out[i : i + step] = scores.topk(top_k, dim=1, largest=True).indices.cpu().numpy() + del db_t, q_t + torch.cuda.empty_cache() + return out + + +def recall_at_k(found: np.ndarray, truth: np.ndarray) -> float: + k = truth.shape[1] + return float(np.mean([len(set(found[i]) & set(truth[i])) / k for i in range(truth.shape[0])])) + + +def best_time(fn, n_repeat: int) -> float: + best = float("inf") + for _ in range(n_repeat): + t = time.perf_counter() + fn() + best = min(best, time.perf_counter() - t) + return best + + +def measure(search_fn, queries: np.ndarray, n_single: int, n_repeat: int): + """Single-query latency (median ms over n_single individual queries) and batched + throughput (q/s, best of n_repeat for all queries at once).""" + for _ in range(3): # warmup (FlashLib JIT-compiles per batch shape) + search_fn(queries[:1]) + search_fn(queries) + + per_query = [] + for i in range(min(n_single, queries.shape[0])): + t = time.perf_counter() + search_fn(queries[i : i + 1]) + per_query.append((time.perf_counter() - t) * 1000.0) + single_ms = float(np.median(per_query)) + + batch_time = best_time(lambda: search_fn(queries), n_repeat) + return single_ms, queries.shape[0] / batch_time + + +def auto_nlist(n_db: int) -> int: + """A sane default nlist ~ 4*sqrt(N), clamped, rounded to a power of two.""" + target = 4 * math.sqrt(max(n_db, 1)) + p = 2 ** round(math.log2(max(target, 256))) + return int(max(256, min(p, 16384))) + + +def _write_meta(index_path: str, backend_name: str, dim: int) -> None: + Path(f"{index_path}.meta.json").write_text( + json.dumps( + { + "version": "1.0", + "backend_name": backend_name, + "embedding_model": "synthetic", + "dimensions": dim, + "backend_kwargs": {"distance_metric": "cosine"}, + "embedding_mode": "sentence-transformers", + "passage_sources": [], + } + ) + ) + + +def _index_size_mb(index_path: str) -> float: + stem = Path(index_path).stem + parent = Path(index_path).parent + total = 0 + for p in parent.glob(f"{stem}.*"): + if p.name.endswith(".meta.json"): + continue + total += p.stat().st_size + return total / (1024 * 1024) + + +def build_backend(name: str, db, ids, index_path: str, nlist: int, nprobe: int) -> dict[str, Any]: + from leann.registry import BACKEND_REGISTRY + + kwargs = { + "dimensions": db.shape[1], + "distance_metric": "cosine", + "nlist": nlist, + "nprobe": nprobe, + } + t = time.perf_counter() + BACKEND_REGISTRY[name].builder(**kwargs).build(db, ids, index_path, **kwargs) + build_time = time.perf_counter() - t + _write_meta(index_path, name, db.shape[1]) + return {"build_time": build_time, "index_size_mb": _index_size_mb(index_path)} + + +def make_searcher(name: str, index_path: str): + from leann.registry import BACKEND_REGISTRY + + return BACKEND_REGISTRY[name].searcher(index_path, enable_warmup=False, use_daemon=False) + + +def run_search(searcher, queries, top_k, truth, nprobe, n_single, n_repeat) -> dict[str, Any]: + def search(x): + return searcher.search(x, top_k=top_k, nprobe=nprobe, recompute_embeddings=False) + + single_ms, qps = measure(search, queries, n_single, n_repeat) + out = search(queries) + found = np.array( + [[int(x) if x.lstrip("-").isdigit() else -1 for x in row] for row in out["labels"]], + dtype=np.int64, + ) + recall = recall_at_k(found, truth) + return {"single_ms": single_ms, "throughput_qps": qps, "recall": recall} + + +def run_size(n_db: int, args) -> dict[str, Any]: + import faiss + import torch + + nlist = args.nlist if args.nlist > 0 else auto_nlist(n_db) + print(f"\n{'=' * 80}") + print( + f"Corpus: {n_db:,} vectors x {args.dim} dims | cosine | nlist={nlist} | " + f"{args.queries} queries | top_k={args.top_k}" + ) + print(f"{'=' * 80}") + + db, queries = make_clustered_data(n_db, args.queries, args.dim, args.seed, args.cluster_std) + ids = [str(i) for i in range(n_db)] + print("Computing exact ground truth (GPU)...") + truth = exact_ground_truth(db, queries, args.top_k) + + nprobe_sweep = [p for p in args.nprobe_sweep if p <= nlist] + result: dict[str, Any] = { + "n_db": n_db, + "nlist": nlist, + "nprobe_sweep": nprobe_sweep, + "rows": [], + } + + with tempfile.TemporaryDirectory() as tmp: + # ---- Build both backends once (build cost is a one-time offline step). ---- + faiss.omp_set_num_threads(min(os.cpu_count() or 1, 64)) # build uses many cores + gpu_path = str(Path(tmp) / "flashlib_ivf.leann") + cpu_path = str(Path(tmp) / "ivf.leann") + + print("Building flashlib_ivf (GPU)...") + gb = build_backend("flashlib_ivf", db, ids, gpu_path, nlist, max(nprobe_sweep)) + print(f" build {gb['build_time']:.2f}s | index {gb['index_size_mb']:.1f} MB") + + print("Building ivf (FAISS, CPU)...") + cb = build_backend("ivf", db, ids, cpu_path, nlist, max(nprobe_sweep)) + print(f" build {cb['build_time']:.2f}s | index {cb['index_size_mb']:.1f} MB") + result["flashlib_ivf_build"] = gb + result["ivf_build"] = cb + + # ---- Sweep nprobe; reuse a single searcher per backend across the sweep. ---- + gpu_searcher = make_searcher("flashlib_ivf", gpu_path) + cpu_searcher = make_searcher("ivf", cpu_path) + faiss.omp_set_num_threads(args.n_threads) # constrain SEARCH to CPU budget + + for nprobe in nprobe_sweep: + g = run_search( + gpu_searcher, queries, args.top_k, truth, nprobe, args.single_queries, args.repeat + ) + c = run_search( + cpu_searcher, queries, args.top_k, truth, nprobe, args.single_queries, args.repeat + ) + row = {"nprobe": nprobe, "gpu": g, "cpu": c} + result["rows"].append(row) + lat = c["single_ms"] / g["single_ms"] if g["single_ms"] else float("nan") + tpt = g["throughput_qps"] / c["throughput_qps"] if c["throughput_qps"] else float("nan") + print( + f" nprobe={nprobe:<4} | " + f"GPU {g['single_ms']:7.3f}ms {g['throughput_qps']:>10,.0f}q/s r{g['recall']:.3f} | " + f"CPU {c['single_ms']:7.3f}ms {c['throughput_qps']:>10,.0f}q/s r{c['recall']:.3f} | " + f"speedup {lat:5.1f}x lat {tpt:6.1f}x tpt" + ) + + del gpu_searcher, cpu_searcher + gc.collect() + torch.cuda.empty_cache() + + return result + + +def print_summary(rows: list[dict[str, Any]], args) -> None: + print(f"\n\n{'#' * 84}") + print("# SUMMARY: flashlib_ivf (GPU) vs ivf (FAISS, CPU) - matched nlist, nprobe sweep") + print(f"# CPU baseline used {args.n_threads} thread(s); metric=cosine; top_k={args.top_k}") + print(f"{'#' * 84}") + + rcol = f"R@{args.top_k}" + hdr = ( + f"{'nprobe':>6} | {'GPU ms':>8} {'GPU q/s':>11} {rcol:>6} | " + f"{'CPU ms':>8} {'CPU q/s':>11} {rcol:>6} | {'lat x':>6} {'tpt x':>6}" + ) + for r in rows: + gb, cb = r["flashlib_ivf_build"], r["ivf_build"] + print(f"\n{'-' * len(hdr)}") + print( + f"Corpus {r['n_db']:,} x {args.dim}d | nlist={r['nlist']} | " + f"build: GPU {gb['build_time']:.1f}s ({gb['index_size_mb']:.0f}MB) vs " + f"CPU {cb['build_time']:.1f}s ({cb['index_size_mb']:.0f}MB)" + ) + print("-" * len(hdr)) + print(hdr) + print("-" * len(hdr)) + for row in r["rows"]: + g, c = row["gpu"], row["cpu"] + lat = c["single_ms"] / g["single_ms"] if g["single_ms"] else float("nan") + tpt = g["throughput_qps"] / c["throughput_qps"] if c["throughput_qps"] else float("nan") + print( + f"{row['nprobe']:>6} | {g['single_ms']:>8.3f} {g['throughput_qps']:>11,.0f} " + f"{g['recall']:>6.3f} | {c['single_ms']:>8.3f} {c['throughput_qps']:>11,.0f} " + f"{c['recall']:>6.3f} | {lat:>6.1f} {tpt:>6.1f}" + ) + + +def main() -> None: + p = argparse.ArgumentParser( + description="FlashLib IVF (GPU) vs FAISS IVF (CPU) comparison for LEANN.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + p.add_argument("--sizes", type=int, nargs="+", default=[100_000, 1_000_000]) + p.add_argument("--dim", type=int, default=768) + p.add_argument("--queries", type=int, default=1000) + p.add_argument("--top-k", type=int, default=10) + p.add_argument("--nlist", type=int, default=0, help="IVF partitions (0 = auto ~4*sqrt(N))") + p.add_argument("--nprobe-sweep", type=int, nargs="+", default=[1, 4, 8, 16, 32, 64]) + p.add_argument( + "--cpu-threads", + type=int, + default=0, + help="FAISS CPU search threads (0 = all cores, capped at 32).", + ) + p.add_argument("--cluster-std", type=float, default=0.1, help="Cluster spread (lower=tighter)") + p.add_argument("--single-queries", type=int, default=200) + p.add_argument("--repeat", type=int, default=5) + p.add_argument("--seed", type=int, default=42) + args = p.parse_args() + + try: + import torch + except ImportError: + _fail("PyTorch is required (with CUDA).") + if not torch.cuda.is_available(): + _fail("FlashLib IVF is GPU-only, but no CUDA GPU is available.") + try: + import faiss + import flashlib + except ImportError as e: + _fail(f"Missing dependency: {e}. Need 'flashlib' and 'faiss-cpu'.") + + from leann.registry import BACKEND_REGISTRY, autodiscover_backends + + autodiscover_backends() + for need in ("ivf", "flashlib_ivf"): + if need not in BACKEND_REGISTRY: + _fail( + f"Backend '{need}' not registered. Install it: " + f"pip install -e packages/leann-backend-{need.replace('_', '-')}" + ) + + all_cores = os.cpu_count() or 1 + args.n_threads = args.cpu_threads if args.cpu_threads > 0 else min(all_cores, 32) + + print("FlashLib IVF (GPU) vs FAISS IVF (CPU) comparison for LEANN") + print(f"GPU: {torch.cuda.get_device_name(0)} | CPU cores available: {all_cores}") + print( + f"flashlib {flashlib.__version__} | faiss {faiss.__version__} | torch {torch.__version__}" + ) + print( + f"Config: dim={args.dim}, queries={args.queries}, top_k={args.top_k}, " + f"FAISS search threads={args.n_threads} (build uses up to 64), " + f"nprobe_sweep={args.nprobe_sweep}" + ) + + rows = [run_size(n, args) for n in args.sizes] + print_summary(rows, args) + print("\nDone.") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nInterrupted.") + sys.exit(130) + finally: + sys.stdout.flush() + sys.stderr.flush() + os._exit(0) diff --git a/benchmarks/issue_159.py b/benchmarks/issue_159.py new file mode 100644 index 0000000..43fd9a5 --- /dev/null +++ b/benchmarks/issue_159.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +""" +Test script to reproduce issue #159: Slow search performance +Configuration: +- GPU: A10 +- embedding_model: BAAI/bge-large-zh-v1.5 +- data size: 180M text (~90K chunks) +- backend: hnsw +""" + +import os +import time +from pathlib import Path + +from leann.api import LeannBuilder, LeannSearcher + +os.environ["LEANN_LOG_LEVEL"] = "DEBUG" + +# Configuration matching the issue +INDEX_PATH = "./test_issue_159.leann" +EMBEDDING_MODEL = "BAAI/bge-large-zh-v1.5" +BACKEND_NAME = "hnsw" + + +def generate_test_data(num_chunks=90000, chunk_size=2000): + """Generate test data similar to 180MB text (~90K chunks)""" + # Each chunk is approximately 2000 characters + # 90K chunks * 2000 chars β‰ˆ 180MB + chunks = [] + base_text = ( + "θΏ™ζ˜―δΈ€δΈͺ桋试文摣。LEANNζ˜―δΈ€δΈͺεˆ›ζ–°ηš„ε‘ι‡ζ•°ζεΊ“, ι€šθΏ‡ε›ΎεŸΊι€‰ζ‹©ζ€§ι‡θ‘η—εžηް97%ηš„ε­˜ε‚¨θŠ‚ηœγ€‚" + ) + + for i in range(num_chunks): + chunk = f"{base_text} 文摣编号: {i}. " * (chunk_size // len(base_text) + 1) + chunks.append(chunk[:chunk_size]) + + return chunks + + +def test_search_performance(): + """Test search performance with different configurations""" + print("=" * 80) + print("Testing LEANN Search Performance (Issue #159)") + print("=" * 80) + + meta_path = Path(f"{INDEX_PATH}.meta.json") + if meta_path.exists(): + print(f"\nβœ“ Index already exists at {INDEX_PATH}") + print(" Skipping build phase. Delete the index to rebuild.") + else: + print("\nπŸ“¦ Building index...") + print(f" Backend: {BACKEND_NAME}") + print(f" Embedding Model: {EMBEDDING_MODEL}") + print(" Generating test data (~90K chunks, ~180MB)...") + + chunks = generate_test_data(num_chunks=90000) + print(f" Generated {len(chunks)} chunks") + print(f" Total text size: {sum(len(c) for c in chunks) / (1024 * 1024):.2f} MB") + + builder = LeannBuilder( + backend_name=BACKEND_NAME, + embedding_model=EMBEDDING_MODEL, + ) + + print(" Adding chunks to builder...") + start_time = time.time() + for i, chunk in enumerate(chunks): + builder.add_text(chunk) + if (i + 1) % 10000 == 0: + print(f" Added {i + 1}/{len(chunks)} chunks...") + + print(" Building index...") + build_start = time.time() + builder.build_index(INDEX_PATH) + build_time = time.time() - build_start + print(f" βœ“ Index built in {build_time:.2f} seconds") + + # Test search with different complexity values + print("\nπŸ” Testing search performance...") + searcher = LeannSearcher(INDEX_PATH) + + test_query = "LEANN向量数ζεΊ“ε­˜ε‚¨δΌ˜εŒ–" + + # Test with minimal complexity (8) + print("\n Test 4: Minimal complexity (8)") + print(f" Query: '{test_query}'") + start_time = time.time() + results = searcher.search(test_query, top_k=10, complexity=8) + search_time = time.time() - start_time + print(f" βœ“ Search completed in {search_time:.2f} seconds") + print(f" Results: {len(results)} items") + + print("\n" + "=" * 80) + + +if __name__ == "__main__": + test_search_performance() diff --git a/benchmarks/laion/.gitignore b/benchmarks/laion/.gitignore new file mode 100644 index 0000000..8fce603 --- /dev/null +++ b/benchmarks/laion/.gitignore @@ -0,0 +1 @@ +data/ diff --git a/benchmarks/laion/README.md b/benchmarks/laion/README.md new file mode 100644 index 0000000..51dc721 --- /dev/null +++ b/benchmarks/laion/README.md @@ -0,0 +1,199 @@ +# LAION Multimodal Benchmark + +A multimodal benchmark for evaluating image retrieval and generation performance using LEANN with CLIP embeddings and Qwen2.5-VL for multimodal generation on LAION dataset subset. + +## Overview + +This benchmark evaluates: +- **Image retrieval timing** using caption-based queries +- **Recall@K performance** for image search +- **Complexity analysis** across different search parameters +- **Index size and storage efficiency** +- **Multimodal generation** with Qwen2.5-VL for image understanding and description + +## Dataset Configuration + +- **Dataset**: LAION-400M subset (10,000 images) +- **Embeddings**: Pre-computed CLIP ViT-B/32 (512 dimensions) +- **Queries**: 200 random captions from the dataset +- **Ground Truth**: Self-recall (query caption β†’ original image) + +## Quick Start + +### 1. Setup the benchmark + +```bash +cd benchmarks/laion +python setup_laion.py --num-samples 10000 --num-queries 200 +``` + +This will: +- Create dummy LAION data (10K samples) +- Generate CLIP embeddings (512-dim) +- Build LEANN index with HNSW backend +- Create 200 evaluation queries + +### 2. Run evaluation + +```bash +# Run all evaluation stages +python evaluate_laion.py --index data/laion_index.leann + +# Run specific stages +python evaluate_laion.py --index data/laion_index.leann --stage 2 # Recall evaluation +python evaluate_laion.py --index data/laion_index.leann --stage 3 # Complexity analysis +python evaluate_laion.py --index data/laion_index.leann --stage 4 # Index comparison +python evaluate_laion.py --index data/laion_index.leann --stage 5 # Multimodal generation + +# Multimodal generation with Qwen2.5-VL +python evaluate_laion.py --index data/laion_index.leann --stage 5 --model-name Qwen/Qwen2.5-VL-7B-Instruct +``` + +### 3. Save results + +```bash +python evaluate_laion.py --index data/laion_index.leann --output results.json +``` + +## Configuration Options + +### Setup Options +```bash +python setup_laion.py \ + --num-samples 10000 \ + --num-queries 200 \ + --index-path data/laion_index.leann \ + --backend hnsw +``` + +### Evaluation Options +```bash +python evaluate_laion.py \ + --index data/laion_index.leann \ + --queries data/evaluation_queries.jsonl \ + --complexity 64 \ + --top-k 3 \ + --num-samples 100 \ + --stage all +``` + +## Evaluation Stages + +### Stage 2: Recall Evaluation +- Evaluates Recall@3 for multimodal retrieval +- Compares LEANN vs FAISS baseline performance +- Self-recall: query caption should retrieve original image + +### Stage 3: Complexity Analysis +- Binary search for optimal complexity (90% recall target) +- Tests performance across different complexity levels +- Analyzes speed vs. accuracy tradeoffs + +### Stage 4: Index Comparison +- Compares compact vs non-compact index sizes +- Measures search performance differences +- Reports storage efficiency and speed ratios + +### Stage 5: Multimodal Generation +- Uses Qwen2.5-VL for image understanding and description +- Retrieval-Augmented Generation (RAG) with multimodal context +- Measures both search and generation timing + +## Output Metrics + +### Timing Metrics +- Average/median/min/max search time +- Standard deviation +- Searches per second +- Latency in milliseconds + +### Recall Metrics +- Recall@3 percentage for image retrieval +- Number of queries with ground truth + +### Index Metrics +- Total index size (MB) +- Component breakdown (index, passages, metadata) +- Storage savings (compact vs non-compact) +- Backend and embedding model info + +### Generation Metrics (Stage 5) +- Average search time per query +- Average generation time per query +- Time distribution (search vs generation) +- Sample multimodal responses +- Model: Qwen2.5-VL performance + +## Benchmark Results + +### LEANN-RAG Performance (CLIP ViT-L/14 + Qwen2.5-VL) + +**Stage 3: Optimal Complexity Analysis** +- **Optimal Complexity**: 85 (achieving 90% Recall@3) +- **Binary Search Range**: 1-128 +- **Target Recall**: 90% +- **Index Type**: Non-compact (for fast binary search) + +**Stage 5: Multimodal Generation Performance (Qwen2.5-VL)** +- **Total Queries**: 20 +- **Average Search Time**: 1.200s per query +- **Average Generation Time**: 6.558s per query +- **Time Distribution**: Search 15.5%, Generation 84.5% +- **LLM Backend**: HuggingFace transformers +- **Model**: Qwen/Qwen2.5-VL-7B-Instruct +- **Optimal Complexity**: 85 + +**System Performance:** +- **Index Size**: ~10,000 image embeddings from LAION subset +- **Embedding Model**: CLIP ViT-L/14 (768 dimensions) +- **Backend**: HNSW with cosine distance + +### Example Results + +``` +🎯 LAION MULTIMODAL BENCHMARK RESULTS +============================================================ + +πŸ“Š Multimodal Generation Results: + Total Queries: 20 + Avg Search Time: 1.200s + Avg Generation Time: 6.558s + Time Distribution: Search 15.5%, Generation 84.5% + LLM Backend: HuggingFace transformers + Model: Qwen/Qwen2.5-VL-7B-Instruct + +βš™οΈ Optimal Complexity Analysis: + Target Recall: 90% + Optimal Complexity: 85 + Binary Search Range: 1-128 + Non-compact Index (fast search, no recompute) + +πŸš€ Performance Summary: + Multimodal RAG: 7.758s total per query + Search: 15.5% of total time + Generation: 84.5% of total time +``` + +## Directory Structure + +``` +benchmarks/laion/ +β”œβ”€β”€ setup_laion.py # Setup script +β”œβ”€β”€ evaluate_laion.py # Evaluation script +β”œβ”€β”€ README.md # This file +└── data/ # Generated data + β”œβ”€β”€ laion_images/ # Image files (placeholder) + β”œβ”€β”€ laion_metadata.jsonl # Image metadata + β”œβ”€β”€ laion_passages.jsonl # LEANN passages + β”œβ”€β”€ laion_embeddings.npy # CLIP embeddings + β”œβ”€β”€ evaluation_queries.jsonl # Evaluation queries + └── laion_index.leann/ # LEANN index files +``` + +## Notes + +- Current implementation uses dummy data for demonstration +- For real LAION data, implement actual download logic in `setup_laion.py` +- CLIP embeddings are randomly generated - replace with real CLIP model for production +- Adjust `num_samples` and `num_queries` based on available resources +- Consider using `--num-samples` during evaluation for faster testing diff --git a/benchmarks/laion/evaluate_laion.py b/benchmarks/laion/evaluate_laion.py new file mode 100644 index 0000000..dd30635 --- /dev/null +++ b/benchmarks/laion/evaluate_laion.py @@ -0,0 +1,725 @@ +""" +LAION Multimodal Benchmark Evaluation Script - Modular Recall-based Evaluation +""" + +import argparse +import json +import logging +import os +import pickle +import time +from pathlib import Path + +import numpy as np +from leann import LeannSearcher +from leann_backend_hnsw import faiss +from sentence_transformers import SentenceTransformer + +from ..llm_utils import evaluate_multimodal_rag, load_qwen_vl_model + +# Setup logging to reduce verbose output +logging.basicConfig(level=logging.WARNING) +logging.getLogger("leann.api").setLevel(logging.WARNING) +logging.getLogger("leann_backend_hnsw").setLevel(logging.WARNING) + + +class RecallEvaluator: + """Stage 2: Evaluate Recall@3 (LEANN vs FAISS baseline for multimodal retrieval)""" + + def __init__(self, index_path: str, baseline_dir: str): + self.index_path = index_path + self.baseline_dir = baseline_dir + self.searcher = LeannSearcher(index_path) + + # Load FAISS flat baseline (image embeddings) + baseline_index_path = os.path.join(baseline_dir, "faiss_flat.index") + metadata_path = os.path.join(baseline_dir, "metadata.pkl") + + self.faiss_index = faiss.read_index(baseline_index_path) + with open(metadata_path, "rb") as f: + self.image_ids = pickle.load(f) + print(f"πŸ“š Loaded FAISS flat baseline with {self.faiss_index.ntotal} image vectors") + + # Load sentence-transformers CLIP for text embedding (ViT-L/14) + self.st_clip = SentenceTransformer("clip-ViT-L-14") + + def evaluate_recall_at_3( + self, captions: list[str], complexity: int = 64, recompute_embeddings: bool = True + ) -> float: + """Evaluate recall@3 for multimodal retrieval: caption queries -> image results""" + recompute_str = "with recompute" if recompute_embeddings else "no recompute" + print(f"πŸ” Evaluating recall@3 with complexity={complexity} ({recompute_str})...") + + total_recall = 0.0 + num_queries = len(captions) + + for i, caption in enumerate(captions): + # Get ground truth: search with FAISS flat using caption text embedding + # Generate CLIP text embedding for caption via sentence-transformers (normalized) + query_embedding = self.st_clip.encode( + [caption], convert_to_numpy=True, normalize_embeddings=True, show_progress_bar=False + ).astype(np.float32) + + # Search FAISS flat for ground truth using LEANN's modified faiss API + n = query_embedding.shape[0] # Number of queries + k = 3 # Number of nearest neighbors + distances = np.zeros((n, k), dtype=np.float32) + labels = np.zeros((n, k), dtype=np.int64) + + self.faiss_index.search( + n, + faiss.swig_ptr(query_embedding), + k, + faiss.swig_ptr(distances), + faiss.swig_ptr(labels), + ) + + # Extract the results (image IDs from FAISS) + baseline_ids = {self.image_ids[idx] for idx in labels[0]} + + # Search with LEANN at specified complexity (using caption as text query) + test_results = self.searcher.search( + caption, + top_k=3, + complexity=complexity, + recompute_embeddings=recompute_embeddings, + ) + test_ids = {result.id for result in test_results} + + # Calculate recall@3 = |intersection| / |ground_truth| + intersection = test_ids.intersection(baseline_ids) + recall = len(intersection) / 3.0 # Ground truth size is 3 + total_recall += recall + + if i < 3: # Show first few examples + print(f" Query {i + 1}: '{caption[:50]}...' -> Recall@3: {recall:.3f}") + print(f" FAISS ground truth: {list(baseline_ids)}") + print(f" LEANN results (C={complexity}, {recompute_str}): {list(test_ids)}") + print(f" Intersection: {list(intersection)}") + + avg_recall = total_recall / num_queries + print(f"πŸ“Š Average Recall@3: {avg_recall:.3f} ({avg_recall * 100:.1f}%)") + return avg_recall + + def cleanup(self): + """Cleanup resources""" + if hasattr(self, "searcher"): + self.searcher.cleanup() + + +class LAIONEvaluator: + def __init__(self, index_path: str): + self.index_path = index_path + self.searcher = LeannSearcher(index_path) + + def load_queries(self, queries_file: str) -> list[str]: + """Load caption queries from evaluation file""" + captions = [] + with open(queries_file, encoding="utf-8") as f: + for line in f: + if line.strip(): + query_data = json.loads(line) + captions.append(query_data["query"]) + + print(f"πŸ“Š Loaded {len(captions)} caption queries") + return captions + + def analyze_index_sizes(self) -> dict: + """Analyze index sizes, emphasizing .index only (exclude passages).""" + print("πŸ“ Analyzing index sizes (.index only)...") + + # Get all index-related files + index_path = Path(self.index_path) + index_dir = index_path.parent + index_name = index_path.stem # Remove .leann extension + + sizes: dict[str, float] = {} + + # Core index files + index_file = index_dir / f"{index_name}.index" + meta_file = index_dir / f"{index_path.name}.meta.json" # Keep .leann for meta file + passages_file = index_dir / f"{index_path.name}.passages.jsonl" # Keep .leann for passages + passages_idx_file = index_dir / f"{index_path.name}.passages.idx" # Keep .leann for idx + + # Core index size (.index only) + index_mb = index_file.stat().st_size / (1024 * 1024) if index_file.exists() else 0.0 + sizes["index_only_mb"] = index_mb + + # Other files for reference (not counted in index_only_mb) + sizes["metadata_mb"] = ( + meta_file.stat().st_size / (1024 * 1024) if meta_file.exists() else 0.0 + ) + sizes["passages_text_mb"] = ( + passages_file.stat().st_size / (1024 * 1024) if passages_file.exists() else 0.0 + ) + sizes["passages_index_mb"] = ( + passages_idx_file.stat().st_size / (1024 * 1024) if passages_idx_file.exists() else 0.0 + ) + + print(f" πŸ“ .index size: {index_mb:.1f} MB") + if sizes["metadata_mb"]: + print(f" 🧾 metadata: {sizes['metadata_mb']:.3f} MB") + if sizes["passages_text_mb"] or sizes["passages_index_mb"]: + print( + f" (passages excluded) text: {sizes['passages_text_mb']:.1f} MB, idx: {sizes['passages_index_mb']:.1f} MB" + ) + + return sizes + + def create_non_compact_index_for_comparison(self, non_compact_index_path: str) -> dict: + """Create a non-compact index for comparison purposes""" + print("πŸ—οΈ Building non-compact index from existing passages...") + + # Load existing passages from current index + from leann import LeannBuilder + + current_index_path = Path(self.index_path) + current_index_dir = current_index_path.parent + current_index_name = current_index_path.name + + # Read metadata to get passage source + meta_path = current_index_dir / f"{current_index_name}.meta.json" + with open(meta_path) as f: + meta = json.load(f) + + passage_source = meta["passage_sources"][0] + passage_file = passage_source["path"] + + # Convert relative path to absolute + if not Path(passage_file).is_absolute(): + passage_file = current_index_dir / Path(passage_file).name + + print(f"πŸ“„ Loading passages from {passage_file}...") + + # Load CLIP embeddings + embeddings_file = current_index_dir / "clip_image_embeddings.npy" + embeddings = np.load(embeddings_file) + print(f"πŸ“ Loaded embeddings shape: {embeddings.shape}") + + # Build non-compact index with same passages and embeddings + builder = LeannBuilder( + backend_name="hnsw", + # Use CLIP text encoder (ViT-L/14) to match image embeddings (768-dim) + embedding_model="clip-ViT-L-14", + embedding_mode="sentence-transformers", + is_recompute=False, # Disable recompute (store embeddings) + is_compact=False, # Disable compact storage + distance_metric="cosine", + **{ + k: v + for k, v in meta.get("backend_kwargs", {}).items() + if k not in ["is_recompute", "is_compact", "distance_metric"] + }, + ) + + # Prepare ids and add passages + ids: list[str] = [] + with open(passage_file, encoding="utf-8") as f: + for line in f: + if line.strip(): + data = json.loads(line) + ids.append(str(data["id"])) + # Ensure metadata contains the id used by the vector index + metadata = {**data.get("metadata", {}), "id": data["id"]} + builder.add_text(text=data["text"], metadata=metadata) + + if len(ids) != embeddings.shape[0]: + raise ValueError( + f"IDs count ({len(ids)}) does not match embeddings ({embeddings.shape[0]})." + ) + + # Persist a pickle for build_index_from_embeddings + pkl_path = current_index_dir / "clip_image_embeddings.pkl" + with open(pkl_path, "wb") as pf: + pickle.dump((ids, embeddings.astype(np.float32)), pf) + + print( + f"πŸ”¨ Building non-compact index at {non_compact_index_path} from precomputed embeddings..." + ) + builder.build_index_from_embeddings(non_compact_index_path, str(pkl_path)) + + # Analyze the non-compact index size + temp_evaluator = LAIONEvaluator(non_compact_index_path) + non_compact_sizes = temp_evaluator.analyze_index_sizes() + non_compact_sizes["index_type"] = "non_compact" + + return non_compact_sizes + + def compare_index_performance( + self, non_compact_path: str, compact_path: str, test_captions: list, complexity: int + ) -> dict: + """Compare performance between non-compact and compact indexes""" + print("⚑ Comparing search performance between indexes...") + + # Test queries + test_queries = test_captions[:5] + + results = { + "non_compact": {"search_times": []}, + "compact": {"search_times": []}, + "avg_search_times": {}, + "speed_ratio": 0.0, + } + + # Test non-compact index (no recompute) + print(" πŸ” Testing non-compact index (no recompute)...") + non_compact_searcher = LeannSearcher(non_compact_path) + + for caption in test_queries: + start_time = time.time() + _ = non_compact_searcher.search( + caption, top_k=3, complexity=complexity, recompute_embeddings=False + ) + search_time = time.time() - start_time + results["non_compact"]["search_times"].append(search_time) + + # Test compact index (with recompute) + print(" πŸ” Testing compact index (with recompute)...") + compact_searcher = LeannSearcher(compact_path) + + for caption in test_queries: + start_time = time.time() + _ = compact_searcher.search( + caption, top_k=3, complexity=complexity, recompute_embeddings=True + ) + search_time = time.time() - start_time + results["compact"]["search_times"].append(search_time) + + # Calculate averages + results["avg_search_times"]["non_compact"] = sum( + results["non_compact"]["search_times"] + ) / len(results["non_compact"]["search_times"]) + results["avg_search_times"]["compact"] = sum(results["compact"]["search_times"]) / len( + results["compact"]["search_times"] + ) + + # Performance ratio + if results["avg_search_times"]["compact"] > 0: + results["speed_ratio"] = ( + results["avg_search_times"]["non_compact"] / results["avg_search_times"]["compact"] + ) + else: + results["speed_ratio"] = float("inf") + + print( + f" Non-compact (no recompute): {results['avg_search_times']['non_compact']:.3f}s avg" + ) + print(f" Compact (with recompute): {results['avg_search_times']['compact']:.3f}s avg") + print(f" Speed ratio: {results['speed_ratio']:.2f}x") + + # Cleanup + non_compact_searcher.cleanup() + compact_searcher.cleanup() + + return results + + def _print_results(self, timing_metrics: dict): + """Print evaluation results""" + print("\n🎯 LAION MULTIMODAL BENCHMARK RESULTS") + print("=" * 60) + + # Index comparison analysis (prefer .index-only view if present) + if "current_index" in timing_metrics and "non_compact_index" in timing_metrics: + current = timing_metrics["current_index"] + non_compact = timing_metrics["non_compact_index"] + + if "index_only_mb" in current and "index_only_mb" in non_compact: + print("\nπŸ“ Index Comparison Analysis (.index only):") + print(f" Compact index (current): {current.get('index_only_mb', 0):.1f} MB") + print(f" Non-compact index: {non_compact.get('index_only_mb', 0):.1f} MB") + print( + f" Storage saving by compact: {timing_metrics.get('storage_saving_percent', 0):.1f}%" + ) + # Show excluded components for reference if available + if any( + k in non_compact + for k in ("passages_text_mb", "passages_index_mb", "metadata_mb") + ): + print(" (passages excluded in totals, shown for reference):") + print( + f" - Passages text: {non_compact.get('passages_text_mb', 0):.1f} MB, " + f"Passages index: {non_compact.get('passages_index_mb', 0):.1f} MB, " + f"Metadata: {non_compact.get('metadata_mb', 0):.3f} MB" + ) + else: + # Fallback to legacy totals if running with older metrics + print("\nπŸ“ Index Comparison Analysis:") + print( + f" Compact index (current): {current.get('total_with_embeddings', 0):.1f} MB" + ) + print( + f" Non-compact index (with embeddings): {non_compact.get('total_with_embeddings', 0):.1f} MB" + ) + print( + f" Storage saving by compact: {timing_metrics.get('storage_saving_percent', 0):.1f}%" + ) + print(" Component breakdown (non-compact):") + print(f" - Main index: {non_compact.get('index', 0):.1f} MB") + print(f" - Passages text: {non_compact.get('passages_text', 0):.1f} MB") + print(f" - Passages index: {non_compact.get('passages_index', 0):.1f} MB") + print(f" - Metadata: {non_compact.get('metadata', 0):.1f} MB") + + # Performance comparison + if "performance_comparison" in timing_metrics: + perf = timing_metrics["performance_comparison"] + print("\n⚑ Performance Comparison:") + print( + f" Non-compact (no recompute): {perf.get('avg_search_times', {}).get('non_compact', 0):.3f}s avg" + ) + print( + f" Compact (with recompute): {perf.get('avg_search_times', {}).get('compact', 0):.3f}s avg" + ) + print(f" Speed ratio: {perf.get('speed_ratio', 0):.2f}x") + + # Legacy single index analysis (fallback) + if "total_with_embeddings" in timing_metrics and "current_index" not in timing_metrics: + print("\nπŸ“ Index Size Analysis:") + print( + f" Index with embeddings: {timing_metrics.get('total_with_embeddings', 0):.1f} MB" + ) + print( + f" Estimated pruned index: {timing_metrics.get('total_without_embeddings', 0):.1f} MB" + ) + print(f" Compression ratio: {timing_metrics.get('compression_ratio', 0):.2f}x") + + def cleanup(self): + """Cleanup resources""" + if self.searcher: + self.searcher.cleanup() + + +def main(): + parser = argparse.ArgumentParser(description="LAION Multimodal Benchmark Evaluation") + parser.add_argument("--index", required=True, help="Path to LEANN index") + parser.add_argument( + "--queries", default="data/evaluation_queries.jsonl", help="Path to evaluation queries" + ) + parser.add_argument( + "--stage", + choices=["2", "3", "4", "5", "all"], + default="all", + help="Which stage to run (2=recall, 3=complexity, 4=index comparison, 5=generation)", + ) + parser.add_argument("--complexity", type=int, default=None, help="Complexity for search") + parser.add_argument("--baseline-dir", default="baseline", help="Baseline output directory") + parser.add_argument("--output", help="Save results to JSON file") + parser.add_argument( + "--llm-backend", + choices=["hf"], + default="hf", + help="LLM backend (Qwen2.5-VL only supports HF)", + ) + parser.add_argument( + "--model-name", default="Qwen/Qwen2.5-VL-7B-Instruct", help="Multimodal model name" + ) + + args = parser.parse_args() + + try: + # Check if baseline exists + baseline_index_path = os.path.join(args.baseline_dir, "faiss_flat.index") + if not os.path.exists(baseline_index_path): + print(f"❌ FAISS baseline not found at {baseline_index_path}") + print("πŸ’‘ Please run setup_laion.py first to build the baseline") + exit(1) + + if args.stage == "2" or args.stage == "all": + # Stage 2: Recall@3 evaluation + print("πŸš€ Starting Stage 2: Recall@3 evaluation for multimodal retrieval") + + evaluator = RecallEvaluator(args.index, args.baseline_dir) + + # Load caption queries for testing + laion_evaluator = LAIONEvaluator(args.index) + captions = laion_evaluator.load_queries(args.queries) + + # Test with queries for robust measurement + test_captions = captions[:100] # Use subset for speed + print(f"πŸ§ͺ Testing with {len(test_captions)} caption queries") + + # Test with complexity 64 + complexity = 64 + recall = evaluator.evaluate_recall_at_3(test_captions, complexity) + print(f"πŸ“ˆ Recall@3 at complexity {complexity}: {recall * 100:.1f}%") + + evaluator.cleanup() + print("βœ… Stage 2 completed!\n") + + # Shared non-compact index path for Stage 3 and 4 + non_compact_index_path = args.index.replace(".leann", "_noncompact.leann") + complexity = args.complexity + + if args.stage == "3" or args.stage == "all": + # Stage 3: Binary search for 90% recall complexity + print("πŸš€ Starting Stage 3: Binary search for 90% recall complexity") + print( + "πŸ’‘ Creating non-compact index for fast binary search with recompute_embeddings=False" + ) + + # Create non-compact index for binary search + print("πŸ—οΈ Creating non-compact index for binary search...") + evaluator = LAIONEvaluator(args.index) + evaluator.create_non_compact_index_for_comparison(non_compact_index_path) + + # Use non-compact index for binary search + binary_search_evaluator = RecallEvaluator(non_compact_index_path, args.baseline_dir) + + # Load caption queries for testing + captions = evaluator.load_queries(args.queries) + + # Use subset for robust measurement + test_captions = captions[:50] # Smaller subset for binary search speed + print(f"πŸ§ͺ Testing with {len(test_captions)} caption queries") + + # Binary search for 90% recall complexity + target_recall = 0.9 + min_complexity, max_complexity = 1, 128 + + print(f"πŸ” Binary search for {target_recall * 100}% recall complexity...") + print(f"Search range: {min_complexity} to {max_complexity}") + + best_complexity = None + best_recall = 0.0 + + while min_complexity <= max_complexity: + mid_complexity = (min_complexity + max_complexity) // 2 + + print( + f"\nπŸ§ͺ Testing complexity {mid_complexity} (no recompute, non-compact index)..." + ) + # Use recompute_embeddings=False on non-compact index for fast binary search + recall = binary_search_evaluator.evaluate_recall_at_3( + test_captions, mid_complexity, recompute_embeddings=False + ) + + print( + f" Complexity {mid_complexity}: Recall@3 = {recall:.3f} ({recall * 100:.1f}%)" + ) + + if recall >= target_recall: + best_complexity = mid_complexity + best_recall = recall + max_complexity = mid_complexity - 1 + print(" βœ… Target reached! Searching for lower complexity...") + else: + min_complexity = mid_complexity + 1 + print(" ❌ Below target. Searching for higher complexity...") + + if best_complexity is not None: + print("\n🎯 Optimal complexity found!") + print(f" Complexity: {best_complexity}") + print(f" Recall@3: {best_recall:.3f} ({best_recall * 100:.1f}%)") + + # Test a few complexities around the optimal one for verification + print("\nπŸ”¬ Verification test around optimal complexity:") + verification_complexities = [ + max(1, best_complexity - 2), + max(1, best_complexity - 1), + best_complexity, + best_complexity + 1, + best_complexity + 2, + ] + + for complexity in verification_complexities: + if complexity <= 512: # reasonable upper bound + recall = binary_search_evaluator.evaluate_recall_at_3( + test_captions, complexity, recompute_embeddings=False + ) + status = "βœ…" if recall >= target_recall else "❌" + print(f" {status} Complexity {complexity:3d}: {recall * 100:5.1f}%") + + # Now test the optimal complexity with compact index and recompute for comparison + print( + f"\nπŸ”„ Testing optimal complexity {best_complexity} on compact index WITH recompute..." + ) + compact_evaluator = RecallEvaluator(args.index, args.baseline_dir) + recall_with_recompute = compact_evaluator.evaluate_recall_at_3( + test_captions[:10], best_complexity, recompute_embeddings=True + ) + print( + f" βœ… Complexity {best_complexity} (compact index with recompute): {recall_with_recompute * 100:.1f}%" + ) + complexity = best_complexity + print( + f" πŸ“Š Recall difference: {abs(best_recall - recall_with_recompute) * 100:.2f}%" + ) + compact_evaluator.cleanup() + else: + print(f"\n❌ Could not find complexity achieving {target_recall * 100}% recall") + print("All tested complexities were below target.") + + # Cleanup evaluators (keep non-compact index for Stage 4) + binary_search_evaluator.cleanup() + evaluator.cleanup() + + print("βœ… Stage 3 completed! Non-compact index saved for Stage 4.\n") + + if args.stage == "4" or args.stage == "all": + # Stage 4: Index comparison (without LLM generation) + print("πŸš€ Starting Stage 4: Index comparison analysis") + + # Use LAION evaluator for index comparison + evaluator = LAIONEvaluator(args.index) + + # Load caption queries + captions = evaluator.load_queries(args.queries) + + # Step 1: Analyze current (compact) index + print("\nπŸ“ Analyzing current index (compact, pruned)...") + compact_size_metrics = evaluator.analyze_index_sizes() + compact_size_metrics["index_type"] = "compact" + + # Step 2: Use existing non-compact index or create if needed + if Path(non_compact_index_path).exists(): + print( + f"\nπŸ“ Using existing non-compact index from Stage 3: {non_compact_index_path}" + ) + temp_evaluator = LAIONEvaluator(non_compact_index_path) + non_compact_size_metrics = temp_evaluator.analyze_index_sizes() + non_compact_size_metrics["index_type"] = "non_compact" + else: + print("\nπŸ—οΈ Creating non-compact index (with embeddings) for comparison...") + non_compact_size_metrics = evaluator.create_non_compact_index_for_comparison( + non_compact_index_path + ) + + # Step 3: Compare index sizes (.index only) + print("\nπŸ“Š Index size comparison (.index only):") + print( + f" Compact index (current): {compact_size_metrics.get('index_only_mb', 0):.1f} MB" + ) + print(f" Non-compact index: {non_compact_size_metrics.get('index_only_mb', 0):.1f} MB") + + storage_saving = 0.0 + if non_compact_size_metrics.get("index_only_mb", 0) > 0: + storage_saving = ( + ( + non_compact_size_metrics.get("index_only_mb", 0) + - compact_size_metrics.get("index_only_mb", 0) + ) + / non_compact_size_metrics.get("index_only_mb", 1) + * 100 + ) + print(f" Storage saving by compact: {storage_saving:.1f}%") + + # Step 4: Performance comparison between the two indexes + if complexity is None: + raise ValueError("Complexity is required for index comparison") + + print("\n⚑ Performance comparison between indexes...") + performance_metrics = evaluator.compare_index_performance( + non_compact_index_path, args.index, captions[:10], complexity=complexity + ) + + # Combine all metrics + combined_metrics = { + "current_index": compact_size_metrics, + "non_compact_index": non_compact_size_metrics, + "performance_comparison": performance_metrics, + "storage_saving_percent": storage_saving, + } + + # Print comprehensive results + evaluator._print_results(combined_metrics) + + # Save results if requested + if args.output: + print(f"\nπŸ’Ύ Saving results to {args.output}...") + with open(args.output, "w") as f: + json.dump(combined_metrics, f, indent=2, default=str) + print(f"βœ… Results saved to {args.output}") + + evaluator.cleanup() + print("βœ… Stage 4 completed!\n") + + if args.stage in ("5", "all"): + print("πŸš€ Starting Stage 5: Multimodal generation with Qwen2.5-VL") + evaluator = LAIONEvaluator(args.index) + captions = evaluator.load_queries(args.queries) + test_captions = captions[: min(20, len(captions))] # Use subset for generation + + print(f"πŸ§ͺ Testing multimodal generation with {len(test_captions)} queries") + + # Load Qwen2.5-VL model + try: + print("Loading Qwen2.5-VL model...") + processor, model = load_qwen_vl_model(args.model_name) + + # Run multimodal generation evaluation + complexity = args.complexity or 64 + gen_results = evaluate_multimodal_rag( + evaluator.searcher, + test_captions, + processor=processor, + model=model, + complexity=complexity, + ) + + print("\nπŸ“Š Multimodal Generation Results:") + print(f" Total Queries: {len(test_captions)}") + print(f" Avg Search Time: {gen_results['avg_search_time']:.3f}s") + print(f" Avg Generation Time: {gen_results['avg_generation_time']:.3f}s") + total_time = gen_results["avg_search_time"] + gen_results["avg_generation_time"] + search_pct = (gen_results["avg_search_time"] / total_time) * 100 + gen_pct = (gen_results["avg_generation_time"] / total_time) * 100 + print(f" Time Distribution: Search {search_pct:.1f}%, Generation {gen_pct:.1f}%") + print(" LLM Backend: HuggingFace transformers") + print(f" Model: {args.model_name}") + + # Show sample results + print("\nπŸ“ Sample Multimodal Generations:") + for i, response in enumerate(gen_results["results"][:3]): + # Handle both string and dict formats for captions + if isinstance(test_captions[i], dict): + caption_text = test_captions[i].get("query", str(test_captions[i])) + else: + caption_text = str(test_captions[i]) + print(f" Query {i + 1}: {caption_text[:60]}...") + print(f" Response {i + 1}: {response[:100]}...") + print() + + except Exception as e: + print(f"❌ Multimodal generation evaluation failed: {e}") + print("πŸ’‘ Make sure transformers and Qwen2.5-VL are installed") + import traceback + + traceback.print_exc() + + evaluator.cleanup() + print("βœ… Stage 5 completed!\n") + + if args.stage == "all": + print("πŸŽ‰ All evaluation stages completed successfully!") + print("\nπŸ“‹ Summary:") + print(" Stage 2: βœ… Multimodal Recall@3 evaluation completed") + print(" Stage 3: βœ… Optimal complexity found") + print(" Stage 4: βœ… Index comparison analysis completed") + print(" Stage 5: βœ… Multimodal generation evaluation completed") + print("\nπŸ”§ Recommended next steps:") + print(" - Use optimal complexity for best speed/accuracy balance") + print(" - Review index comparison for storage vs performance tradeoffs") + + # Clean up non-compact index after all stages complete + print("\n🧹 Cleaning up temporary non-compact index...") + if Path(non_compact_index_path).exists(): + temp_index_dir = Path(non_compact_index_path).parent + temp_index_name = Path(non_compact_index_path).name + for temp_file in temp_index_dir.glob(f"{temp_index_name}*"): + temp_file.unlink() + print(f"βœ… Cleaned up {non_compact_index_path}") + else: + print("πŸ“ No temporary index to clean up") + + except KeyboardInterrupt: + print("\n⚠️ Evaluation interrupted by user") + exit(1) + except Exception as e: + print(f"\n❌ Stage {args.stage} failed: {e}") + import traceback + + traceback.print_exc() + exit(1) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/laion/setup_laion.py b/benchmarks/laion/setup_laion.py new file mode 100644 index 0000000..849e7e6 --- /dev/null +++ b/benchmarks/laion/setup_laion.py @@ -0,0 +1,576 @@ +""" +LAION Multimodal Benchmark Setup Script +Downloads LAION subset and builds LEANN index with sentence embeddings +""" + +import argparse +import asyncio +import io +import json +import os +import pickle +import time +from pathlib import Path + +import aiohttp +import numpy as np +from datasets import load_dataset +from leann import LeannBuilder +from PIL import Image +from sentence_transformers import SentenceTransformer +from tqdm import tqdm + + +class LAIONSetup: + def __init__(self, data_dir: str = "data"): + self.data_dir = Path(data_dir) + self.images_dir = self.data_dir / "laion_images" + self.metadata_file = self.data_dir / "laion_metadata.jsonl" + + # Create directories + self.data_dir.mkdir(exist_ok=True) + self.images_dir.mkdir(exist_ok=True) + + async def download_single_image(self, session, sample_data, semaphore, progress_bar): + """Download a single image asynchronously""" + async with semaphore: # Limit concurrent downloads + try: + image_url = sample_data["url"] + image_path = sample_data["image_path"] + + # Skip if already exists + if os.path.exists(image_path): + progress_bar.update(1) + return sample_data + + async with session.get(image_url, timeout=10) as response: + if response.status == 200: + content = await response.read() + + # Verify it's a valid image + try: + img = Image.open(io.BytesIO(content)) + img = img.convert("RGB") + img.save(image_path, "JPEG") + progress_bar.update(1) + return sample_data + except Exception: + progress_bar.update(1) + return None # Skip invalid images + else: + progress_bar.update(1) + return None + + except Exception: + progress_bar.update(1) + return None + + def download_laion_subset(self, num_samples: int = 1000): + """Download LAION subset from HuggingFace datasets with async parallel downloading""" + print(f"πŸ“₯ Downloading LAION subset ({num_samples} samples)...") + + # Load LAION-400M subset from HuggingFace + print("πŸ€— Loading from HuggingFace datasets...") + dataset = load_dataset("laion/laion400m", split="train", streaming=True) + + # Collect sample metadata first (fast) + print("πŸ“‹ Collecting sample metadata...") + candidates = [] + for sample in dataset: + if len(candidates) >= num_samples * 3: # Get 3x more candidates in case some fail + break + + image_url = sample.get("url", "") + caption = sample.get("caption", "") + + if not image_url or not caption: + continue + + image_filename = f"laion_{len(candidates):06d}.jpg" + image_path = self.images_dir / image_filename + + candidate = { + "id": f"laion_{len(candidates):06d}", + "url": image_url, + "caption": caption, + "image_path": str(image_path), + "width": sample.get("original_width", 512), + "height": sample.get("original_height", 512), + "similarity": sample.get("similarity", 0.0), + } + candidates.append(candidate) + + print( + f"πŸ“Š Collected {len(candidates)} candidates, downloading {num_samples} in parallel..." + ) + + # Download images in parallel + async def download_batch(): + semaphore = asyncio.Semaphore(20) # Limit to 20 concurrent downloads + connector = aiohttp.TCPConnector(limit=100, limit_per_host=20) + timeout = aiohttp.ClientTimeout(total=30) + + progress_bar = tqdm(total=len(candidates[: num_samples * 2]), desc="Downloading images") + + async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session: + tasks = [] + for candidate in candidates[: num_samples * 2]: # Try 2x more than needed + task = self.download_single_image(session, candidate, semaphore, progress_bar) + tasks.append(task) + + # Wait for all downloads + results = await asyncio.gather(*tasks, return_exceptions=True) + progress_bar.close() + + # Filter successful downloads + successful = [r for r in results if r is not None and not isinstance(r, Exception)] + return successful[:num_samples] + + # Run async download + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + samples = loop.run_until_complete(download_batch()) + finally: + loop.close() + + # Save metadata + with open(self.metadata_file, "w", encoding="utf-8") as f: + for sample in samples: + f.write(json.dumps(sample) + "\n") + + print(f"βœ… Downloaded {len(samples)} real LAION samples with async parallel downloading") + return samples + + def generate_clip_image_embeddings(self, samples: list[dict]): + """Generate CLIP image embeddings for downloaded images""" + print("πŸ” Generating CLIP image embeddings...") + + # Load sentence-transformers CLIP (ViT-L/14, 768-dim) for image embeddings + # This single model can encode both images and text. + model = SentenceTransformer("clip-ViT-L-14") + + embeddings = [] + valid_samples = [] + + for sample in tqdm(samples, desc="Processing images"): + try: + # Load image + image_path = sample["image_path"] + image = Image.open(image_path).convert("RGB") + + # Encode image to 768-dim embedding via sentence-transformers (normalized) + vec = model.encode( + [image], + convert_to_numpy=True, + normalize_embeddings=True, + batch_size=1, + show_progress_bar=False, + )[0] + embeddings.append(vec.astype(np.float32)) + valid_samples.append(sample) + + except Exception as e: + print(f" ⚠️ Failed to process {sample['id']}: {e}") + # Skip invalid images + + embeddings = np.array(embeddings, dtype=np.float32) + + # Save embeddings + embeddings_file = self.data_dir / "clip_image_embeddings.npy" + np.save(embeddings_file, embeddings) + print(f"βœ… Generated {len(embeddings)} image embeddings, shape: {embeddings.shape}") + + return embeddings, valid_samples + + def build_faiss_baseline( + self, embeddings: np.ndarray, samples: list[dict], output_dir: str = "baseline" + ): + """Build FAISS flat baseline using CLIP image embeddings""" + print("πŸ”¨ Building FAISS Flat baseline...") + + from leann_backend_hnsw import faiss + + os.makedirs(output_dir, exist_ok=True) + baseline_path = os.path.join(output_dir, "faiss_flat.index") + metadata_path = os.path.join(output_dir, "metadata.pkl") + + if os.path.exists(baseline_path) and os.path.exists(metadata_path): + print(f"βœ… Baseline already exists at {baseline_path}") + return baseline_path + + # Extract image IDs (must be present) + if not samples or "id" not in samples[0]: + raise KeyError("samples missing 'id' field for FAISS baseline") + image_ids: list[str] = [str(sample["id"]) for sample in samples] + + print(f"πŸ“ Embedding shape: {embeddings.shape}") + print(f"πŸ“„ Processing {len(image_ids)} images") + + # Build FAISS flat index + print("πŸ—οΈ Building FAISS IndexFlatIP...") + dimension = embeddings.shape[1] + index = faiss.IndexFlatIP(dimension) + + # Add embeddings to flat index + embeddings_f32 = embeddings.astype(np.float32) + index.add(embeddings_f32.shape[0], faiss.swig_ptr(embeddings_f32)) + + # Save index and metadata + faiss.write_index(index, baseline_path) + with open(metadata_path, "wb") as f: + pickle.dump(image_ids, f) + + print(f"βœ… FAISS baseline saved to {baseline_path}") + print(f"βœ… Metadata saved to {metadata_path}") + print(f"πŸ“Š Total vectors: {index.ntotal}") + + return baseline_path + + def create_leann_passages(self, samples: list[dict]): + """Create LEANN-compatible passages from LAION data""" + print("πŸ“ Creating LEANN passages...") + + passages_file = self.data_dir / "laion_passages.jsonl" + + with open(passages_file, "w", encoding="utf-8") as f: + for i, sample in enumerate(samples): + passage = { + "id": sample["id"], + "text": sample["caption"], # Use caption as searchable text + "metadata": { + "image_url": sample["url"], + "image_path": sample.get("image_path", ""), + "width": sample["width"], + "height": sample["height"], + "similarity": sample["similarity"], + "image_index": i, # Index for embedding lookup + }, + } + f.write(json.dumps(passage) + "\n") + + print(f"βœ… Created {len(samples)} passages") + return passages_file + + def build_compact_index( + self, passages_file: Path, embeddings: np.ndarray, index_path: str, backend: str = "hnsw" + ): + """Build compact LEANN index with CLIP embeddings (recompute=True, compact=True)""" + print(f"πŸ—οΈ Building compact LEANN index with {backend} backend...") + + start_time = time.time() + + # Save CLIP embeddings (npy) and also a pickle with (ids, embeddings) + npy_path = self.data_dir / "clip_image_embeddings.npy" + np.save(npy_path, embeddings) + print(f"πŸ’Ύ Saved CLIP embeddings to {npy_path}") + + # Prepare ids in the same order as passages_file (matches embeddings order) + ids: list[str] = [] + with open(passages_file, encoding="utf-8") as f: + for line in f: + if line.strip(): + rec = json.loads(line) + ids.append(str(rec["id"])) + + if len(ids) != embeddings.shape[0]: + raise ValueError( + f"IDs count ({len(ids)}) does not match embeddings ({embeddings.shape[0]})." + ) + + pkl_path = self.data_dir / "clip_image_embeddings.pkl" + with open(pkl_path, "wb") as pf: + pickle.dump((ids, embeddings.astype(np.float32)), pf) + print(f"πŸ’Ύ Saved (ids, embeddings) pickle to {pkl_path}") + + # Initialize builder - compact with recompute + # Note: For multimodal case, we need to handle embeddings differently + # Let's try using sentence-transformers mode but with custom embeddings + builder = LeannBuilder( + backend_name=backend, + # Use CLIP text encoder (ViT-L/14) to match image space (768-dim) + embedding_model="clip-ViT-L-14", + embedding_mode="sentence-transformers", + # HNSW params (or forwarded to chosen backend) + graph_degree=32, + complexity=64, + # Compact/pruned with recompute at query time + is_recompute=True, + is_compact=True, + distance_metric="cosine", # CLIP uses normalized vectors; cosine is appropriate + num_threads=4, + ) + + # Add passages (text + metadata) + print("πŸ“š Adding passages...") + self._add_passages_with_embeddings(builder, passages_file, embeddings) + + print(f"πŸ”¨ Building compact index at {index_path} from precomputed embeddings...") + builder.build_index_from_embeddings(index_path, str(pkl_path)) + + build_time = time.time() - start_time + print(f"βœ… Compact index built in {build_time:.2f}s") + + # Analyze index size + self._analyze_index_size(index_path) + + return index_path + + def build_non_compact_index( + self, passages_file: Path, embeddings: np.ndarray, index_path: str, backend: str = "hnsw" + ): + """Build non-compact LEANN index with CLIP embeddings (recompute=False, compact=False)""" + print(f"πŸ—οΈ Building non-compact LEANN index with {backend} backend...") + + start_time = time.time() + + # Ensure embeddings are saved (npy + pickle) + npy_path = self.data_dir / "clip_image_embeddings.npy" + if not npy_path.exists(): + np.save(npy_path, embeddings) + print(f"πŸ’Ύ Saved CLIP embeddings to {npy_path}") + # Prepare ids in same order as passages_file + ids: list[str] = [] + with open(passages_file, encoding="utf-8") as f: + for line in f: + if line.strip(): + rec = json.loads(line) + ids.append(str(rec["id"])) + if len(ids) != embeddings.shape[0]: + raise ValueError( + f"IDs count ({len(ids)}) does not match embeddings ({embeddings.shape[0]})." + ) + pkl_path = self.data_dir / "clip_image_embeddings.pkl" + if not pkl_path.exists(): + with open(pkl_path, "wb") as pf: + pickle.dump((ids, embeddings.astype(np.float32)), pf) + print(f"πŸ’Ύ Saved (ids, embeddings) pickle to {pkl_path}") + + # Initialize builder - non-compact without recompute + builder = LeannBuilder( + backend_name=backend, + embedding_model="clip-ViT-L-14", + embedding_mode="sentence-transformers", + graph_degree=32, + complexity=64, + is_recompute=False, # Store embeddings (no recompute needed) + is_compact=False, # Store full index (not pruned) + distance_metric="cosine", + num_threads=4, + ) + + # Add passages - embeddings will be loaded from file + print("πŸ“š Adding passages...") + self._add_passages_with_embeddings(builder, passages_file, embeddings) + + print(f"πŸ”¨ Building non-compact index at {index_path} from precomputed embeddings...") + builder.build_index_from_embeddings(index_path, str(pkl_path)) + + build_time = time.time() - start_time + print(f"βœ… Non-compact index built in {build_time:.2f}s") + + # Analyze index size + self._analyze_index_size(index_path) + + return index_path + + def _add_passages_with_embeddings(self, builder, passages_file: Path, embeddings: np.ndarray): + """Helper to add passages with pre-computed CLIP embeddings""" + with open(passages_file, encoding="utf-8") as f: + for line in tqdm(f, desc="Adding passages"): + if line.strip(): + passage = json.loads(line) + + # Add image metadata - LEANN will handle embeddings separately + # Note: We store image metadata and caption text for searchability + # Important: ensure passage ID in metadata matches vector ID + builder.add_text( + text=passage["text"], # Image caption for searchability + metadata={**passage["metadata"], "id": passage["id"]}, + ) + + def _analyze_index_size(self, index_path: str): + """Analyze index file sizes""" + print("πŸ“ Analyzing index sizes...") + + index_path = Path(index_path) + index_dir = index_path.parent + index_name = index_path.name # e.g., laion_index.leann + index_prefix = index_path.stem # e.g., laion_index + + files = [ + (f"{index_prefix}.index", ".index", "core"), + (f"{index_name}.meta.json", ".meta.json", "core"), + (f"{index_name}.ids.txt", ".ids.txt", "core"), + (f"{index_name}.passages.jsonl", ".passages.jsonl", "passages"), + (f"{index_name}.passages.idx", ".passages.idx", "passages"), + ] + + def _fmt_size(bytes_val: int) -> str: + if bytes_val < 1024: + return f"{bytes_val} B" + kb = bytes_val / 1024 + if kb < 1024: + return f"{kb:.1f} KB" + mb = kb / 1024 + if mb < 1024: + return f"{mb:.2f} MB" + gb = mb / 1024 + return f"{gb:.2f} GB" + + total_index_only_mb = 0.0 + total_all_mb = 0.0 + for filename, label, group in files: + file_path = index_dir / filename + if file_path.exists(): + size_bytes = file_path.stat().st_size + print(f" {label}: {_fmt_size(size_bytes)}") + size_mb = size_bytes / (1024 * 1024) + total_all_mb += size_mb + if group == "core": + total_index_only_mb += size_mb + else: + print(f" {label}: (missing)") + print(f" Total (index only, exclude passages): {total_index_only_mb:.2f} MB") + print(f" Total (including passages): {total_all_mb:.2f} MB") + + def create_evaluation_queries(self, samples: list[dict], num_queries: int = 200): + """Create evaluation queries from captions""" + print(f"πŸ“ Creating {num_queries} evaluation queries...") + + # Sample random captions as queries + import random + + random.seed(42) # For reproducibility + + query_samples = random.sample(samples, min(num_queries, len(samples))) + + queries_file = self.data_dir / "evaluation_queries.jsonl" + with open(queries_file, "w", encoding="utf-8") as f: + for sample in query_samples: + query = { + "id": sample["id"], + "query": sample["caption"], + "ground_truth_id": sample["id"], # For potential recall evaluation + } + f.write(json.dumps(query) + "\n") + + print(f"βœ… Created {len(query_samples)} evaluation queries") + return queries_file + + +def main(): + parser = argparse.ArgumentParser(description="Setup LAION Multimodal Benchmark") + parser.add_argument("--data-dir", default="data", help="Data directory") + parser.add_argument("--num-samples", type=int, default=1000, help="Number of LAION samples") + parser.add_argument("--num-queries", type=int, default=50, help="Number of evaluation queries") + parser.add_argument("--index-path", default="data/laion_index.leann", help="Output index path") + parser.add_argument( + "--backend", default="hnsw", choices=["hnsw", "diskann"], help="LEANN backend" + ) + parser.add_argument("--skip-download", action="store_true", help="Skip LAION dataset download") + parser.add_argument("--skip-build", action="store_true", help="Skip index building") + + args = parser.parse_args() + + print("πŸš€ Setting up LAION Multimodal Benchmark") + print("=" * 50) + + try: + # Initialize setup + setup = LAIONSetup(args.data_dir) + + # Step 1: Download LAION subset + if not args.skip_download: + print("\nπŸ“¦ Step 1: Download LAION subset") + samples = setup.download_laion_subset(args.num_samples) + + # Step 2: Generate CLIP image embeddings + print("\nπŸ” Step 2: Generate CLIP image embeddings") + embeddings, valid_samples = setup.generate_clip_image_embeddings(samples) + + # Step 3: Create LEANN passages (image metadata with embeddings) + print("\nπŸ“ Step 3: Create LEANN passages") + passages_file = setup.create_leann_passages(valid_samples) + else: + print("⏭️ Skipping LAION dataset download") + # Load existing data + passages_file = setup.data_dir / "laion_passages.jsonl" + embeddings_file = setup.data_dir / "clip_image_embeddings.npy" + + if not passages_file.exists() or not embeddings_file.exists(): + raise FileNotFoundError( + "Passages or embeddings file not found. Run without --skip-download first." + ) + + embeddings = np.load(embeddings_file) + print(f"πŸ“Š Loaded {len(embeddings)} embeddings from {embeddings_file}") + + # Step 4: Build LEANN indexes (both compact and non-compact) + if not args.skip_build: + print("\nπŸ—οΈ Step 4: Build LEANN indexes with CLIP image embeddings") + + # Build compact index (production mode - small, recompute required) + compact_index_path = args.index_path + print(f"Building compact index: {compact_index_path}") + setup.build_compact_index(passages_file, embeddings, compact_index_path, args.backend) + + # Build non-compact index (comparison mode - large, fast search) + non_compact_index_path = args.index_path.replace(".leann", "_noncompact.leann") + print(f"Building non-compact index: {non_compact_index_path}") + setup.build_non_compact_index( + passages_file, embeddings, non_compact_index_path, args.backend + ) + + # Step 5: Build FAISS flat baseline + print("\nπŸ”¨ Step 5: Build FAISS flat baseline") + if not args.skip_download: + baseline_path = setup.build_faiss_baseline(embeddings, valid_samples) + else: + # Load valid_samples from passages file for FAISS baseline + valid_samples = [] + with open(passages_file, encoding="utf-8") as f: + for line in f: + if line.strip(): + passage = json.loads(line) + valid_samples.append({"id": passage["id"], "caption": passage["text"]}) + baseline_path = setup.build_faiss_baseline(embeddings, valid_samples) + + # Step 6: Create evaluation queries + print("\nπŸ“ Step 6: Create evaluation queries") + queries_file = setup.create_evaluation_queries(valid_samples, args.num_queries) + else: + print("⏭️ Skipping index building") + baseline_path = "data/baseline/faiss_index.bin" + queries_file = setup.data_dir / "evaluation_queries.jsonl" + + print("\nπŸŽ‰ Setup completed successfully!") + print("πŸ“Š Summary:") + if not args.skip_download: + print(f" Downloaded samples: {len(samples)}") + print(f" Valid samples with embeddings: {len(valid_samples)}") + else: + print(f" Loaded {len(embeddings)} embeddings") + + if not args.skip_build: + print(f" Compact index: {compact_index_path}") + print(f" Non-compact index: {non_compact_index_path}") + print(f" FAISS baseline: {baseline_path}") + print(f" Queries: {queries_file}") + + print("\nπŸ”§ Next steps:") + print(f" Run evaluation: python evaluate_laion.py --index {compact_index_path}") + print(f" Or compare with: python evaluate_laion.py --index {non_compact_index_path}") + else: + print(" Skipped building indexes") + + except KeyboardInterrupt: + print("\n⚠️ Setup interrupted by user") + exit(1) + except Exception as e: + print(f"\n❌ Setup failed: {e}") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/llm_utils.py b/benchmarks/llm_utils.py new file mode 100644 index 0000000..f64c55b --- /dev/null +++ b/benchmarks/llm_utils.py @@ -0,0 +1,342 @@ +""" +LLM utils for RAG benchmarks with Qwen3-8B and Qwen2.5-VL (multimodal) +""" + +import time + +try: + import torch + from transformers import AutoModelForCausalLM, AutoTokenizer + + HF_AVAILABLE = True +except ImportError: + HF_AVAILABLE = False + +try: + from vllm import LLM, SamplingParams + + VLLM_AVAILABLE = True +except ImportError: + VLLM_AVAILABLE = False + + +def is_qwen3_model(model_name): + """Check if model is Qwen3""" + return "Qwen3" in model_name or "qwen3" in model_name.lower() + + +def is_qwen_vl_model(model_name): + """Check if model is Qwen2.5-VL""" + return "Qwen2.5-VL" in model_name or "qwen2.5-vl" in model_name.lower() + + +def apply_qwen3_chat_template(tokenizer, prompt): + """Apply Qwen3 chat template with thinking enabled""" + messages = [{"role": "user", "content": prompt}] + return tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True, + enable_thinking=True, + ) + + +def extract_thinking_answer(response): + """Extract final answer from Qwen3 thinking model response""" + if "" in response and "" in response: + try: + think_end = response.index("") + len("") + final_answer = response[think_end:].strip() + return final_answer + except (ValueError, IndexError): + pass + + return response.strip() + + +def load_hf_model(model_name="Qwen/Qwen3-8B", trust_remote_code=False): + """Load HuggingFace model + + Args: + model_name (str): Name of the model to load + trust_remote_code (bool): Whether to allow execution of code from the model repository. + Defaults to False for security. Only enable for trusted models. + """ + if not HF_AVAILABLE: + raise ImportError("transformers not available") + + if trust_remote_code: + print( + "⚠️ WARNING: Loading model with trust_remote_code=True. This can execute arbitrary code." + ) + + print(f"Loading HF: {model_name}") + tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=trust_remote_code) + model = AutoModelForCausalLM.from_pretrained( + model_name, + torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32, + device_map="auto", + trust_remote_code=trust_remote_code, + ) + return tokenizer, model + + +def load_vllm_model(model_name="Qwen/Qwen3-8B", trust_remote_code=False): + """Load vLLM model + + Args: + model_name (str): Name of the model to load + trust_remote_code (bool): Whether to allow execution of code from the model repository. + Defaults to False for security. Only enable for trusted models. + """ + if not VLLM_AVAILABLE: + raise ImportError("vllm not available") + + if trust_remote_code: + print( + "⚠️ WARNING: Loading model with trust_remote_code=True. This can execute arbitrary code." + ) + + print(f"Loading vLLM: {model_name}") + llm = LLM(model=model_name, trust_remote_code=trust_remote_code) + + # Qwen3 specific config + if is_qwen3_model(model_name): + stop_tokens = ["<|im_end|>", "<|end_of_text|>"] + max_tokens = 2048 + else: + stop_tokens = None + max_tokens = 1024 + + sampling_params = SamplingParams(temperature=0.7, max_tokens=max_tokens, stop=stop_tokens) + return llm, sampling_params + + +def generate_hf(tokenizer, model, prompt, max_tokens=None): + """Generate with HF - supports Qwen3 thinking models""" + model_name = getattr(model, "name_or_path", "unknown") + is_qwen3 = is_qwen3_model(model_name) + + # Apply chat template for Qwen3 + if is_qwen3: + prompt = apply_qwen3_chat_template(tokenizer, prompt) + max_tokens = max_tokens or 2048 + else: + max_tokens = max_tokens or 1024 + + inputs = tokenizer(prompt, return_tensors="pt").to(model.device) + with torch.no_grad(): + outputs = model.generate( + **inputs, + max_new_tokens=max_tokens, + temperature=0.7, + do_sample=True, + pad_token_id=tokenizer.eos_token_id, + ) + response = tokenizer.decode(outputs[0], skip_special_tokens=True) + response = response[len(prompt) :].strip() + + # Extract final answer for thinking models + if is_qwen3: + return extract_thinking_answer(response) + return response + + +def generate_vllm(llm, sampling_params, prompt): + """Generate with vLLM - supports Qwen3 thinking models""" + outputs = llm.generate([prompt], sampling_params) + response = outputs[0].outputs[0].text.strip() + + # Extract final answer for Qwen3 thinking models + model_name = str(llm.llm_engine.model_config.model) + if is_qwen3_model(model_name): + return extract_thinking_answer(response) + return response + + +def create_prompt(context, query, domain="default"): + """Create RAG prompt""" + if domain == "emails": + return f"Email content:\n{context}\n\nQuestion: {query}\n\nAnswer:" + elif domain == "finance": + return f"Financial content:\n{context}\n\nQuestion: {query}\n\nAnswer:" + elif domain == "multimodal": + return f"Image context:\n{context}\n\nQuestion: {query}\n\nAnswer:" + else: + return f"Context: {context}\n\nQuestion: {query}\n\nAnswer:" + + +def evaluate_rag(searcher, llm_func, queries, domain="default", top_k=3, complexity=64): + """Simple RAG evaluation with timing""" + search_times = [] + gen_times = [] + results = [] + + for i, query in enumerate(queries): + # Search + start = time.time() + docs = searcher.search(query, top_k=top_k, complexity=complexity) + search_time = time.time() - start + + # Generate + context = "\n\n".join([doc.text for doc in docs]) + prompt = create_prompt(context, query, domain) + + start = time.time() + response = llm_func(prompt) + gen_time = time.time() - start + + search_times.append(search_time) + gen_times.append(gen_time) + results.append(response) + + if i < 3: + print(f"Q{i + 1}: Search={search_time:.3f}s, Gen={gen_time:.3f}s") + + return { + "avg_search_time": sum(search_times) / len(search_times), + "avg_generation_time": sum(gen_times) / len(gen_times), + "results": results, + } + + +def load_qwen_vl_model(model_name="Qwen/Qwen2.5-VL-7B-Instruct", trust_remote_code=False): + """Load Qwen2.5-VL multimodal model + + Args: + model_name (str): Name of the model to load + trust_remote_code (bool): Whether to allow execution of code from the model repository. + Defaults to False for security. Only enable for trusted models. + """ + if not HF_AVAILABLE: + raise ImportError("transformers not available") + + if trust_remote_code: + print( + "⚠️ WARNING: Loading model with trust_remote_code=True. This can execute arbitrary code." + ) + + print(f"Loading Qwen2.5-VL: {model_name}") + + try: + from transformers import AutoModelForVision2Seq, AutoProcessor + + processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=trust_remote_code) + model = AutoModelForVision2Seq.from_pretrained( + model_name, + torch_dtype=torch.bfloat16, + device_map="auto", + trust_remote_code=trust_remote_code, + ) + + return processor, model + + except Exception as e: + print(f"Failed to load with AutoModelForVision2Seq, trying specific class: {e}") + + # Fallback to specific class + try: + from transformers import AutoProcessor, Qwen2VLForConditionalGeneration + + processor = AutoProcessor.from_pretrained( + model_name, trust_remote_code=trust_remote_code + ) + model = Qwen2VLForConditionalGeneration.from_pretrained( + model_name, + torch_dtype=torch.bfloat16, + device_map="auto", + trust_remote_code=trust_remote_code, + ) + + return processor, model + + except Exception as e2: + raise ImportError(f"Failed to load Qwen2.5-VL model: {e2}") + + +def generate_qwen_vl(processor, model, prompt, image_path=None, max_tokens=512): + """Generate with Qwen2.5-VL multimodal model""" + from PIL import Image + + # Prepare inputs + if image_path: + image = Image.open(image_path) + inputs = processor(text=prompt, images=image, return_tensors="pt").to(model.device) + else: + inputs = processor(text=prompt, return_tensors="pt").to(model.device) + + # Generate + with torch.no_grad(): + generated_ids = model.generate( + **inputs, max_new_tokens=max_tokens, do_sample=False, temperature=0.1 + ) + + # Decode response + generated_ids = generated_ids[:, inputs["input_ids"].shape[1] :] + response = processor.decode(generated_ids[0], skip_special_tokens=True) + + return response + + +def create_multimodal_prompt(context, query, image_descriptions, task_type="images"): + """Create prompt for multimodal RAG""" + if task_type == "images": + return f"""Based on the retrieved images and their descriptions, answer the following question. + +Retrieved Image Descriptions: +{context} + +Question: {query} + +Provide a detailed answer based on the visual content described above.""" + + return f"Context: {context}\nQuestion: {query}\nAnswer:" + + +def evaluate_multimodal_rag(searcher, queries, processor=None, model=None, complexity=64): + """Evaluate multimodal RAG with Qwen2.5-VL""" + search_times = [] + gen_times = [] + results = [] + + for i, query_item in enumerate(queries): + # Handle both string and dict formats for queries + if isinstance(query_item, dict): + query = query_item.get("query", "") + image_path = query_item.get("image_path") # Optional reference image + else: + query = str(query_item) + image_path = None + + # Search + start_time = time.time() + search_results = searcher.search(query, top_k=3, complexity=complexity) + search_time = time.time() - start_time + search_times.append(search_time) + + # Prepare context from search results + context_parts = [] + for result in search_results: + context_parts.append(f"- {result.text}") + context = "\n".join(context_parts) + + # Generate with multimodal model + start_time = time.time() + if processor and model: + prompt = create_multimodal_prompt(context, query, context_parts) + response = generate_qwen_vl(processor, model, prompt, image_path) + else: + response = f"Context: {context}" + gen_time = time.time() - start_time + + gen_times.append(gen_time) + results.append(response) + + if i < 3: + print(f"Q{i + 1}: Search={search_time:.3f}s, Gen={gen_time:.3f}s") + + return { + "avg_search_time": sum(search_times) / len(search_times), + "avg_generation_time": sum(gen_times) / len(gen_times), + "results": results, + } diff --git a/benchmarks/micro_tpt.py b/benchmarks/micro_tpt.py new file mode 100644 index 0000000..37e8b4b --- /dev/null +++ b/benchmarks/micro_tpt.py @@ -0,0 +1,659 @@ +# python embedd_micro.py --use_int8 Fastest + +import argparse +import time +from contextlib import contextmanager +from dataclasses import dataclass + +import numpy as np +import torch +from torch import nn +from tqdm import tqdm +from transformers import AutoModel, BitsAndBytesConfig + + +@dataclass +class BenchmarkConfig: + model_path: str + batch_sizes: list[int] + seq_length: int + num_runs: int + use_fp16: bool = True + use_int4: bool = False + use_int8: bool = False # Add this parameter + use_cuda_graphs: bool = False + use_flash_attention: bool = False + use_linear8bitlt: bool = False + + +class GraphContainer: + """Container for managing graphs for different batch sizes (CUDA graphs on NVIDIA, regular on others).""" + + def __init__(self, model: nn.Module, seq_length: int): + self.model = model + self.seq_length = seq_length + self.graphs: dict[int, GraphWrapper] = {} + + def get_or_create(self, batch_size: int) -> "GraphWrapper": + if batch_size not in self.graphs: + self.graphs[batch_size] = GraphWrapper(self.model, batch_size, self.seq_length) + return self.graphs[batch_size] + + +class GraphWrapper: + """Wrapper for graph capture and replay (CUDA graphs on NVIDIA, regular on others).""" + + def __init__(self, model: nn.Module, batch_size: int, seq_length: int): + self.model = model + self.device = self._get_device() + self.static_input = self._create_random_batch(batch_size, seq_length) + self.static_attention_mask = torch.ones_like(self.static_input) + + # Warm up + self._warmup() + + # Only use CUDA graphs on NVIDIA GPUs + if torch.cuda.is_available() and hasattr(torch.cuda, "CUDAGraph"): + # Capture graph + self.graph = torch.cuda.CUDAGraph() + with torch.cuda.graph(self.graph): + self.static_output = self.model( + input_ids=self.static_input, + attention_mask=self.static_attention_mask, + ) + self.use_cuda_graph = True + else: + # For MPS or CPU, just store the model + self.use_cuda_graph = False + self.static_output = None + + def _get_device(self) -> str: + if torch.cuda.is_available(): + return "cuda" + elif torch.backends.mps.is_available(): + return "mps" + else: + return "cpu" + + def _create_random_batch(self, batch_size: int, seq_length: int) -> torch.Tensor: + return torch.randint( + 0, 1000, (batch_size, seq_length), device=self.device, dtype=torch.long + ) + + def _warmup(self, num_warmup: int = 3): + with torch.no_grad(): + for _ in range(num_warmup): + self.model( + input_ids=self.static_input, + attention_mask=self.static_attention_mask, + ) + + def __call__(self, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor: + if self.use_cuda_graph: + self.static_input.copy_(input_ids) + self.static_attention_mask.copy_(attention_mask) + self.graph.replay() + return self.static_output + else: + # For MPS/CPU, just run normally + return self.model(input_ids=input_ids, attention_mask=attention_mask) + + +class ModelOptimizer: + """Applies various optimizations to the model.""" + + @staticmethod + def optimize(model: nn.Module, config: BenchmarkConfig) -> nn.Module: + print("\nApplying model optimizations:") + + if model is None: + raise ValueError("Cannot optimize None model") + + # Move to GPU + if torch.cuda.is_available(): + model = model.cuda() + device = "cuda" + elif torch.backends.mps.is_available(): + model = model.to("mps") + device = "mps" + else: + model = model.cpu() + device = "cpu" + print(f"- Model moved to {device}") + + # FP16 + if config.use_fp16 and not config.use_int4: + model = model.half() + # use torch compile + model = torch.compile(model) + print("- Using FP16 precision") + + # Check if using SDPA (only on CUDA) + if ( + torch.cuda.is_available() + and torch.version.cuda + and float(torch.version.cuda[:3]) >= 11.6 + ): + if hasattr(torch.nn.functional, "scaled_dot_product_attention"): + print("- Using PyTorch SDPA (scaled_dot_product_attention)") + else: + print("- PyTorch SDPA not available") + + # Flash Attention (only on CUDA) + if config.use_flash_attention and torch.cuda.is_available(): + try: + from flash_attn.flash_attention import FlashAttention # noqa: F401 + + print("- Flash Attention 2 available") + if hasattr(model.config, "attention_mode"): + model.config.attention_mode = "flash_attention_2" + print(" - Enabled Flash Attention 2 mode") + except ImportError: + print("- Flash Attention not available") + + # Memory efficient attention (only on CUDA) + if torch.cuda.is_available(): + try: + from xformers.ops import memory_efficient_attention # noqa: F401 + + if hasattr(model, "enable_xformers_memory_efficient_attention"): + model.enable_xformers_memory_efficient_attention() + print("- Enabled xformers memory efficient attention") + else: + print("- Model doesn't support xformers") + except (ImportError, AttributeError): + print("- Xformers not available") + + model.eval() + print("- Model set to eval mode") + + return model + + +class Timer: + """Handles accurate GPU timing using GPU events or CPU timing.""" + + def __init__(self): + if torch.cuda.is_available(): + self.start_event = torch.cuda.Event(enable_timing=True) + self.end_event = torch.cuda.Event(enable_timing=True) + self.use_gpu_timing = True + elif torch.backends.mps.is_available(): + # MPS doesn't have events, use CPU timing + self.use_gpu_timing = False + else: + # CPU timing + self.use_gpu_timing = False + + @contextmanager + def timing(self): + if self.use_gpu_timing: + self.start_event.record() + yield + self.end_event.record() + self.end_event.synchronize() + else: + # Use CPU timing for MPS/CPU + start_time = time.time() + yield + self.cpu_elapsed = time.time() - start_time + + def elapsed_time(self) -> float: + if self.use_gpu_timing: + return self.start_event.elapsed_time(self.end_event) / 1000 # ms to seconds + else: + return self.cpu_elapsed + + +class Benchmark: + """Main benchmark runner.""" + + def __init__(self, config: BenchmarkConfig): + self.config = config + try: + self.model = self._load_model() + if self.model is None: + raise ValueError("Model initialization failed - model is None") + + # Only use CUDA graphs on NVIDIA GPUs + if config.use_cuda_graphs and torch.cuda.is_available(): + self.graphs = GraphContainer(self.model, config.seq_length) + else: + self.graphs = None + self.timer = Timer() + except Exception as e: + print(f"ERROR in benchmark initialization: {e!s}") + raise + + def _load_model(self) -> nn.Module: + print(f"Loading model from {self.config.model_path}...") + + try: + # Int4 quantization using HuggingFace integration + if self.config.use_int4: + import bitsandbytes as bnb + + print(f"- bitsandbytes version: {bnb.__version__}") + + # Check if using custom 8bit quantization + if hasattr(self.config, "use_linear8bitlt") and self.config.use_linear8bitlt: + print("- Using custom Linear8bitLt replacement for all linear layers") + + # Load original model (without quantization config) + import bitsandbytes as bnb + import torch + + # set default to half + torch.set_default_dtype(torch.float16) + compute_dtype = torch.float16 if self.config.use_fp16 else torch.float32 + model = AutoModel.from_pretrained( + self.config.model_path, + torch_dtype=compute_dtype, + ) + + # Define replacement function + def replace_linear_with_linear8bitlt(model): + """Recursively replace all nn.Linear layers with Linear8bitLt""" + for name, module in list(model.named_children()): + if isinstance(module, nn.Linear): + # Get original linear layer parameters + in_features = module.in_features + out_features = module.out_features + bias = module.bias is not None + + # Create 8bit linear layer + # print size + print(f"in_features: {in_features}, out_features: {out_features}") + new_module = bnb.nn.Linear8bitLt( + in_features, + out_features, + bias=bias, + has_fp16_weights=False, + ) + + # Copy weights and bias + new_module.weight.data = module.weight.data + if bias: + new_module.bias.data = module.bias.data + + # Replace module + setattr(model, name, new_module) + else: + # Process child modules recursively + replace_linear_with_linear8bitlt(module) + + return model + + # Replace all linear layers + model = replace_linear_with_linear8bitlt(model) + # add torch compile + model = torch.compile(model) + + # Move model to GPU (quantization happens here) + device = ( + "cuda" + if torch.cuda.is_available() + else "mps" + if torch.backends.mps.is_available() + else "cpu" + ) + model = model.to(device) + + print("- All linear layers replaced with Linear8bitLt") + + else: + # Use original Int4 quantization method + print("- Using bitsandbytes for Int4 quantization") + + # Create quantization config + + compute_dtype = torch.float16 if self.config.use_fp16 else torch.float32 + quantization_config = BitsAndBytesConfig( + load_in_4bit=True, + bnb_4bit_compute_dtype=compute_dtype, + bnb_4bit_use_double_quant=True, + bnb_4bit_quant_type="nf4", + ) + + print("- Quantization config:", quantization_config) + + # Load model directly with quantization config + model = AutoModel.from_pretrained( + self.config.model_path, + quantization_config=quantization_config, + torch_dtype=compute_dtype, + device_map="auto", # Let HF decide on device mapping + ) + + # Check if model loaded successfully + if model is None: + raise ValueError("Model loading returned None") + + print(f"- Model type: {type(model)}") + + # Apply optimizations directly here + print("\nApplying model optimizations:") + + if hasattr(self.config, "use_linear8bitlt") and self.config.use_linear8bitlt: + print("- Model moved to GPU with Linear8bitLt quantization") + else: + # Skip moving to GPU since device_map="auto" already did that + print("- Model already on GPU due to device_map='auto'") + + # Skip FP16 conversion since we specified compute_dtype + print(f"- Using {compute_dtype} for compute dtype") + + # Check CUDA and SDPA + if ( + torch.cuda.is_available() + and torch.version.cuda + and float(torch.version.cuda[:3]) >= 11.6 + ): + if hasattr(torch.nn.functional, "scaled_dot_product_attention"): + print("- Using PyTorch SDPA (scaled_dot_product_attention)") + else: + print("- PyTorch SDPA not available") + + # Try xformers if available (only on CUDA) + if torch.cuda.is_available(): + try: + if hasattr(model, "enable_xformers_memory_efficient_attention"): + model.enable_xformers_memory_efficient_attention() + print("- Enabled xformers memory efficient attention") + else: + print("- Model doesn't support xformers") + except (ImportError, AttributeError): + print("- Xformers not available") + + # Set to eval mode + model.eval() + print("- Model set to eval mode") + # Int8 quantization using HuggingFace integration + elif self.config.use_int8: + print("- Using INT8 quantization") + # For now, just use standard loading with INT8 config + compute_dtype = torch.float16 if self.config.use_fp16 else torch.float32 + quantization_config = BitsAndBytesConfig( + load_in_8bit=True, + llm_int8_threshold=6.0, + llm_int8_has_fp16_weight=False, + ) + + model = AutoModel.from_pretrained( + self.config.model_path, + quantization_config=quantization_config, + torch_dtype=compute_dtype, + device_map="auto", + ) + + if model is None: + raise ValueError("Model loading returned None") + + print(f"- Model type: {type(model)}") + model.eval() + print("- Model set to eval mode") + + else: + # Standard loading for FP16/FP32 + model = AutoModel.from_pretrained(self.config.model_path) + print("- Model loaded in standard precision") + print(f"- Model type: {type(model)}") + + # Apply standard optimizations + # set default to half + import torch + + torch.set_default_dtype(torch.bfloat16) + model = ModelOptimizer.optimize(model, self.config) + model = model.half() + # add torch compile + model = torch.compile(model) + + # Final check to ensure model is not None + if model is None: + raise ValueError("Model is None after optimization") + + print(f"- Final model type: {type(model)}") + return model + + except Exception as e: + print(f"ERROR loading model: {e!s}") + import traceback + + traceback.print_exc() + raise + + def _create_random_batch(self, batch_size: int) -> torch.Tensor: + device = ( + "cuda" + if torch.cuda.is_available() + else "mps" + if torch.backends.mps.is_available() + else "cpu" + ) + return torch.randint( + 0, + 1000, + (batch_size, self.config.seq_length), + device=device, + dtype=torch.long, + ) + + def _run_inference( + self, input_ids: torch.Tensor, graph_wrapper: GraphWrapper | None = None + ) -> tuple[float, torch.Tensor]: + attention_mask = torch.ones_like(input_ids) + + with torch.no_grad(), self.timer.timing(): + if graph_wrapper is not None: + output = graph_wrapper(input_ids, attention_mask) + else: + output = self.model(input_ids=input_ids, attention_mask=attention_mask) + + return self.timer.elapsed_time(), output + + def run(self) -> dict[int, dict[str, float]]: + results = {} + + # Reset peak memory stats + if torch.cuda.is_available(): + torch.cuda.reset_peak_memory_stats() + elif torch.backends.mps.is_available(): + # MPS doesn't have reset_peak_memory_stats, skip it + pass + else: + print("- No GPU memory stats available") + + for batch_size in self.config.batch_sizes: + print(f"\nTesting batch size: {batch_size}") + times = [] + + # Get or create graph for this batch size + graph_wrapper = ( + self.graphs.get_or_create(batch_size) if self.graphs is not None else None + ) + + # Pre-allocate input tensor + input_ids = self._create_random_batch(batch_size) + print(f"Input shape: {input_ids.shape}") + + # Run benchmark + for i in tqdm(range(self.config.num_runs), desc=f"Batch size {batch_size}"): + try: + elapsed_time, output = self._run_inference(input_ids, graph_wrapper) + if i == 0: # Only print on first run + print(f"Output shape: {output.last_hidden_state.shape}") + times.append(elapsed_time) + except Exception as e: + print(f"Error during inference: {e}") + break + + if not times: + print(f"No successful runs for batch size {batch_size}, skipping") + continue + + # Calculate statistics + avg_time = np.mean(times) + std_time = np.std(times) + throughput = batch_size / avg_time + + results[batch_size] = { + "avg_time": avg_time, + "std_time": std_time, + "throughput": throughput, + } + + print(f"Avg Time: {avg_time:.4f}s Β± {std_time:.4f}s") + print(f"Throughput: {throughput:.2f} sequences/second") + + # Log memory usage + if torch.cuda.is_available(): + peak_memory_gb = torch.cuda.max_memory_allocated() / (1024**3) + elif torch.backends.mps.is_available(): + # MPS doesn't have max_memory_allocated, use 0 + peak_memory_gb = 0.0 + else: + peak_memory_gb = 0.0 + print("- No GPU memory usage available") + + if peak_memory_gb > 0: + print(f"\nPeak GPU memory usage: {peak_memory_gb:.2f} GB") + else: + print("\n- GPU memory usage not available") + + # Add memory info to results + for batch_size in results: + results[batch_size]["peak_memory_gb"] = peak_memory_gb + + return results + + +def main(): + parser = argparse.ArgumentParser(description="Model Inference Benchmark") + parser.add_argument( + "--model_path", + type=str, + default="facebook/contriever", + help="Path to the model", + ) + parser.add_argument( + "--batch_sizes", + type=str, + default="1,2,4,8,16,32", + help="Comma-separated list of batch sizes", + ) + parser.add_argument( + "--seq_length", + type=int, + default=256, + help="Sequence length for input", + ) + parser.add_argument( + "--num_runs", + type=int, + default=5, + help="Number of runs for each batch size", + ) + parser.add_argument( + "--use_fp16", + action="store_true", + help="Enable FP16 inference", + ) + parser.add_argument( + "--use_int4", + action="store_true", + help="Enable INT4 quantization using bitsandbytes", + ) + parser.add_argument( + "--use_int8", + action="store_true", + help="Enable INT8 quantization for both activations and weights using bitsandbytes", + ) + parser.add_argument( + "--use_cuda_graphs", + action="store_true", + help="Enable CUDA Graphs optimization (only on NVIDIA GPUs)", + ) + parser.add_argument( + "--use_flash_attention", + action="store_true", + help="Enable Flash Attention 2 if available (only on NVIDIA GPUs)", + ) + parser.add_argument( + "--use_linear8bitlt", + action="store_true", + help="Enable Linear8bitLt quantization for all linear layers", + ) + + args = parser.parse_args() + + # Print arguments for debugging + print("\nCommand line arguments:") + for arg, value in vars(args).items(): + print(f"- {arg}: {value}") + + config = BenchmarkConfig( + model_path=args.model_path, + batch_sizes=[int(bs) for bs in args.batch_sizes.split(",")], + seq_length=args.seq_length, + num_runs=args.num_runs, + use_fp16=args.use_fp16, + use_int4=args.use_int4, + use_int8=args.use_int8, # Add this line + use_cuda_graphs=args.use_cuda_graphs, + use_flash_attention=args.use_flash_attention, + use_linear8bitlt=args.use_linear8bitlt, + ) + + # Print configuration for debugging + print("\nBenchmark configuration:") + for field, value in vars(config).items(): + print(f"- {field}: {value}") + + try: + benchmark = Benchmark(config) + results = benchmark.run() + + # Save results to file + import json + import os + + # Create results directory if it doesn't exist + os.makedirs("results", exist_ok=True) + + # Generate filename based on configuration + precision_type = ( + "int4" + if config.use_int4 + else "int8" + if config.use_int8 + else "fp16" + if config.use_fp16 + else "fp32" + ) + model_name = os.path.basename(config.model_path) + output_file = f"results/benchmark_{model_name}_{precision_type}.json" + + # Save results + with open(output_file, "w") as f: + json.dump( + { + "config": { + k: str(v) if isinstance(v, list) else v for k, v in vars(config).items() + }, + "results": {str(k): v for k, v in results.items()}, + }, + f, + indent=2, + ) + print(f"Results saved to {output_file}") + + except Exception as e: + print(f"Benchmark failed: {e}") + import traceback + + traceback.print_exc() + + +if __name__ == "__main__": + main() diff --git a/benchmarks/run_evaluation.py b/benchmarks/run_evaluation.py new file mode 100644 index 0000000..6c34f15 --- /dev/null +++ b/benchmarks/run_evaluation.py @@ -0,0 +1,393 @@ +#!/usr/bin/env python3 +""" +This script runs a recall evaluation on a given LEANN index. +It correctly compares results by fetching the text content for both the new search +results and the golden standard results, making the comparison robust to ID changes. +""" + +import argparse +import json +import sys +import time +from pathlib import Path + +import numpy as np +from leann.api import LeannBuilder, LeannChat, LeannSearcher + + +def download_data_if_needed(data_root: Path, download_embeddings: bool = False): + """Checks if the data directory exists, and if not, downloads it from HF Hub.""" + if not data_root.exists(): + print(f"Data directory '{data_root}' not found.") + print("Downloading evaluation data from Hugging Face Hub... (this may take a moment)") + try: + from huggingface_hub import snapshot_download + + if download_embeddings: + # Download everything including embeddings (large files) + snapshot_download( + repo_id="LEANN-RAG/leann-rag-evaluation-data", + repo_type="dataset", + local_dir=data_root, + local_dir_use_symlinks=False, + ) + print("Data download complete (including embeddings)!") + else: + # Download only specific folders, excluding embeddings + allow_patterns = [ + "ground_truth/**", + "indices/**", + "queries/**", + "*.md", + "*.txt", + ] + snapshot_download( + repo_id="LEANN-RAG/leann-rag-evaluation-data", + repo_type="dataset", + local_dir=data_root, + local_dir_use_symlinks=False, + allow_patterns=allow_patterns, + ) + print("Data download complete (excluding embeddings)!") + except ImportError: + print( + "Error: huggingface_hub is not installed. Please install it to download the data:" + ) + print("uv sync --only-group dev") + sys.exit(1) + except Exception as e: + print(f"An error occurred during data download: {e}") + sys.exit(1) + + +def download_embeddings_if_needed(data_root: Path, dataset_type: str | None = None): + """Download embeddings files specifically.""" + embeddings_dir = data_root / "embeddings" + + if dataset_type: + # Check if specific dataset embeddings exist + target_file = embeddings_dir / dataset_type / "passages_00.pkl" + if target_file.exists(): + print(f"Embeddings for {dataset_type} already exist") + return str(target_file) + + print("Downloading embeddings from HuggingFace Hub...") + try: + from huggingface_hub import snapshot_download + + # Download only embeddings folder + snapshot_download( + repo_id="LEANN-RAG/leann-rag-evaluation-data", + repo_type="dataset", + local_dir=data_root, + local_dir_use_symlinks=False, + allow_patterns=["embeddings/**/*.pkl"], + ) + print("Embeddings download complete!") + + if dataset_type: + target_file = embeddings_dir / dataset_type / "passages_00.pkl" + if target_file.exists(): + return str(target_file) + + return str(embeddings_dir) + + except Exception as e: + print(f"Error downloading embeddings: {e}") + sys.exit(1) + + +# --- Helper Function to get Golden Passages --- +def get_golden_texts(searcher: LeannSearcher, golden_ids: list[int]) -> set: + """ + Retrieves the text for golden passage IDs directly from the LeannSearcher's + passage manager. + """ + golden_texts = set() + for gid in golden_ids: + try: + # PassageManager uses string IDs + passage_data = searcher.passage_manager.get_passage(str(gid)) + golden_texts.add(passage_data["text"]) + except KeyError: + print(f"Warning: Golden passage ID '{gid}' not found in the index's passage data.") + return golden_texts + + +def load_queries(file_path: Path) -> list[str]: + queries = [] + with open(file_path, encoding="utf-8") as f: + for line in f: + data = json.loads(line) + queries.append(data["query"]) + return queries + + +def build_index_from_embeddings(embeddings_file: str, output_path: str, backend: str = "hnsw"): + """ + Build a LEANN index from pre-computed embeddings. + + Args: + embeddings_file: Path to pickle file with (ids, embeddings) tuple + output_path: Path where to save the index + backend: Backend to use ("hnsw" or "diskann") + """ + print(f"Building {backend} index from embeddings: {embeddings_file}") + + # Create builder with appropriate parameters + if backend == "hnsw": + builder_kwargs = { + "M": 32, # Graph degree + "efConstruction": 256, # Construction complexity + "is_compact": True, # Use compact storage + "is_recompute": True, # Enable pruning for better recall + } + elif backend == "diskann": + builder_kwargs = { + "complexity": 64, + "graph_degree": 32, + "search_memory_maximum": 8.0, # GB + "build_memory_maximum": 16.0, # GB + } + else: + builder_kwargs = {} + + builder = LeannBuilder( + backend_name=backend, + embedding_model="facebook/contriever-msmarco", # Model used to create embeddings + dimensions=768, # Will be auto-detected from embeddings + **builder_kwargs, + ) + + # Build index from precomputed embeddings + builder.build_index_from_embeddings(output_path, embeddings_file) + print(f"Index saved to: {output_path}") + return output_path + + +def main(): + parser = argparse.ArgumentParser(description="Run recall evaluation on a LEANN index.") + parser.add_argument( + "index_path", + type=str, + nargs="?", + help="Path to the LEANN index to evaluate or build (optional).", + ) + parser.add_argument( + "--mode", + choices=["evaluate", "build"], + default="evaluate", + help="Mode: 'evaluate' existing index or 'build' from embeddings", + ) + parser.add_argument( + "--embeddings-file", + type=str, + help="Path to embeddings pickle file (optional for build mode)", + ) + parser.add_argument( + "--backend", + choices=["hnsw", "diskann"], + default="hnsw", + help="Backend to use for building index (default: hnsw)", + ) + parser.add_argument( + "--num-queries", type=int, default=10, help="Number of queries to evaluate." + ) + parser.add_argument("--top-k", type=int, default=3, help="The 'k' value for recall@k.") + parser.add_argument( + "--ef-search", type=int, default=120, help="The 'efSearch' parameter for HNSW." + ) + parser.add_argument( + "--batch-size", + type=int, + default=0, + help="Batch size for HNSW batched search (0 disables batching)", + ) + parser.add_argument( + "--llm-type", + type=str, + choices=["ollama", "hf", "openai", "gemini", "simulated"], + default="ollama", + help="LLM backend type to optionally query during evaluation (default: ollama)", + ) + parser.add_argument( + "--llm-model", + type=str, + default="qwen3:1.7b", + help="LLM model identifier for the chosen backend (default: qwen3:1.7b)", + ) + args = parser.parse_args() + + # --- Path Configuration --- + # Assumes a project structure where the script is in 'benchmarks/' + # and evaluation data is in 'benchmarks/data/'. + script_dir = Path(__file__).resolve().parent + data_root = script_dir / "data" + + # Download data based on mode + if args.mode == "build": + # For building mode, we need embeddings + download_data_if_needed(data_root, download_embeddings=False) # Basic data first + + # Auto-detect dataset type and download embeddings + if args.embeddings_file: + embeddings_file = args.embeddings_file + # Try to detect dataset type from embeddings file path + if "rpj_wiki" in str(embeddings_file): + dataset_type = "rpj_wiki" + elif "dpr" in str(embeddings_file): + dataset_type = "dpr" + else: + dataset_type = "dpr" # Default + else: + # Auto-detect from index path if provided, otherwise default to DPR + if args.index_path: + index_path_str = str(args.index_path) + if "rpj_wiki" in index_path_str: + dataset_type = "rpj_wiki" + elif "dpr" in index_path_str: + dataset_type = "dpr" + else: + dataset_type = "dpr" # Default to DPR + else: + dataset_type = "dpr" # Default to DPR + + embeddings_file = download_embeddings_if_needed(data_root, dataset_type) + + # Auto-generate index path if not provided + if not args.index_path: + indices_dir = data_root / "indices" / dataset_type + indices_dir.mkdir(parents=True, exist_ok=True) + args.index_path = str(indices_dir / f"{dataset_type}_from_embeddings") + print(f"Auto-generated index path: {args.index_path}") + + print(f"Building index from embeddings: {embeddings_file}") + built_index_path = build_index_from_embeddings( + embeddings_file, args.index_path, args.backend + ) + print(f"Index built successfully: {built_index_path}") + + # Ask if user wants to run evaluation + eval_response = input("Run evaluation on the built index? (y/n): ").strip().lower() + if eval_response != "y": + print("Index building complete. Exiting.") + return + else: + # For evaluation mode, don't need embeddings + download_data_if_needed(data_root, download_embeddings=False) + + # Auto-detect index path if not provided + if not args.index_path: + # Default to using downloaded indices + indices_dir = data_root / "indices" + + # Try common datasets in order of preference + for dataset in ["dpr", "rpj_wiki"]: + dataset_dir = indices_dir / dataset + if dataset_dir.exists(): + # Look for index files + index_files = list(dataset_dir.glob("*.index")) + list( + dataset_dir.glob("*_disk.index") + ) + if index_files: + args.index_path = str( + index_files[0].with_suffix("") + ) # Remove .index extension + print(f"Using index: {args.index_path}") + break + + if not args.index_path: + print("No indices found. The data download should have included pre-built indices.") + print( + "Please check the benchmarks/data/indices/ directory or provide --index-path manually." + ) + sys.exit(1) + + # Detect dataset type from index path to select the correct ground truth + index_path_str = str(args.index_path) + if "rpj_wiki" in index_path_str: + dataset_type = "rpj_wiki" + elif "dpr" in index_path_str: + dataset_type = "dpr" + else: + # Fallback: try to infer from the index directory name + dataset_type = Path(args.index_path).name + print(f"WARNING: Could not detect dataset type from path, inferred '{dataset_type}'.") + + queries_file = data_root / "queries" / "nq_open.jsonl" + golden_results_file = data_root / "ground_truth" / dataset_type / "flat_results_nq_k3.json" + + print(f"INFO: Detected dataset type: {dataset_type}") + print(f"INFO: Using queries file: {queries_file}") + print(f"INFO: Using ground truth file: {golden_results_file}") + + try: + searcher = LeannSearcher(args.index_path) + queries = load_queries(queries_file) + + with open(golden_results_file) as f: + golden_results_data = json.load(f) + + num_eval_queries = min(args.num_queries, len(queries)) + queries = queries[:num_eval_queries] + + print(f"\nRunning evaluation on {num_eval_queries} queries...") + recall_scores = [] + search_times = [] + + for i in range(num_eval_queries): + start_time = time.time() + new_results = searcher.search( + queries[i], + top_k=args.top_k, + complexity=args.ef_search, + batch_size=args.batch_size, + ) + search_times.append(time.time() - start_time) + + # Optional: also call the LLM with configurable backend/model (does not affect recall) + llm_config = {"type": args.llm_type, "model": args.llm_model} + chat = LeannChat(args.index_path, llm_config=llm_config, searcher=searcher) + answer = chat.ask( + queries[i], + top_k=args.top_k, + complexity=args.ef_search, + batch_size=args.batch_size, + ) + print(f"Answer: {answer}") + # Correct Recall Calculation: Based on TEXT content + new_texts = {result.text for result in new_results} + + # Get golden texts directly from the searcher's passage manager + golden_ids = golden_results_data["indices"][i][: args.top_k] + golden_texts = get_golden_texts(searcher, golden_ids) + + overlap = len(new_texts & golden_texts) + recall = overlap / len(golden_texts) if golden_texts else 0 + recall_scores.append(recall) + + print("\n--- EVALUATION RESULTS ---") + print(f"Query: {queries[i]}") + print(f"New Results: {new_texts}") + print(f"Golden Results: {golden_texts}") + print(f"Overlap: {overlap}") + print(f"Recall: {recall}") + print(f"Search Time: {search_times[-1]:.4f}s") + print("--------------------------------") + + avg_recall = np.mean(recall_scores) if recall_scores else 0 + avg_time = np.mean(search_times) if search_times else 0 + + print("\nπŸŽ‰ --- Evaluation Complete ---") + print(f"Avg. Recall@{args.top_k} (efSearch={args.ef_search}): {avg_recall:.4f}") + print(f"Avg. Search Time: {avg_time:.4f}s") + + except Exception as e: + print(f"\n❌ An error occurred during evaluation: {e}") + import traceback + + traceback.print_exc() + + +if __name__ == "__main__": + main() diff --git a/benchmarks/simple_mac_tpt_test.py b/benchmarks/simple_mac_tpt_test.py new file mode 100644 index 0000000..d812b8e --- /dev/null +++ b/benchmarks/simple_mac_tpt_test.py @@ -0,0 +1,317 @@ +import time +from dataclasses import dataclass + +import numpy as np +import torch +from torch import nn +from tqdm import tqdm +from transformers import AutoModel + +# Add MLX imports +try: + import mlx.core as mx + from mlx_lm.utils import load + + MLX_AVAILABLE = True +except ImportError: + print("MLX not available. Install with: uv pip install mlx mlx-lm") + MLX_AVAILABLE = False + + +@dataclass +class BenchmarkConfig: + model_path: str = "facebook/contriever-msmarco" + batch_sizes: list[int] = None + seq_length: int = 256 + num_runs: int = 5 + use_fp16: bool = True + use_int4: bool = False + use_int8: bool = False + use_cuda_graphs: bool = False + use_flash_attention: bool = False + use_linear8bitlt: bool = False + use_mlx: bool = False # New flag for MLX testing + + def __post_init__(self): + if self.batch_sizes is None: + self.batch_sizes = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024] + + +class MLXBenchmark: + """MLX-specific benchmark for embedding models""" + + def __init__(self, config: BenchmarkConfig): + self.config = config + self.model, self.tokenizer = self._load_model() + + def _load_model(self): + """Load MLX model and tokenizer following the API pattern""" + print(f"Loading MLX model from {self.config.model_path}...") + try: + model, tokenizer = load(self.config.model_path) + print("MLX model loaded successfully") + return model, tokenizer + except Exception as e: + print(f"Error loading MLX model: {e}") + raise + + def _create_random_batch(self, batch_size: int): + """Create random input batches for MLX testing - same as PyTorch""" + return torch.randint(0, 1000, (batch_size, self.config.seq_length), dtype=torch.long) + + def _run_inference(self, input_ids: torch.Tensor) -> float: + """Run MLX inference with same input as PyTorch""" + start_time = time.time() + try: + # Convert PyTorch tensor to MLX array + input_ids_mlx = mx.array(input_ids.numpy()) + + # Get embeddings + embeddings = self.model(input_ids_mlx) + + # Mean pooling (following the API pattern) + pooled = embeddings.mean(axis=1) + + # Convert to numpy (following the API pattern) + pooled_numpy = np.array(pooled.tolist(), dtype=np.float32) + + # Force computation + _ = pooled_numpy.shape + + except Exception as e: + print(f"MLX inference error: {e}") + return float("inf") + end_time = time.time() + + return end_time - start_time + + def run(self) -> dict[int, dict[str, float]]: + """Run the MLX benchmark across all batch sizes""" + results = {} + + print(f"Starting MLX benchmark with model: {self.config.model_path}") + print(f"Testing batch sizes: {self.config.batch_sizes}") + + for batch_size in self.config.batch_sizes: + print(f"\n=== Testing MLX batch size: {batch_size} ===") + times = [] + + # Create input batch (same as PyTorch) + input_ids = self._create_random_batch(batch_size) + + # Warm up + print("Warming up...") + for _ in range(3): + try: + self._run_inference(input_ids[:2]) # Warm up with smaller batch + except Exception as e: + print(f"Warmup error: {e}") + break + + # Run benchmark + for _i in tqdm(range(self.config.num_runs), desc=f"MLX Batch size {batch_size}"): + try: + elapsed_time = self._run_inference(input_ids) + if elapsed_time != float("inf"): + times.append(elapsed_time) + except Exception as e: + print(f"Error during MLX inference: {e}") + break + + if not times: + print(f"Skipping batch size {batch_size} due to errors") + continue + + # Calculate statistics + avg_time = np.mean(times) + std_time = np.std(times) + throughput = batch_size / avg_time + + results[batch_size] = { + "avg_time": avg_time, + "std_time": std_time, + "throughput": throughput, + "min_time": np.min(times), + "max_time": np.max(times), + } + + print(f"MLX Results for batch size {batch_size}:") + print(f" Avg Time: {avg_time:.4f}s Β± {std_time:.4f}s") + print(f" Min Time: {np.min(times):.4f}s") + print(f" Max Time: {np.max(times):.4f}s") + print(f" Throughput: {throughput:.2f} sequences/second") + + return results + + +class Benchmark: + def __init__(self, config: BenchmarkConfig): + self.config = config + self.device = ( + "cuda" + if torch.cuda.is_available() + else "mps" + if torch.backends.mps.is_available() + else "cpu" + ) + self.model = self._load_model() + + def _load_model(self) -> nn.Module: + print(f"Loading model from {self.config.model_path}...") + + model = AutoModel.from_pretrained(self.config.model_path) + if self.config.use_fp16: + model = model.half() + model = torch.compile(model) + model = model.to(self.device) + + model.eval() + return model + + def _create_random_batch(self, batch_size: int) -> torch.Tensor: + return torch.randint( + 0, + 1000, + (batch_size, self.config.seq_length), + device=self.device, + dtype=torch.long, + ) + + def _run_inference(self, input_ids: torch.Tensor) -> float: + attention_mask = torch.ones_like(input_ids) + # print shape of input_ids and attention_mask + print(f"input_ids shape: {input_ids.shape}") + print(f"attention_mask shape: {attention_mask.shape}") + start_time = time.time() + with torch.no_grad(): + self.model(input_ids=input_ids, attention_mask=attention_mask) + if torch.cuda.is_available(): + torch.cuda.synchronize() + if torch.backends.mps.is_available(): + torch.mps.synchronize() + end_time = time.time() + + return end_time - start_time + + def run(self) -> dict[int, dict[str, float]]: + results = {} + + if torch.cuda.is_available(): + torch.cuda.reset_peak_memory_stats() + + for batch_size in self.config.batch_sizes: + print(f"\nTesting batch size: {batch_size}") + times = [] + + input_ids = self._create_random_batch(batch_size) + + for _i in tqdm(range(self.config.num_runs), desc=f"Batch size {batch_size}"): + try: + elapsed_time = self._run_inference(input_ids) + times.append(elapsed_time) + except Exception as e: + print(f"Error during inference: {e}") + break + + if not times: + continue + + avg_time = np.mean(times) + std_time = np.std(times) + throughput = batch_size / avg_time + + results[batch_size] = { + "avg_time": avg_time, + "std_time": std_time, + "throughput": throughput, + } + + print(f"Avg Time: {avg_time:.4f}s Β± {std_time:.4f}s") + print(f"Throughput: {throughput:.2f} sequences/second") + + if torch.cuda.is_available(): + peak_memory_gb = torch.cuda.max_memory_allocated() / (1024**3) + else: + peak_memory_gb = 0.0 + + for batch_size in results: + results[batch_size]["peak_memory_gb"] = peak_memory_gb + + return results + + +def run_benchmark(): + """Main function to run the benchmark with optimized parameters.""" + config = BenchmarkConfig() + + try: + benchmark = Benchmark(config) + results = benchmark.run() + + max_throughput = max(results[batch_size]["throughput"] for batch_size in results) + avg_throughput = np.mean([results[batch_size]["throughput"] for batch_size in results]) + + return { + "max_throughput": max_throughput, + "avg_throughput": avg_throughput, + "results": results, + } + + except Exception as e: + print(f"Benchmark failed: {e}") + return {"max_throughput": 0.0, "avg_throughput": 0.0, "error": str(e)} + + +def run_mlx_benchmark(): + """Run MLX-specific benchmark""" + if not MLX_AVAILABLE: + print("MLX not available, skipping MLX benchmark") + return { + "max_throughput": 0.0, + "avg_throughput": 0.0, + "error": "MLX not available", + } + + config = BenchmarkConfig(model_path="mlx-community/all-MiniLM-L6-v2-4bit", use_mlx=True) + + try: + benchmark = MLXBenchmark(config) + results = benchmark.run() + + if not results: + return { + "max_throughput": 0.0, + "avg_throughput": 0.0, + "error": "No valid results", + } + + max_throughput = max(results[batch_size]["throughput"] for batch_size in results) + avg_throughput = np.mean([results[batch_size]["throughput"] for batch_size in results]) + + return { + "max_throughput": max_throughput, + "avg_throughput": avg_throughput, + "results": results, + } + + except Exception as e: + print(f"MLX benchmark failed: {e}") + return {"max_throughput": 0.0, "avg_throughput": 0.0, "error": str(e)} + + +if __name__ == "__main__": + print("=== PyTorch Benchmark ===") + pytorch_result = run_benchmark() + print(f"PyTorch Max throughput: {pytorch_result['max_throughput']:.2f} sequences/second") + print(f"PyTorch Average throughput: {pytorch_result['avg_throughput']:.2f} sequences/second") + + print("\n=== MLX Benchmark ===") + mlx_result = run_mlx_benchmark() + print(f"MLX Max throughput: {mlx_result['max_throughput']:.2f} sequences/second") + print(f"MLX Average throughput: {mlx_result['avg_throughput']:.2f} sequences/second") + + # Compare results + if pytorch_result["max_throughput"] > 0 and mlx_result["max_throughput"] > 0: + speedup = mlx_result["max_throughput"] / pytorch_result["max_throughput"] + print("\n=== Comparison ===") + print(f"MLX is {speedup:.2f}x {'faster' if speedup > 1 else 'slower'} than PyTorch") diff --git a/benchmarks/update/README.md b/benchmarks/update/README.md new file mode 100644 index 0000000..585342b --- /dev/null +++ b/benchmarks/update/README.md @@ -0,0 +1,143 @@ +# Update Benchmarks + +This directory hosts two benchmark suites that exercise LEANN’s HNSW β€œupdate + +search” pipeline under different assumptions: + +1. **RNG recompute latency** – measure how random-neighbour pruning and cache + settings influence incremental `add()` latency when embeddings are fetched + over the ZMQ embedding server. +2. **Update strategy comparison** – compare a fully sequential update pipeline + against an offline approach that keeps the graph static and fuses results. + +Both suites build a non-compact, `is_recompute=True` index so that new +embeddings are pulled from the embedding server. Benchmark outputs are written +under `.leann/bench/` by default and appended to CSV files for later plotting. + +## Benchmarks + +### 1. HNSW RNG Recompute Benchmark + +`bench_hnsw_rng_recompute.py` evaluates incremental update latency under four +random-neighbour (RNG) configurations. Each scenario uses the same dataset but +changes the forward / reverse RNG pruning flags and whether the embedding cache +is enabled: + +| Scenario name | Forward RNG | Reverse RNG | ZMQ embedding cache | +| ---------------------------------- | ----------- | ----------- | ------------------- | +| `baseline` | Enabled | Enabled | Enabled | +| `no_cache_baseline` | Enabled | Enabled | **Disabled** | +| `disable_forward_rng` | **Disabled**| Enabled | Enabled | +| `disable_forward_and_reverse_rng` | **Disabled**| **Disabled**| Enabled | + +For each scenario the script: +1. (Re)builds a `is_recompute=True` index and writes it to `.leann/bench/`. +2. Starts `leann_backend_hnsw.hnsw_embedding_server` for remote embeddings. +3. Appends the requested updates using the scenario’s RNG flags. +4. Records total time, latency per passage, ZMQ fetch counts, and stage-level + timings before appending a row to the CSV output. + +**Run:** +```bash +LEANN_HNSW_LOG_PATH=.leann/bench/hnsw_server.log \ +LEANN_LOG_LEVEL=INFO \ +uv run -m benchmarks.update.bench_hnsw_rng_recompute \ + --runs 1 \ + --index-path .leann/bench/test.leann \ + --initial-files data/PrideandPrejudice.txt \ + --update-files data/huawei_pangu.md \ + --max-initial 300 \ + --max-updates 1 \ + --add-timeout 120 +``` + +**Output:** +- `benchmarks/update/bench_results.csv` – per-scenario timing statistics + (including ms/passage) for each run. +- `.leann/bench/hnsw_server.log` – detailed ZMQ/server logs (path controlled by + `LEANN_HNSW_LOG_PATH`). + _The reference CSVs checked into this branch were generated on a workstation with an NVIDIA RTX 4090 GPU; throughput numbers will differ on other hardware._ + +### 2. Sequential vs. Offline Update Benchmark + +`bench_update_vs_offline_search.py` compares two end-to-end strategies on the +same dataset: + +- **Scenario A – Sequential Update** + - Start an embedding server. + - Sequentially call `index.add()`; each call fetches embeddings via ZMQ and + mutates the HNSW graph. + - After all inserts, run a search on the updated graph. + - Metrics recorded: update time (`add_total_s`), post-update search time + (`search_time_s`), combined total (`total_time_s`), and per-passage + latency. + +- **Scenario B – Offline Embedding + Concurrent Search** + - Stop Scenario A’s server and start a fresh embedding server. + - Spawn two threads: one generates embeddings for the new passages offline + (graph unchanged); the other computes the query embedding and searches the + existing graph. + - Merge offline similarities with the graph search results to emulate late + fusion, then report the merged top‑k preview. + - Metrics recorded: embedding time (`emb_time_s`), search time + (`search_time_s`), concurrent makespan (`makespan_s`), and scenario total. + +**Run (both scenarios):** +```bash +uv run -m benchmarks.update.bench_update_vs_offline_search \ + --index-path .leann/bench/offline_vs_update.leann \ + --max-initial 300 \ + --num-updates 1 +``` + +You can pass `--only A` or `--only B` to run a single scenario. The script will +print timing summaries to stdout and append the results to CSV. + +**Output:** +- `benchmarks/update/offline_vs_update.csv` – per-scenario timing statistics for + Scenario A and B. +- Console output includes Scenario B’s merged top‑k preview for quick sanity + checks. + _The sample results committed here come from runs on an RTX 4090-equipped machine; expect variations if you benchmark on different GPUs._ + +### 3. Visualisation + +`plot_bench_results.py` combines the RNG benchmark and the update strategy +benchmark into a single two-panel plot. + +**Run:** +```bash +uv run -m benchmarks.update.plot_bench_results \ + --csv benchmarks/update/bench_results.csv \ + --csv-right benchmarks/update/offline_vs_update.csv \ + --out benchmarks/update/bench_latency_from_csv.png +``` + +**Options:** +- `--broken-y` – Enable a broken Y-axis (default: true when appropriate). +- `--csv` – RNG benchmark results CSV (left panel). +- `--csv-right` – Update strategy results CSV (right panel). +- `--out` – Output image path (PNG/PDF supported). + +**Output:** +- `benchmarks/update/bench_latency_from_csv.png` – visual comparison of the two + suites. +- `benchmarks/update/bench_latency_from_csv.pdf` – PDF version, suitable for + slides/papers. + +## Parameters & Environment + +### Common CLI Flags +- `--max-initial` – Number of initial passages used to seed the index. +- `--max-updates` / `--num-updates` – Number of passages to treat as updates. +- `--index-path` – Base path (without extension) where the LEANN index is stored. +- `--runs` – Number of repetitions (RNG benchmark only). + +### Environment Variables +- `LEANN_HNSW_LOG_PATH` – File to receive embedding-server logs (optional). +- `LEANN_LOG_LEVEL` – Logging verbosity (DEBUG/INFO/WARNING/ERROR). +- `CUDA_VISIBLE_DEVICES` – Set to empty string if you want to force CPU + execution of the embedding model. + +With these scripts you can easily replicate LEANN’s update benchmarks, compare +multiple RNG strategies, and evaluate whether sequential updates or offline +fusion better match your latency/accuracy trade-offs. diff --git a/benchmarks/update/__init__.py b/benchmarks/update/__init__.py new file mode 100644 index 0000000..4970eba --- /dev/null +++ b/benchmarks/update/__init__.py @@ -0,0 +1,16 @@ +"""Benchmarks for LEANN update workflows.""" + +# Expose helper to locate repository root for other modules that need it. +from pathlib import Path + + +def find_repo_root() -> Path: + """Return the project root containing pyproject.toml.""" + current = Path(__file__).resolve() + for parent in current.parents: + if (parent / "pyproject.toml").exists(): + return parent + return current.parents[1] + + +__all__ = ["find_repo_root"] diff --git a/benchmarks/update/bench_hnsw_rng_recompute.py b/benchmarks/update/bench_hnsw_rng_recompute.py new file mode 100644 index 0000000..81272ae --- /dev/null +++ b/benchmarks/update/bench_hnsw_rng_recompute.py @@ -0,0 +1,804 @@ +"""Benchmark incremental HNSW add() under different RNG pruning modes with real +embedding recomputation. + +This script clones the structure of ``examples/dynamic_update_no_recompute.py`` +so that we build a non-compact ``is_recompute=True`` index, spin up the +standard HNSW embedding server, and measure how long incremental ``add`` takes +when RNG pruning is fully enabled vs. partially/fully disabled. + +Example usage (run from the repo root; downloads the model on first run):: + + uv run -m benchmarks.update.bench_hnsw_rng_recompute \ + --index-path .leann/bench/leann-demo.leann \ + --runs 1 + +You can tweak the input documents with ``--initial-files`` / ``--update-files`` +if you want a larger or different workload, and change the embedding model via +``--model-name``. +""" + +import argparse +import json +import logging +import os +import pickle +import re +import sys +import time +from pathlib import Path +from typing import Any + +import msgpack +import numpy as np +import zmq +from leann.api import LeannBuilder + +if os.environ.get("LEANN_FORCE_CPU", "").lower() in ("1", "true", "yes"): + os.environ.setdefault("CUDA_VISIBLE_DEVICES", "") + +from leann.embedding_compute import compute_embeddings +from leann.embedding_server_manager import EmbeddingServerManager +from leann.registry import register_project_directory +from leann_backend_hnsw import faiss # type: ignore +from leann_backend_hnsw.convert_to_csr import prune_hnsw_embeddings_inplace + +logger = logging.getLogger(__name__) +if not logging.getLogger().handlers: + logging.basicConfig(level=logging.INFO) + + +def _find_repo_root() -> Path: + """Locate project root by walking up until pyproject.toml is found.""" + current = Path(__file__).resolve() + for parent in current.parents: + if (parent / "pyproject.toml").exists(): + return parent + # Fallback: assume repo is two levels up (../..) + return current.parents[2] + + +REPO_ROOT = _find_repo_root() +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from apps.chunking import create_text_chunks # noqa: E402 + +DEFAULT_INITIAL_FILES = [ + REPO_ROOT / "data" / "2501.14312v1 (1).pdf", + REPO_ROOT / "data" / "huawei_pangu.md", +] +DEFAULT_UPDATE_FILES = [REPO_ROOT / "data" / "2506.08276v1.pdf"] + +DEFAULT_HNSW_LOG = Path(".leann/bench/hnsw_server.log") + + +def load_chunks_from_files(paths: list[Path], limit: int | None = None) -> list[str]: + from llama_index.core import SimpleDirectoryReader + + documents = [] + for path in paths: + p = path.expanduser().resolve() + if not p.exists(): + raise FileNotFoundError(f"Input path not found: {p}") + if p.is_dir(): + reader = SimpleDirectoryReader(str(p), recursive=False) + documents.extend(reader.load_data(show_progress=True)) + else: + reader = SimpleDirectoryReader(input_files=[str(p)]) + documents.extend(reader.load_data(show_progress=True)) + + if not documents: + return [] + + chunks = create_text_chunks( + documents, + chunk_size=512, + chunk_overlap=128, + use_ast_chunking=False, + ) + cleaned = [c for c in chunks if isinstance(c, str) and c.strip()] + if limit is not None: + cleaned = cleaned[:limit] + return cleaned + + +def ensure_index_dir(index_path: Path) -> None: + index_path.parent.mkdir(parents=True, exist_ok=True) + + +def cleanup_index_files(index_path: Path) -> None: + parent = index_path.parent + if not parent.exists(): + return + stem = index_path.stem + for file in parent.glob(f"{stem}*"): + if file.is_file(): + file.unlink() + + +def build_initial_index( + index_path: Path, + paragraphs: list[str], + model_name: str, + embedding_mode: str, + distance_metric: str, + ef_construction: int, +) -> None: + builder = LeannBuilder( + backend_name="hnsw", + embedding_model=model_name, + embedding_mode=embedding_mode, + is_compact=False, + is_recompute=True, + distance_metric=distance_metric, + backend_kwargs={ + "distance_metric": distance_metric, + "is_compact": False, + "is_recompute": True, + "efConstruction": ef_construction, + }, + ) + for idx, passage in enumerate(paragraphs): + builder.add_text(passage, metadata={"id": str(idx)}) + builder.build_index(str(index_path)) + + +def prepare_new_chunks(paragraphs: list[str]) -> list[dict[str, Any]]: + return [{"text": text, "metadata": {}} for text in paragraphs] + + +def benchmark_update_with_mode( + index_path: Path, + new_chunks: list[dict[str, Any]], + model_name: str, + embedding_mode: str, + distance_metric: str, + disable_forward_rng: bool, + disable_reverse_rng: bool, + server_port: int, + add_timeout: int, + ef_construction: int, +) -> tuple[float, float]: + meta_path = index_path.parent / f"{index_path.name}.meta.json" + passages_file = index_path.parent / f"{index_path.name}.passages.jsonl" + offset_file = index_path.parent / f"{index_path.name}.passages.idx" + index_file = index_path.parent / f"{index_path.stem}.index" + + with open(meta_path, encoding="utf-8") as f: + meta = json.load(f) + + with open(offset_file, "rb") as f: + offset_map: dict[str, int] = pickle.load(f) + existing_ids = set(offset_map.keys()) + + valid_chunks: list[dict[str, Any]] = [] + for chunk in new_chunks: + text = chunk.get("text", "") + if not isinstance(text, str) or not text.strip(): + continue + metadata = chunk.setdefault("metadata", {}) + passage_id = chunk.get("id") or metadata.get("id") + if passage_id and passage_id in existing_ids: + raise ValueError(f"Passage ID '{passage_id}' already exists in the index.") + valid_chunks.append(chunk) + + if not valid_chunks: + raise ValueError("No valid chunks to append.") + + texts_to_embed = [chunk["text"] for chunk in valid_chunks] + embeddings = compute_embeddings( + texts_to_embed, + model_name, + mode=embedding_mode, + is_build=False, + batch_size=16, + ) + + embeddings = np.ascontiguousarray(embeddings, dtype=np.float32) + if distance_metric == "cosine": + norms = np.linalg.norm(embeddings, axis=1, keepdims=True) + norms[norms == 0] = 1 + embeddings = embeddings / norms + + index = faiss.read_index(str(index_file)) + index.is_recompute = True + if getattr(index, "storage", None) is None: + if index.metric_type == faiss.METRIC_INNER_PRODUCT: + storage_index = faiss.IndexFlatIP(index.d) + else: + storage_index = faiss.IndexFlatL2(index.d) + index.storage = storage_index + index.own_fields = True + try: + storage_index.ntotal = index.ntotal + except AttributeError: + pass + try: + index.hnsw.set_disable_rng_during_add(disable_forward_rng) + index.hnsw.set_disable_reverse_prune(disable_reverse_rng) + if ef_construction is not None: + index.hnsw.efConstruction = ef_construction + except AttributeError: + pass + + applied_forward = getattr(index.hnsw, "disable_rng_during_add", None) + applied_reverse = getattr(index.hnsw, "disable_reverse_prune", None) + logger.info( + "HNSW RNG config -> requested forward=%s, reverse=%s | applied forward=%s, reverse=%s", + disable_forward_rng, + disable_reverse_rng, + applied_forward, + applied_reverse, + ) + + base_id = index.ntotal + for offset, chunk in enumerate(valid_chunks): + new_id = str(base_id + offset) + chunk.setdefault("metadata", {})["id"] = new_id + chunk["id"] = new_id + + rollback_size = passages_file.stat().st_size if passages_file.exists() else 0 + offset_map_backup = offset_map.copy() + + try: + with open(passages_file, "a", encoding="utf-8") as f: + for chunk in valid_chunks: + offset = f.tell() + json.dump( + { + "id": chunk["id"], + "text": chunk["text"], + "metadata": chunk.get("metadata", {}), + }, + f, + ensure_ascii=False, + ) + f.write("\n") + offset_map[chunk["id"]] = offset + + with open(offset_file, "wb") as f: + pickle.dump(offset_map, f) + + server_manager = EmbeddingServerManager( + backend_module_name="leann_backend_hnsw.hnsw_embedding_server" + ) + server_started, actual_port = server_manager.start_server( + port=server_port, + model_name=model_name, + embedding_mode=embedding_mode, + passages_file=str(meta_path), + distance_metric=distance_metric, + ) + if not server_started: + raise RuntimeError("Failed to start embedding server.") + + if hasattr(index.hnsw, "set_zmq_port"): + index.hnsw.set_zmq_port(actual_port) + elif hasattr(index, "set_zmq_port"): + index.set_zmq_port(actual_port) + + _warmup_embedding_server(actual_port) + + total_start = time.time() + add_elapsed = 0.0 + + try: + import signal + + def _timeout_handler(signum, frame): + raise TimeoutError("incremental add timed out") + + if add_timeout > 0: + signal.signal(signal.SIGALRM, _timeout_handler) + signal.alarm(add_timeout) + + add_start = time.time() + for i in range(embeddings.shape[0]): + index.add(1, faiss.swig_ptr(embeddings[i : i + 1])) + add_elapsed = time.time() - add_start + if add_timeout > 0: + signal.alarm(0) + faiss.write_index(index, str(index_file)) + finally: + server_manager.stop_server() + + except TimeoutError: + raise + except Exception: + if passages_file.exists(): + with open(passages_file, "rb+") as f: + f.truncate(rollback_size) + with open(offset_file, "wb") as f: + pickle.dump(offset_map_backup, f) + raise + + prune_hnsw_embeddings_inplace(str(index_file)) + + meta["total_passages"] = len(offset_map) + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, indent=2) + + # Reset toggles so the index on disk returns to baseline behaviour. + try: + index.hnsw.set_disable_rng_during_add(False) + index.hnsw.set_disable_reverse_prune(False) + except AttributeError: + pass + faiss.write_index(index, str(index_file)) + + total_elapsed = time.time() - total_start + + return total_elapsed, add_elapsed + + +def _total_zmq_nodes(log_path: Path) -> int: + if not log_path.exists(): + return 0 + with log_path.open("r", encoding="utf-8") as log_file: + text = log_file.read() + return sum(int(match) for match in re.findall(r"ZMQ received (\d+) node IDs", text)) + + +def _warmup_embedding_server(port: int) -> None: + """Send a dummy REQ so the embedding server loads its model.""" + ctx = zmq.Context() + try: + sock = ctx.socket(zmq.REQ) + sock.setsockopt(zmq.LINGER, 0) + sock.setsockopt(zmq.RCVTIMEO, 5000) + sock.setsockopt(zmq.SNDTIMEO, 5000) + sock.connect(f"tcp://127.0.0.1:{port}") + payload = msgpack.packb(["__WARMUP__"], use_bin_type=True) + sock.send(payload) + try: + sock.recv() + except zmq.error.Again: + pass + finally: + sock.close() + ctx.term() + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--index-path", + type=Path, + default=Path(".leann/bench/leann-demo.leann"), + help="Output index base path (without extension).", + ) + parser.add_argument( + "--initial-files", + nargs="*", + type=Path, + default=DEFAULT_INITIAL_FILES, + help="Files used to build the initial index.", + ) + parser.add_argument( + "--update-files", + nargs="*", + type=Path, + default=DEFAULT_UPDATE_FILES, + help="Files appended during the benchmark.", + ) + parser.add_argument( + "--runs", type=int, default=1, help="How many times to repeat each scenario." + ) + parser.add_argument( + "--model-name", + default="sentence-transformers/all-MiniLM-L6-v2", + help="Embedding model used for build/update.", + ) + parser.add_argument( + "--embedding-mode", + default="sentence-transformers", + help="Embedding mode passed to LeannBuilder/embedding server.", + ) + parser.add_argument( + "--distance-metric", + default="mips", + choices=["mips", "l2", "cosine"], + help="Distance metric for HNSW backend.", + ) + parser.add_argument( + "--ef-construction", + type=int, + default=200, + help="efConstruction setting for initial build.", + ) + parser.add_argument( + "--server-port", + type=int, + default=5557, + help="Port for the real embedding server.", + ) + parser.add_argument( + "--max-initial", + type=int, + default=300, + help="Optional cap on initial passages (after chunking).", + ) + parser.add_argument( + "--max-updates", + type=int, + default=1, + help="Optional cap on update passages (after chunking).", + ) + parser.add_argument( + "--add-timeout", + type=int, + default=900, + help="Timeout in seconds for the incremental add loop (0 = no timeout).", + ) + parser.add_argument( + "--plot-path", + type=Path, + default=Path("bench_latency.png"), + help="Where to save the latency bar plot.", + ) + parser.add_argument( + "--cap-y", + type=float, + default=None, + help="Cap Y-axis (ms). Bars above are hatched and annotated.", + ) + parser.add_argument( + "--broken-y", + action="store_true", + help="Use broken Y-axis (two stacked axes with gap). Overrides --cap-y unless both provided.", + ) + parser.add_argument( + "--lower-cap-y", + type=float, + default=None, + help="Lower axes upper bound for broken Y (ms). Default=1.1x second-highest.", + ) + parser.add_argument( + "--upper-start-y", + type=float, + default=None, + help="Upper axes lower bound for broken Y (ms). Default=1.2x second-highest.", + ) + parser.add_argument( + "--csv-path", + type=Path, + default=Path("benchmarks/update/bench_results.csv"), + help="Where to append per-scenario results as CSV.", + ) + + args = parser.parse_args() + + register_project_directory(REPO_ROOT) + + initial_paragraphs = load_chunks_from_files(args.initial_files, args.max_initial) + update_paragraphs = load_chunks_from_files(args.update_files, args.max_updates) + if not update_paragraphs: + raise ValueError("No update passages found; please provide --update-files with content.") + + update_chunks = prepare_new_chunks(update_paragraphs) + ensure_index_dir(args.index_path) + + scenarios = [ + ("baseline", False, False, True), + ("no_cache_baseline", False, False, False), + ("disable_forward_rng", True, False, True), + ("disable_forward_and_reverse_rng", True, True, True), + ] + + log_path = Path(os.environ.get("LEANN_HNSW_LOG_PATH", DEFAULT_HNSW_LOG)) + log_path.parent.mkdir(parents=True, exist_ok=True) + os.environ["LEANN_HNSW_LOG_PATH"] = str(log_path.resolve()) + os.environ.setdefault("LEANN_LOG_LEVEL", "INFO") + + results_total: dict[str, list[float]] = {name: [] for name, *_ in scenarios} + results_add: dict[str, list[float]] = {name: [] for name, *_ in scenarios} + results_zmq: dict[str, list[int]] = {name: [] for name, *_ in scenarios} + results_stageA: dict[str, list[float]] = {name: [] for name, *_ in scenarios} + results_stageBC: dict[str, list[float]] = {name: [] for name, *_ in scenarios} + results_ms_per_passage: dict[str, list[float]] = {name: [] for name, *_ in scenarios} + + # CSV setup + import csv + + run_id = time.strftime("%Y%m%d-%H%M%S") + csv_fields = [ + "run_id", + "scenario", + "cache_enabled", + "ef_construction", + "max_initial", + "max_updates", + "total_time_s", + "add_only_s", + "latency_ms_per_passage", + "zmq_nodes", + "stageA_time_s", + "stageBC_time_s", + "model_name", + "embedding_mode", + "distance_metric", + ] + # Create CSV with header if missing + if args.csv_path: + args.csv_path.parent.mkdir(parents=True, exist_ok=True) + if not args.csv_path.exists() or args.csv_path.stat().st_size == 0: + with args.csv_path.open("w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=csv_fields) + writer.writeheader() + + for run in range(args.runs): + print(f"\n=== Benchmark run {run + 1}/{args.runs} ===") + for name, disable_forward, disable_reverse, cache_enabled in scenarios: + print(f"\nScenario: {name}") + cleanup_index_files(args.index_path) + if log_path.exists(): + try: + log_path.unlink() + except OSError: + pass + os.environ["LEANN_ZMQ_EMBED_CACHE"] = "1" if cache_enabled else "0" + build_initial_index( + args.index_path, + initial_paragraphs, + args.model_name, + args.embedding_mode, + args.distance_metric, + args.ef_construction, + ) + + prev_size = log_path.stat().st_size if log_path.exists() else 0 + + try: + total_elapsed, add_elapsed = benchmark_update_with_mode( + args.index_path, + update_chunks, + args.model_name, + args.embedding_mode, + args.distance_metric, + disable_forward, + disable_reverse, + args.server_port, + args.add_timeout, + args.ef_construction, + ) + except TimeoutError as exc: + print(f"Scenario {name} timed out: {exc}") + continue + + curr_size = log_path.stat().st_size if log_path.exists() else 0 + if curr_size < prev_size: + prev_size = 0 + zmq_count = 0 + if log_path.exists(): + with log_path.open("r", encoding="utf-8") as log_file: + log_file.seek(prev_size) + new_entries = log_file.read() + zmq_count = sum( + int(match) for match in re.findall(r"ZMQ received (\d+) node IDs", new_entries) + ) + stageA = sum( + float(x) + for x in re.findall(r"Distance calculation E2E time: ([0-9.]+)s", new_entries) + ) + stageBC = sum( + float(x) for x in re.findall(r"ZMQ E2E time: ([0-9.]+)s", new_entries) + ) + else: + stageA = 0.0 + stageBC = 0.0 + + per_chunk = add_elapsed / len(update_chunks) + print( + f"Total time: {total_elapsed:.3f} s | add-only: {add_elapsed:.3f} s " + f"for {len(update_chunks)} passages => {per_chunk * 1e3:.3f} ms/passage" + ) + print(f"ZMQ node fetch total: {zmq_count}") + results_total[name].append(total_elapsed) + results_add[name].append(add_elapsed) + results_zmq[name].append(zmq_count) + results_ms_per_passage[name].append(per_chunk * 1e3) + results_stageA[name].append(stageA) + results_stageBC[name].append(stageBC) + + # Append row to CSV + if args.csv_path: + row = { + "run_id": run_id, + "scenario": name, + "cache_enabled": 1 if cache_enabled else 0, + "ef_construction": args.ef_construction, + "max_initial": args.max_initial, + "max_updates": args.max_updates, + "total_time_s": round(total_elapsed, 6), + "add_only_s": round(add_elapsed, 6), + "latency_ms_per_passage": round(per_chunk * 1e3, 6), + "zmq_nodes": int(zmq_count), + "stageA_time_s": round(stageA, 6), + "stageBC_time_s": round(stageBC, 6), + "model_name": args.model_name, + "embedding_mode": args.embedding_mode, + "distance_metric": args.distance_metric, + } + with args.csv_path.open("a", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=csv_fields) + writer.writerow(row) + + print("\n=== Summary ===") + for name in results_add: + add_values = results_add[name] + total_values = results_total[name] + zmq_values = results_zmq[name] + latency_values = results_ms_per_passage[name] + if not add_values: + print(f"{name}: no successful runs") + continue + avg_add = sum(add_values) / len(add_values) + avg_total = sum(total_values) / len(total_values) + avg_zmq = sum(zmq_values) / len(zmq_values) if zmq_values else 0.0 + avg_latency = sum(latency_values) / len(latency_values) if latency_values else 0.0 + runs = len(add_values) + print( + f"{name}: add-only avg {avg_add:.3f} s | total avg {avg_total:.3f} s " + f"| ZMQ avg {avg_zmq:.1f} node fetches | latency {avg_latency:.2f} ms/passage over {runs} run(s)" + ) + + if args.plot_path: + try: + import matplotlib.pyplot as plt + + labels = [name for name, *_ in scenarios] + values = [ + sum(results_ms_per_passage[name]) / len(results_ms_per_passage[name]) + if results_ms_per_passage[name] + else 0.0 + for name in labels + ] + + def _auto_cap(vals: list[float]) -> float | None: + s = sorted(vals, reverse=True) + if len(s) < 2: + return None + if s[1] > 0 and s[0] >= 2.5 * s[1]: + return s[1] * 1.1 + return None + + def _fmt_ms(v: float) -> str: + return f"{v / 1000:.1f}k" if v >= 1000 else f"{v:.1f}" + + colors = ["#4e79a7", "#f28e2c", "#e15759", "#76b7b2"] + + if args.broken_y: + s = sorted(values, reverse=True) + second = s[1] if len(s) >= 2 else (s[0] if s else 0.0) + lower_cap = args.lower_cap_y if args.lower_cap_y is not None else second * 1.1 + upper_start = ( + args.upper_start_y + if args.upper_start_y is not None + else max(second * 1.2, lower_cap * 1.02) + ) + ymax = max(values) * 1.10 if values else 1.0 + fig, (ax_top, ax_bottom) = plt.subplots( + 2, + 1, + sharex=True, + figsize=(7.4, 5.0), + gridspec_kw={"height_ratios": [1, 3], "hspace": 0.05}, + ) + x = list(range(len(labels))) + ax_bottom.bar(x, values, color=colors[: len(labels)], width=0.8) + ax_top.bar(x, values, color=colors[: len(labels)], width=0.8) + ax_bottom.set_ylim(0, lower_cap) + ax_top.set_ylim(upper_start, ymax) + for i, v in enumerate(values): + if v <= lower_cap: + ax_bottom.text( + i, + v + lower_cap * 0.02, + _fmt_ms(v), + ha="center", + va="bottom", + fontsize=9, + ) + else: + ax_top.text(i, v, _fmt_ms(v), ha="center", va="bottom", fontsize=9) + ax_top.spines["bottom"].set_visible(False) + ax_bottom.spines["top"].set_visible(False) + ax_top.tick_params(labeltop=False) + ax_bottom.xaxis.tick_bottom() + d = 0.015 + kwargs = {"transform": ax_top.transAxes, "color": "k", "clip_on": False} + ax_top.plot((-d, +d), (-d, +d), **kwargs) + ax_top.plot((1 - d, 1 + d), (-d, +d), **kwargs) + kwargs.update({"transform": ax_bottom.transAxes}) + ax_bottom.plot((-d, +d), (1 - d, 1 + d), **kwargs) + ax_bottom.plot((1 - d, 1 + d), (1 - d, 1 + d), **kwargs) + ax_bottom.set_xticks(range(len(labels))) + ax_bottom.set_xticklabels(labels) + ax = ax_bottom + else: + cap = args.cap_y or _auto_cap(values) + plt.figure(figsize=(7.2, 4.2)) + ax = plt.gca() + if cap is not None: + show_vals = [min(v, cap) for v in values] + bars = [] + for i, (v, show) in enumerate(zip(values, show_vals)): + b = ax.bar(i, show, color=colors[i], width=0.8) + bars.append(b[0]) + if v > cap: + bars[-1].set_hatch("//") + ax.text(i, cap * 1.02, _fmt_ms(v), ha="center", va="bottom", fontsize=9) + else: + ax.text( + i, + show + max(1.0, 0.01 * (cap or show)), + _fmt_ms(v), + ha="center", + va="bottom", + fontsize=9, + ) + ax.set_ylim(0, cap * 1.10) + ax.plot( + [0.02 - 0.02, 0.02 + 0.02], + [0.98 + 0.02, 0.98 - 0.02], + transform=ax.transAxes, + color="k", + lw=1, + ) + ax.plot( + [0.98 - 0.02, 0.98 + 0.02], + [0.98 + 0.02, 0.98 - 0.02], + transform=ax.transAxes, + color="k", + lw=1, + ) + if any(v > cap for v in values): + ax.legend( + [bars[0]], ["capped"], fontsize=8, frameon=False, loc="upper right" + ) + ax.set_xticks(range(len(labels))) + ax.set_xticklabels(labels) + else: + ax.bar(labels, values, color=colors[: len(labels)]) + for idx, val in enumerate(values): + ax.text(idx, val + 1.0, f"{val:.1f}", ha="center", va="bottom") + + plt.ylabel("Average add latency (ms per passage)") + plt.title(f"Initial passages {args.max_initial}, updates {args.max_updates}") + plt.tight_layout() + plt.savefig(args.plot_path) + print(f"Saved latency bar plot to {args.plot_path}") + # ZMQ time split (Stage A vs B/C) + try: + plt.figure(figsize=(6, 4)) + a_vals = [sum(results_stageA[n]) / max(1, len(results_stageA[n])) for n in labels] + bc_vals = [ + sum(results_stageBC[n]) / max(1, len(results_stageBC[n])) for n in labels + ] + ind = range(len(labels)) + plt.bar(ind, a_vals, color="#4e79a7", label="Stage A distance (s)") + plt.bar( + ind, bc_vals, bottom=a_vals, color="#e15759", label="Stage B/C embed-by-id (s)" + ) + plt.xticks(list(ind), labels, rotation=10) + plt.ylabel("Server ZMQ time (s)") + plt.title( + f"ZMQ time split (initial {args.max_initial}, updates {args.max_updates})" + ) + plt.legend() + out2 = args.plot_path.with_name( + args.plot_path.stem + "_zmq_split" + args.plot_path.suffix + ) + plt.tight_layout() + plt.savefig(out2) + print(f"Saved ZMQ time split plot to {out2}") + except Exception as e: + print("Failed to plot ZMQ split:", e) + except ImportError: + print("matplotlib not available; skipping plot generation") + + # leave the last build on disk for inspection + + +if __name__ == "__main__": + main() diff --git a/benchmarks/update/bench_update_vs_offline_search.py b/benchmarks/update/bench_update_vs_offline_search.py new file mode 100644 index 0000000..250bd19 --- /dev/null +++ b/benchmarks/update/bench_update_vs_offline_search.py @@ -0,0 +1,704 @@ +""" +Compare two latency models for small incremental updates vs. search: + +Scenario A (sequential update then search): + - Build initial HNSW (is_recompute=True) + - Start embedding server (ZMQ) for recompute + - Add N passages one-by-one (each triggers recompute over ZMQ) + - Then run a search query on the updated index + - Report total time = sum(add_i) + search_time, with breakdowns + +Scenario B (offline embeds + concurrent search; no graph updates): + - Do NOT insert the N passages into the graph + - In parallel: (1) compute embeddings for the N passages; (2) compute query + embedding and run a search on the existing index + - After both finish, compute similarity between the query embedding and the N + new passage embeddings, merge with the index search results by score, and + report time = max(embed_time, search_time) (i.e., no blocking on updates) + +This script reuses the model/data loading conventions of +examples/bench_hnsw_rng_recompute.py but focuses on end-to-end latency +comparison for the two execution strategies above. + +Example (from the repository root): + uv run -m benchmarks.update.bench_update_vs_offline_search \ + --index-path .leann/bench/offline_vs_update.leann \ + --max-initial 300 --num-updates 5 --k 10 +""" + +import argparse +import csv +import json +import logging +import os +import pickle +import sys +import threading +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import numpy as np +import psutil # type: ignore +from leann.api import LeannBuilder + +if os.environ.get("LEANN_FORCE_CPU", "").lower() in ("1", "true", "yes"): + os.environ.setdefault("CUDA_VISIBLE_DEVICES", "") + +from leann.embedding_compute import compute_embeddings +from leann.embedding_server_manager import EmbeddingServerManager +from leann.registry import register_project_directory +from leann_backend_hnsw import faiss # type: ignore + +logger = logging.getLogger(__name__) +if not logging.getLogger().handlers: + logging.basicConfig(level=logging.INFO) + + +def _find_repo_root() -> Path: + """Locate project root by walking up until pyproject.toml is found.""" + current = Path(__file__).resolve() + for parent in current.parents: + if (parent / "pyproject.toml").exists(): + return parent + # Fallback: assume repo is two levels up (../..) + return current.parents[2] + + +REPO_ROOT = _find_repo_root() +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from apps.chunking import create_text_chunks # noqa: E402 + +DEFAULT_INITIAL_FILES = [ + REPO_ROOT / "data" / "2501.14312v1 (1).pdf", + REPO_ROOT / "data" / "huawei_pangu.md", +] +DEFAULT_UPDATE_FILES = [REPO_ROOT / "data" / "2506.08276v1.pdf"] + + +def load_chunks_from_files(paths: list[Path], limit: int | None = None) -> list[str]: + from llama_index.core import SimpleDirectoryReader + + documents = [] + for path in paths: + p = path.expanduser().resolve() + if not p.exists(): + raise FileNotFoundError(f"Input path not found: {p}") + if p.is_dir(): + reader = SimpleDirectoryReader(str(p), recursive=False) + documents.extend(reader.load_data(show_progress=True)) + else: + reader = SimpleDirectoryReader(input_files=[str(p)]) + documents.extend(reader.load_data(show_progress=True)) + + if not documents: + return [] + + chunks = create_text_chunks( + documents, + chunk_size=512, + chunk_overlap=128, + use_ast_chunking=False, + ) + cleaned = [c for c in chunks if isinstance(c, str) and c.strip()] + if limit is not None: + cleaned = cleaned[:limit] + return cleaned + + +def ensure_index_dir(index_path: Path) -> None: + index_path.parent.mkdir(parents=True, exist_ok=True) + + +def cleanup_index_files(index_path: Path) -> None: + parent = index_path.parent + if not parent.exists(): + return + stem = index_path.stem + for file in parent.glob(f"{stem}*"): + if file.is_file(): + file.unlink() + + +def build_initial_index( + index_path: Path, + paragraphs: list[str], + model_name: str, + embedding_mode: str, + distance_metric: str, + ef_construction: int, +) -> None: + builder = LeannBuilder( + backend_name="hnsw", + embedding_model=model_name, + embedding_mode=embedding_mode, + is_compact=False, + is_recompute=True, + distance_metric=distance_metric, + backend_kwargs={ + "distance_metric": distance_metric, + "is_compact": False, + "is_recompute": True, + "efConstruction": ef_construction, + }, + ) + for idx, passage in enumerate(paragraphs): + builder.add_text(passage, metadata={"id": str(idx)}) + builder.build_index(str(index_path)) + + +def _maybe_norm_cosine(vecs: np.ndarray, metric: str) -> np.ndarray: + if metric == "cosine": + vecs = np.ascontiguousarray(vecs, dtype=np.float32) + norms = np.linalg.norm(vecs, axis=1, keepdims=True) + norms[norms == 0] = 1 + vecs = vecs / norms + return vecs + + +def _read_index_for_search(index_path: Path) -> Any: + index_file = index_path.parent / f"{index_path.stem}.index" + # Force-disable experimental disk cache when loading the index so that + # incremental benchmarks don't pick up stale top-degree bitmaps. + cfg = faiss.HNSWIndexConfig() + cfg.is_recompute = True + if hasattr(cfg, "disk_cache_ratio"): + cfg.disk_cache_ratio = 0.0 + if hasattr(cfg, "external_storage_path"): + cfg.external_storage_path = None + io_flags = getattr(faiss, "IO_FLAG_MMAP", 0) + index = faiss.read_index(str(index_file), io_flags, cfg) + # ensure recompute mode persists after reload + try: + index.is_recompute = True + except AttributeError: + pass + try: + actual_ntotal = index.hnsw.levels.size() + except AttributeError: + actual_ntotal = index.ntotal + if actual_ntotal != index.ntotal: + print( + f"[bench_update_vs_offline_search] Correcting ntotal from {index.ntotal} to {actual_ntotal}", + flush=True, + ) + index.ntotal = actual_ntotal + if getattr(index, "storage", None) is None: + if index.metric_type == faiss.METRIC_INNER_PRODUCT: + storage_index = faiss.IndexFlatIP(index.d) + else: + storage_index = faiss.IndexFlatL2(index.d) + index.storage = storage_index + index.own_fields = True + return index + + +def _append_passages_for_updates( + meta_path: Path, + start_id: int, + texts: list[str], +) -> list[str]: + """Append update passages so the embedding server can serve recompute fetches.""" + + if not texts: + return [] + + index_dir = meta_path.parent + meta_name = meta_path.name + if not meta_name.endswith(".meta.json"): + raise ValueError(f"Unexpected meta filename: {meta_path}") + index_base = meta_name[: -len(".meta.json")] + + passages_file = index_dir / f"{index_base}.passages.jsonl" + offsets_file = index_dir / f"{index_base}.passages.idx" + + if not passages_file.exists() or not offsets_file.exists(): + raise FileNotFoundError( + "Passage store missing; cannot register update passages for recompute mode." + ) + + with open(offsets_file, "rb") as f: + offset_map: dict[str, int] = pickle.load(f) + + assigned_ids: list[str] = [] + with open(passages_file, "a", encoding="utf-8") as f: + for i, text in enumerate(texts): + passage_id = str(start_id + i) + offset = f.tell() + json.dump({"id": passage_id, "text": text, "metadata": {}}, f, ensure_ascii=False) + f.write("\n") + offset_map[passage_id] = offset + assigned_ids.append(passage_id) + + with open(offsets_file, "wb") as f: + pickle.dump(offset_map, f) + + try: + with open(meta_path, encoding="utf-8") as f: + meta = json.load(f) + except json.JSONDecodeError: + meta = {} + meta["total_passages"] = len(offset_map) + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, indent=2) + + return assigned_ids + + +def _search(index: Any, q: np.ndarray, k: int) -> tuple[np.ndarray, np.ndarray]: + q = np.ascontiguousarray(q, dtype=np.float32) + distances = np.zeros((1, k), dtype=np.float32) + indices = np.zeros((1, k), dtype=np.int64) + index.search( + 1, + faiss.swig_ptr(q), + k, + faiss.swig_ptr(distances), + faiss.swig_ptr(indices), + ) + return distances[0], indices[0] + + +def _score_for_metric(dist: float, metric: str) -> float: + # Convert FAISS distance to a "higher is better" score + if metric in ("mips", "cosine"): + return float(dist) + # l2 distance (smaller better) -> negative distance as score + return -float(dist) + + +def _merge_results( + index_results: tuple[np.ndarray, np.ndarray], + offline_scores: list[tuple[int, float]], + k: int, + metric: str, +) -> list[tuple[str, float]]: + distances, indices = index_results + merged: list[tuple[str, float]] = [] + for distance, idx in zip(distances.tolist(), indices.tolist()): + merged.append((f"idx:{idx}", _score_for_metric(distance, metric))) + for j, s in offline_scores: + merged.append((f"offline:{j}", s)) + merged.sort(key=lambda x: x[1], reverse=True) + return merged[:k] + + +@dataclass +class ScenarioResult: + name: str + update_total_s: float + search_s: float + overall_s: float + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--index-path", + type=Path, + default=Path(".leann/bench/offline-vs-update.leann"), + ) + parser.add_argument( + "--initial-files", + nargs="*", + type=Path, + default=DEFAULT_INITIAL_FILES, + ) + parser.add_argument( + "--update-files", + nargs="*", + type=Path, + default=DEFAULT_UPDATE_FILES, + ) + parser.add_argument("--max-initial", type=int, default=300) + parser.add_argument("--num-updates", type=int, default=5) + parser.add_argument("--k", type=int, default=10, help="Top-k for search/merge") + parser.add_argument( + "--query", + type=str, + default="neural network", + help="Query text used for the search benchmark.", + ) + parser.add_argument("--server-port", type=int, default=5557) + parser.add_argument("--add-timeout", type=int, default=600) + parser.add_argument("--model-name", default="sentence-transformers/all-MiniLM-L6-v2") + parser.add_argument("--embedding-mode", default="sentence-transformers") + parser.add_argument( + "--distance-metric", + default="mips", + choices=["mips", "l2", "cosine"], + ) + parser.add_argument("--ef-construction", type=int, default=200) + parser.add_argument( + "--only", + choices=["A", "B", "both"], + default="both", + help="Run only Scenario A, Scenario B, or both", + ) + parser.add_argument( + "--csv-path", + type=Path, + default=Path("benchmarks/update/offline_vs_update.csv"), + help="Where to append results (CSV).", + ) + + args = parser.parse_args() + + register_project_directory(REPO_ROOT) + + # Load data + initial_paragraphs = load_chunks_from_files(args.initial_files, args.max_initial) + update_paragraphs = load_chunks_from_files(args.update_files, None) + if not update_paragraphs: + raise ValueError("No update passages loaded from --update-files") + update_paragraphs = update_paragraphs[: args.num_updates] + if len(update_paragraphs) < args.num_updates: + raise ValueError( + f"Not enough update passages ({len(update_paragraphs)}) for --num-updates={args.num_updates}" + ) + + ensure_index_dir(args.index_path) + cleanup_index_files(args.index_path) + + # Build initial index + build_initial_index( + args.index_path, + initial_paragraphs, + args.model_name, + args.embedding_mode, + args.distance_metric, + args.ef_construction, + ) + + # Prepare index object and meta + meta_path = args.index_path.parent / f"{args.index_path.name}.meta.json" + index = _read_index_for_search(args.index_path) + + # CSV setup + run_id = time.strftime("%Y%m%d-%H%M%S") + if args.csv_path: + args.csv_path.parent.mkdir(parents=True, exist_ok=True) + csv_fields = [ + "run_id", + "scenario", + "max_initial", + "num_updates", + "k", + "total_time_s", + "add_total_s", + "search_time_s", + "emb_time_s", + "makespan_s", + "model_name", + "embedding_mode", + "distance_metric", + ] + if not args.csv_path.exists() or args.csv_path.stat().st_size == 0: + with args.csv_path.open("w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=csv_fields) + writer.writeheader() + + # Debug: list existing HNSW server PIDs before starting + try: + existing = [ + p + for p in psutil.process_iter(attrs=["pid", "cmdline"]) + if any( + isinstance(arg, str) and "leann_backend_hnsw.hnsw_embedding_server" in arg + for arg in (p.info.get("cmdline") or []) + ) + ] + if existing: + print("[debug] Found existing hnsw_embedding_server processes before run:") + for p in existing: + print(f"[debug] PID={p.info['pid']} cmd={' '.join(p.info.get('cmdline') or [])}") + except Exception as _e: + pass + + add_total = 0.0 + search_after_add = 0.0 + total_seq = 0.0 + port_a = None + if args.only in ("A", "both"): + # Scenario A: sequential update then search + start_id = index.ntotal + assigned_ids = _append_passages_for_updates(meta_path, start_id, update_paragraphs) + if assigned_ids: + logger.debug( + "Registered %d update passages starting at id %s", + len(assigned_ids), + assigned_ids[0], + ) + server_manager = EmbeddingServerManager( + backend_module_name="leann_backend_hnsw.hnsw_embedding_server" + ) + ok, port = server_manager.start_server( + port=args.server_port, + model_name=args.model_name, + embedding_mode=args.embedding_mode, + passages_file=str(meta_path), + distance_metric=args.distance_metric, + ) + if not ok: + raise RuntimeError("Failed to start embedding server") + try: + # Set ZMQ port for recompute mode + if hasattr(index.hnsw, "set_zmq_port"): + index.hnsw.set_zmq_port(port) + elif hasattr(index, "set_zmq_port"): + index.set_zmq_port(port) + + # Start A overall timer BEFORE computing update embeddings + t0 = time.time() + + # Compute embeddings for updates (counted into A's overall) + t_emb0 = time.time() + upd_embs = compute_embeddings( + update_paragraphs, + args.model_name, + mode=args.embedding_mode, + is_build=False, + batch_size=16, + ) + emb_time_updates = time.time() - t_emb0 + upd_embs = np.asarray(upd_embs, dtype=np.float32) + upd_embs = _maybe_norm_cosine(upd_embs, args.distance_metric) + + # Perform sequential adds + for i in range(upd_embs.shape[0]): + t_add0 = time.time() + index.add(1, faiss.swig_ptr(upd_embs[i : i + 1])) + add_total += time.time() - t_add0 + # Don't persist index after adds to avoid contaminating Scenario B + # index_file = args.index_path.parent / f"{args.index_path.stem}.index" + # faiss.write_index(index, str(index_file)) + + # Search after updates + q_emb = compute_embeddings( + [args.query], args.model_name, mode=args.embedding_mode, is_build=False + ) + q_emb = np.asarray(q_emb, dtype=np.float32) + q_emb = _maybe_norm_cosine(q_emb, args.distance_metric) + + # Warm up search with a dummy query first + print("[DEBUG] Warming up search...") + _ = _search(index, q_emb, 1) + + t_s0 = time.time() + D_upd, I_upd = _search(index, q_emb, args.k) + search_after_add = time.time() - t_s0 + total_seq = time.time() - t0 + finally: + server_manager.stop_server() + port_a = port + + print("\n=== Scenario A: update->search (sequential) ===") + # emb_time_updates is defined only when A runs + try: + _emb_a = emb_time_updates + except NameError: + _emb_a = 0.0 + print( + f"Adds: {args.num_updates} passages; embeds={_emb_a:.3f}s; add_total={add_total:.3f}s; " + f"search={search_after_add:.3f}s; overall={total_seq:.3f}s" + ) + # CSV row for A + if args.csv_path: + row_a = { + "run_id": run_id, + "scenario": "A", + "max_initial": args.max_initial, + "num_updates": args.num_updates, + "k": args.k, + "total_time_s": round(total_seq, 6), + "add_total_s": round(add_total, 6), + "search_time_s": round(search_after_add, 6), + "emb_time_s": round(_emb_a, 6), + "makespan_s": 0.0, + "model_name": args.model_name, + "embedding_mode": args.embedding_mode, + "distance_metric": args.distance_metric, + } + with args.csv_path.open("a", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=csv_fields) + writer.writerow(row_a) + + # Verify server cleanup + try: + # short sleep to allow signal handling to finish + time.sleep(0.5) + leftovers = [ + p + for p in psutil.process_iter(attrs=["pid", "cmdline"]) + if any( + isinstance(arg, str) and "leann_backend_hnsw.hnsw_embedding_server" in arg + for arg in (p.info.get("cmdline") or []) + ) + ] + if leftovers: + print("[warn] hnsw_embedding_server process(es) still alive after A-stop:") + for p in leftovers: + print( + f"[warn] PID={p.info['pid']} cmd={' '.join(p.info.get('cmdline') or [])}" + ) + else: + print("[debug] server cleanup confirmed: no hnsw_embedding_server found") + except Exception: + pass + + # Scenario B: offline embeds + concurrent search (no graph updates) + if args.only in ("B", "both"): + # ensure a server is available for recompute search + server_manager_b = EmbeddingServerManager( + backend_module_name="leann_backend_hnsw.hnsw_embedding_server" + ) + requested_port = args.server_port if port_a is None else port_a + ok_b, port_b = server_manager_b.start_server( + port=requested_port, + model_name=args.model_name, + embedding_mode=args.embedding_mode, + passages_file=str(meta_path), + distance_metric=args.distance_metric, + ) + if not ok_b: + raise RuntimeError("Failed to start embedding server for Scenario B") + + # Wait for server to fully initialize + print("[DEBUG] Waiting 2s for embedding server to fully initialize...") + time.sleep(2) + + try: + # Read the index first + index_no_update = _read_index_for_search(args.index_path) # unchanged index + + # Then configure ZMQ port on the correct index object + if hasattr(index_no_update.hnsw, "set_zmq_port"): + index_no_update.hnsw.set_zmq_port(port_b) + elif hasattr(index_no_update, "set_zmq_port"): + index_no_update.set_zmq_port(port_b) + + # Warmup the embedding model before benchmarking (do this for both --only B and --only both) + # This ensures fair comparison as Scenario A has warmed up the model during update embeddings + logger.info("Warming up embedding model for Scenario B...") + _ = compute_embeddings( + ["warmup text"], args.model_name, mode=args.embedding_mode, is_build=False + ) + + # Prepare worker A: compute embeddings for the same N passages + emb_time = 0.0 + updates_embs_offline: np.ndarray | None = None + + def _worker_emb(): + nonlocal emb_time, updates_embs_offline + t = time.time() + updates_embs_offline = compute_embeddings( + update_paragraphs, + args.model_name, + mode=args.embedding_mode, + is_build=False, + batch_size=16, + ) + emb_time = time.time() - t + + # Pre-compute query embedding and warm up search outside of timed section. + q_vec = compute_embeddings( + [args.query], args.model_name, mode=args.embedding_mode, is_build=False + ) + q_vec = np.asarray(q_vec, dtype=np.float32) + q_vec = _maybe_norm_cosine(q_vec, args.distance_metric) + print("[DEBUG B] Warming up search...") + _ = _search(index_no_update, q_vec, 1) + + # Worker B: timed search on the warmed index + search_time = 0.0 + offline_elapsed = 0.0 + index_results: tuple[np.ndarray, np.ndarray] | None = None + + def _worker_search(): + nonlocal search_time, index_results + t = time.time() + distances, indices = _search(index_no_update, q_vec, args.k) + search_time = time.time() - t + index_results = (distances, indices) + + # Run two workers concurrently + t0 = time.time() + th1 = threading.Thread(target=_worker_emb) + th2 = threading.Thread(target=_worker_search) + th1.start() + th2.start() + th1.join() + th2.join() + offline_elapsed = time.time() - t0 + + # For mixing: compute query vs. offline update similarities (pure client-side) + offline_scores: list[tuple[int, float]] = [] + if updates_embs_offline is not None: + upd2 = np.asarray(updates_embs_offline, dtype=np.float32) + upd2 = _maybe_norm_cosine(upd2, args.distance_metric) + # For mips/cosine, score = dot; for l2, score = -||x-y||^2 + for j in range(upd2.shape[0]): + if args.distance_metric in ("mips", "cosine"): + s = float(np.dot(q_vec[0], upd2[j])) + else: + diff = q_vec[0] - upd2[j] + s = -float(np.dot(diff, diff)) + offline_scores.append((j, s)) + + merged_topk = ( + _merge_results(index_results, offline_scores, args.k, args.distance_metric) + if index_results + else [] + ) + + print("\n=== Scenario B: offline embeds + concurrent search (no add) ===") + print( + f"embeddings({args.num_updates})={emb_time:.3f}s; search={search_time:.3f}s; makespanβ‰ˆ{offline_elapsed:.3f}s (β‰ˆmax)" + ) + if merged_topk: + preview = ", ".join([f"{lab}:{score:.3f}" for lab, score in merged_topk[:5]]) + print(f"Merged top-5 preview: {preview}") + # CSV row for B + if args.csv_path: + row_b = { + "run_id": run_id, + "scenario": "B", + "max_initial": args.max_initial, + "num_updates": args.num_updates, + "k": args.k, + "total_time_s": 0.0, + "add_total_s": 0.0, + "search_time_s": round(search_time, 6), + "emb_time_s": round(emb_time, 6), + "makespan_s": round(offline_elapsed, 6), + "model_name": args.model_name, + "embedding_mode": args.embedding_mode, + "distance_metric": args.distance_metric, + } + with args.csv_path.open("a", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=csv_fields) + writer.writerow(row_b) + + finally: + server_manager_b.stop_server() + + # Summary + print("\n=== Summary ===") + msg_a = ( + f"A: seq-add+search overall={total_seq:.3f}s (adds={add_total:.3f}s, search={search_after_add:.3f}s)" + if args.only in ("A", "both") + else "A: skipped" + ) + msg_b = ( + f"B: offline+concurrent overallβ‰ˆ{offline_elapsed:.3f}s (emb={emb_time:.3f}s, search={search_time:.3f}s)" + if args.only in ("B", "both") + else "B: skipped" + ) + print(msg_a + "\n" + msg_b) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/update/plot_bench_results.py b/benchmarks/update/plot_bench_results.py new file mode 100644 index 0000000..3c9e56f --- /dev/null +++ b/benchmarks/update/plot_bench_results.py @@ -0,0 +1,645 @@ +#!/usr/bin/env python3 +""" +Plot latency bars from the benchmark CSV produced by +benchmarks/update/bench_hnsw_rng_recompute.py. + +If you also provide an offline_vs_update.csv via --csv-right +(from benchmarks/update/bench_update_vs_offline_search.py), this script will +output a side-by-side figure: +- Left: ms/passage bars (four RNG scenarios). +- Right: seconds bars (Scenario A seq add+search vs Scenario B offline+search). + +Usage: + uv run python benchmarks/update/plot_bench_results.py \ + --csv benchmarks/update/bench_results.csv \ + --out benchmarks/update/bench_latency_from_csv.png + +The script selects the latest run_id in the CSV and plots four bars for +the default scenarios: + - baseline + - no_cache_baseline + - disable_forward_rng + - disable_forward_and_reverse_rng + +If multiple rows exist per scenario for that run_id, the script averages +their latency_ms_per_passage values. +""" + +import argparse +import csv +from collections import defaultdict +from pathlib import Path + +DEFAULT_SCENARIOS = [ + "no_cache_baseline", + "baseline", + "disable_forward_rng", + "disable_forward_and_reverse_rng", +] + +SCENARIO_LABELS = { + "baseline": "+ Cache", + "no_cache_baseline": "Naive \n Recompute", + "disable_forward_rng": "+ w/o \n Fwd RNG", + "disable_forward_and_reverse_rng": "+ w/o \n Bwd RNG", +} + +# Paper-style colors and hatches for scenarios +SCENARIO_STYLES = { + "no_cache_baseline": {"edgecolor": "dimgrey", "hatch": "/////"}, + "baseline": {"edgecolor": "#63B8B6", "hatch": "xxxxx"}, + "disable_forward_rng": {"edgecolor": "green", "hatch": "....."}, + "disable_forward_and_reverse_rng": {"edgecolor": "tomato", "hatch": "\\\\\\\\\\"}, +} + + +def load_latest_run(csv_path: Path): + rows = [] + with csv_path.open("r", encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + rows.append(row) + if not rows: + raise SystemExit("CSV is empty: no rows to plot") + # Choose latest run_id lexicographically (YYYYMMDD-HHMMSS) + run_ids = [r.get("run_id", "") for r in rows] + latest = max(run_ids) + latest_rows = [r for r in rows if r.get("run_id", "") == latest] + if not latest_rows: + # Fallback: take last 4 rows + latest_rows = rows[-4:] + latest = latest_rows[-1].get("run_id", "unknown") + return latest, latest_rows + + +def aggregate_latency(rows): + acc = defaultdict(list) + for r in rows: + sc = r.get("scenario", "") + try: + val = float(r.get("latency_ms_per_passage", "nan")) + except ValueError: + continue + acc[sc].append(val) + avg = {k: (sum(v) / len(v) if v else 0.0) for k, v in acc.items()} + return avg + + +def _auto_cap(values: list[float]) -> float | None: + if not values: + return None + sorted_vals = sorted(values, reverse=True) + if len(sorted_vals) < 2: + return None + max_v, second = sorted_vals[0], sorted_vals[1] + if second <= 0: + return None + # If the tallest bar dwarfs the second by 2.5x+, cap near the second + if max_v >= 2.5 * second: + return second * 1.1 + return None + + +def _add_break_marker(ax, y, rel_x0=0.02, rel_x1=0.98, size=0.02): + # Draw small diagonal ticks near left/right to signal cap + x0, x1 = rel_x0, rel_x1 + ax.plot([x0 - size, x0 + size], [y + size, y - size], transform=ax.transAxes, color="k", lw=1) + ax.plot([x1 - size, x1 + size], [y + size, y - size], transform=ax.transAxes, color="k", lw=1) + + +def _fmt_ms(v: float) -> str: + if v >= 1000: + return f"{v / 1000:.1f}k" + return f"{v:.1f}" + + +def main(): + # Set LaTeX style for paper figures (matching paper_fig.py) + import matplotlib.pyplot as plt + + plt.rcParams["font.family"] = "Helvetica" + plt.rcParams["ytick.direction"] = "in" + plt.rcParams["hatch.linewidth"] = 1.5 + plt.rcParams["font.weight"] = "bold" + plt.rcParams["axes.labelweight"] = "bold" + plt.rcParams["text.usetex"] = True + + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument( + "--csv", + type=Path, + default=Path("benchmarks/update/bench_results.csv"), + help="Path to results CSV (defaults to bench_results.csv)", + ) + ap.add_argument( + "--out", + type=Path, + default=Path("add_ablation.pdf"), + help="Output image path", + ) + ap.add_argument( + "--csv-right", + type=Path, + default=Path("benchmarks/update/offline_vs_update.csv"), + help="Optional: offline_vs_update.csv to render right subplot (A vs B)", + ) + ap.add_argument( + "--cap-y", + type=float, + default=None, + help="Cap Y-axis at this ms value; bars above are hatched and annotated.", + ) + ap.add_argument( + "--no-auto-cap", + action="store_true", + help="Disable auto-cap heuristic when --cap-y is not provided.", + ) + ap.add_argument( + "--broken-y", + action="store_true", + default=True, + help="Use a broken Y-axis (two stacked axes with a gap). Overrides --cap-y unless both provided.", + ) + ap.add_argument( + "--lower-cap-y", + type=float, + default=None, + help="Lower axes upper bound for broken Y (ms). Default = 1.1x second-highest.", + ) + ap.add_argument( + "--upper-start-y", + type=float, + default=None, + help="Upper axes lower bound for broken Y (ms). Default = 1.2x second-highest.", + ) + args = ap.parse_args() + + latest_run, latest_rows = load_latest_run(args.csv) + avg = aggregate_latency(latest_rows) + + try: + import matplotlib.pyplot as plt + except Exception as e: + raise SystemExit(f"matplotlib not available: {e}") + + scenarios = DEFAULT_SCENARIOS + values = [avg.get(name, 0.0) for name in scenarios] + labels = [SCENARIO_LABELS.get(name, name) for name in scenarios] + colors = ["#4e79a7", "#f28e2c", "#e15759", "#76b7b2"] + + # If right CSV is provided, build side-by-side figure + if args.csv_right is not None: + try: + right_rows_all = [] + with args.csv_right.open("r", encoding="utf-8") as f: + rreader = csv.DictReader(f) + right_rows_all = list(rreader) + if right_rows_all: + r_latest = max(r.get("run_id", "") for r in right_rows_all) + right_rows = [r for r in right_rows_all if r.get("run_id", "") == r_latest] + else: + r_latest = None + right_rows = [] + except Exception: + r_latest = None + right_rows = [] + + a_total = 0.0 + b_makespan = 0.0 + for r in right_rows: + sc = (r.get("scenario", "") or "").strip().upper() + if sc == "A": + try: + a_total = float(r.get("total_time_s", 0.0)) + except Exception: + pass + elif sc == "B": + try: + b_makespan = float(r.get("makespan_s", 0.0)) + except Exception: + pass + + import matplotlib.pyplot as plt + from matplotlib import gridspec + + # Left subplot (reuse current style, with optional cap) + cap = args.cap_y + if cap is None and not args.no_auto_cap: + cap = _auto_cap(values) + x = list(range(len(labels))) + + if args.broken_y: + # Use broken axis for left subplot + # Auto-adjust width ratios: left has 4 bars, right has 2 bars + fig = plt.figure(figsize=(4.8, 1.8)) # Scaled down to 80% + gs = gridspec.GridSpec( + 2, 2, height_ratios=[1, 3], width_ratios=[1.5, 1], hspace=0.08, wspace=0.35 + ) + ax_left_top = fig.add_subplot(gs[0, 0]) + ax_left_bottom = fig.add_subplot(gs[1, 0], sharex=ax_left_top) + ax_right = fig.add_subplot(gs[:, 1]) + + # Determine break points + s = sorted(values, reverse=True) + second = s[1] if len(s) >= 2 else (s[0] if s else 0.0) + lower_cap = ( + args.lower_cap_y if args.lower_cap_y is not None else second * 1.4 + ) # Increased to show more range + upper_start = ( + args.upper_start_y + if args.upper_start_y is not None + else max(second * 1.5, lower_cap * 1.02) + ) + ymax = ( + max(values) * 1.90 if values else 1.0 + ) # Increase headroom to 1.90 for text label and tick range + + # Draw bars on both axes + ax_left_bottom.bar(x, values, color=colors[: len(labels)], width=0.8) + ax_left_top.bar(x, values, color=colors[: len(labels)], width=0.8) + + # Set limits + ax_left_bottom.set_ylim(0, lower_cap) + ax_left_top.set_ylim(upper_start, ymax) + + # Annotate values (convert ms to s) + values_s = [v / 1000.0 for v in values] + lower_cap_s = lower_cap / 1000.0 + upper_start_s = upper_start / 1000.0 + ymax_s = ymax / 1000.0 + + ax_left_bottom.set_ylim(0, lower_cap_s) + ax_left_top.set_ylim(upper_start_s, ymax_s) + + # Redraw bars with s values (paper style: white fill + colored edge + hatch) + ax_left_bottom.clear() + ax_left_top.clear() + bar_width = 0.50 # Reduced for wider spacing between bars + for i, (scenario_name, v) in enumerate(zip(scenarios, values_s)): + style = SCENARIO_STYLES.get(scenario_name, {"edgecolor": "black", "hatch": ""}) + # Draw in bottom axis for all bars + ax_left_bottom.bar( + i, + v, + width=bar_width, + color="white", + edgecolor=style["edgecolor"], + hatch=style["hatch"], + linewidth=1.2, + ) + # Only draw in top axis if the bar is tall enough to reach the upper range + if v > upper_start_s: + ax_left_top.bar( + i, + v, + width=bar_width, + color="white", + edgecolor=style["edgecolor"], + hatch=style["hatch"], + linewidth=1.2, + ) + ax_left_bottom.set_ylim(0, lower_cap_s) + ax_left_top.set_ylim(upper_start_s, ymax_s) + + for i, v in enumerate(values_s): + if v <= lower_cap_s: + ax_left_bottom.text( + i, + v + lower_cap_s * 0.02, + f"{v:.2f}", + ha="center", + va="bottom", + fontsize=8, + fontweight="bold", + ) + else: + ax_left_top.text( + i, + v + (ymax_s - upper_start_s) * 0.02, + f"{v:.2f}", + ha="center", + va="bottom", + fontsize=8, + fontweight="bold", + ) + + # Hide spines between axes + ax_left_top.spines["bottom"].set_visible(False) + ax_left_bottom.spines["top"].set_visible(False) + ax_left_top.tick_params( + labeltop=False, labelbottom=False, bottom=False + ) # Hide tick marks + ax_left_bottom.xaxis.tick_bottom() + ax_left_bottom.tick_params(top=False) # Hide top tick marks + + # Draw break marks (matching paper_fig.py style) + d = 0.015 + kwargs = { + "transform": ax_left_top.transAxes, + "color": "k", + "clip_on": False, + "linewidth": 0.8, + "zorder": 10, + } + ax_left_top.plot((-d, +d), (-d, +d), **kwargs) + ax_left_top.plot((1 - d, 1 + d), (-d, +d), **kwargs) + kwargs.update({"transform": ax_left_bottom.transAxes}) + ax_left_bottom.plot((-d, +d), (1 - d, 1 + d), **kwargs) + ax_left_bottom.plot((1 - d, 1 + d), (1 - d, 1 + d), **kwargs) + + ax_left_bottom.set_xticks(x) + ax_left_bottom.set_xticklabels(labels, rotation=0, fontsize=7) + # Don't set ylabel here - will use fig.text for alignment + ax_left_bottom.tick_params(axis="y", labelsize=10) + ax_left_top.tick_params(axis="y", labelsize=10) + # Add subtle grid for better readability + ax_left_bottom.grid(axis="y", alpha=0.3, linestyle="--", linewidth=0.5) + ax_left_top.grid(axis="y", alpha=0.3, linestyle="--", linewidth=0.5) + ax_left_top.set_title("Single Add Operation", fontsize=11, pad=10, fontweight="bold") + + # Set x-axis limits to match bar width with right subplot + ax_left_bottom.set_xlim(-0.6, 3.6) + ax_left_top.set_xlim(-0.6, 3.6) + + ax_left = ax_left_bottom # for compatibility + else: + # Regular side-by-side layout + fig, (ax_left, ax_right) = plt.subplots(1, 2, figsize=(8.4, 3.15)) + + if cap is not None: + show_vals = [min(v, cap) for v in values] + bars = ax_left.bar(x, show_vals, color=colors[: len(labels)], width=0.8) + for i, (val, show) in enumerate(zip(values, show_vals)): + if val > cap: + bars[i].set_hatch("//") + ax_left.text( + i, cap * 1.02, _fmt_ms(val), ha="center", va="bottom", fontsize=9 + ) + else: + ax_left.text( + i, + show + max(1.0, 0.01 * (cap or show)), + _fmt_ms(val), + ha="center", + va="bottom", + fontsize=9, + ) + ax_left.set_ylim(0, cap * 1.10) + _add_break_marker(ax_left, y=0.98) + ax_left.set_xticks(x) + ax_left.set_xticklabels(labels, rotation=0, fontsize=10) + else: + ax_left.bar(x, values, color=colors[: len(labels)], width=0.8) + for i, v in enumerate(values): + ax_left.text(i, v + 1.0, _fmt_ms(v), ha="center", va="bottom", fontsize=9) + ax_left.set_xticks(x) + ax_left.set_xticklabels(labels, rotation=0, fontsize=10) + ax_left.set_ylabel("Latency (ms per passage)") + max_initial = latest_rows[0].get("max_initial", "?") + max_updates = latest_rows[0].get("max_updates", "?") + ax_left.set_title( + f"HNSW RNG (run {latest_run}) | init={max_initial}, upd={max_updates}" + ) + + # Right subplot (A vs B, seconds) - paper style + r_labels = ["Sequential", "Delayed \n Add+Search"] + r_values = [a_total or 0.0, b_makespan or 0.0] + r_styles = [ + {"edgecolor": "#59a14f", "hatch": "xxxxx"}, + {"edgecolor": "#edc948", "hatch": "/////"}, + ] + # 2 bars, centered with proper spacing + xr = [0, 1] + bar_width = 0.50 # Reduced for wider spacing between bars + for i, (v, style) in enumerate(zip(r_values, r_styles)): + ax_right.bar( + xr[i], + v, + width=bar_width, + color="white", + edgecolor=style["edgecolor"], + hatch=style["hatch"], + linewidth=1.2, + ) + for i, v in enumerate(r_values): + max_v = max(r_values) if r_values else 1.0 + offset = max(0.0002, 0.02 * max_v) + ax_right.text( + xr[i], + v + offset, + f"{v:.2f}", + ha="center", + va="bottom", + fontsize=8, + fontweight="bold", + ) + ax_right.set_xticks(xr) + ax_right.set_xticklabels(r_labels, rotation=0, fontsize=7) + # Don't set ylabel here - will use fig.text for alignment + ax_right.tick_params(axis="y", labelsize=10) + # Add subtle grid for better readability + ax_right.grid(axis="y", alpha=0.3, linestyle="--", linewidth=0.5) + ax_right.set_title("Batched Add Operation", fontsize=11, pad=10, fontweight="bold") + + # Set x-axis limits to match left subplot's bar width visually + # Accounting for width_ratios=[1.5, 1]: + # Left: 4 bars, xlim(-0.6, 3.6), range=4.2, physical_width=1.5*unit + # bar_width_visual = 0.72 * (1.5*unit / 4.2) + # Right: 2 bars, need same visual width + # 0.72 * (1.0*unit / range_right) = 0.72 * (1.5*unit / 4.2) + # range_right = 4.2 / 1.5 = 2.8 + # For bars at 0, 1: padding = (2.8 - 1) / 2 = 0.9 + ax_right.set_xlim(-0.9, 1.9) + + # Set y-axis limit with headroom for text labels + if r_values: + max_v = max(r_values) + ax_right.set_ylim(0, max_v * 1.15) + + # Format y-axis to avoid scientific notation + ax_right.ticklabel_format(style="plain", axis="y") + + plt.tight_layout() + + # Add aligned ylabels using fig.text (after tight_layout) + # Get the vertical center of the entire figure + fig_center_y = 0.5 + # Left ylabel - closer to left plot + left_x = 0.05 + fig.text( + left_x, + fig_center_y, + "Latency (s)", + va="center", + rotation="vertical", + fontsize=11, + fontweight="bold", + ) + # Right ylabel - closer to right plot + right_bbox = ax_right.get_position() + right_x = right_bbox.x0 - 0.07 + fig.text( + right_x, + fig_center_y, + "Latency (s)", + va="center", + rotation="vertical", + fontsize=11, + fontweight="bold", + ) + + plt.savefig(args.out, bbox_inches="tight", pad_inches=0.05) + # Also save PDF for paper + pdf_out = args.out.with_suffix(".pdf") + plt.savefig(pdf_out, bbox_inches="tight", pad_inches=0.05) + print(f"Saved: {args.out}") + print(f"Saved: {pdf_out}") + return + + # Broken-Y mode + if args.broken_y: + import matplotlib.pyplot as plt + + fig, (ax_top, ax_bottom) = plt.subplots( + 2, + 1, + sharex=True, + figsize=(7.5, 6.75), + gridspec_kw={"height_ratios": [1, 3], "hspace": 0.08}, + ) + + # Determine default breaks from second-highest + s = sorted(values, reverse=True) + second = s[1] if len(s) >= 2 else (s[0] if s else 0.0) + lower_cap = args.lower_cap_y if args.lower_cap_y is not None else second * 1.1 + upper_start = ( + args.upper_start_y + if args.upper_start_y is not None + else max(second * 1.2, lower_cap * 1.02) + ) + ymax = max(values) * 1.10 if values else 1.0 + + x = list(range(len(labels))) + ax_bottom.bar(x, values, color=colors[: len(labels)], width=0.8) + ax_top.bar(x, values, color=colors[: len(labels)], width=0.8) + + # Limits + ax_bottom.set_ylim(0, lower_cap) + ax_top.set_ylim(upper_start, ymax) + + # Annotate values + for i, v in enumerate(values): + if v <= lower_cap: + ax_bottom.text( + i, v + lower_cap * 0.02, _fmt_ms(v), ha="center", va="bottom", fontsize=9 + ) + else: + ax_top.text(i, v, _fmt_ms(v), ha="center", va="bottom", fontsize=9) + + # Hide spines between axes and draw diagonal break marks + ax_top.spines["bottom"].set_visible(False) + ax_bottom.spines["top"].set_visible(False) + ax_top.tick_params(labeltop=False) # don't put tick labels at the top + ax_bottom.xaxis.tick_bottom() + + # Diagonal lines at the break (matching paper_fig.py style) + d = 0.015 + kwargs = { + "transform": ax_top.transAxes, + "color": "k", + "clip_on": False, + "linewidth": 0.8, + "zorder": 10, + } + ax_top.plot((-d, +d), (-d, +d), **kwargs) # top-left diagonal + ax_top.plot((1 - d, 1 + d), (-d, +d), **kwargs) # top-right diagonal + kwargs.update({"transform": ax_bottom.transAxes}) + ax_bottom.plot((-d, +d), (1 - d, 1 + d), **kwargs) # bottom-left diagonal + ax_bottom.plot((1 - d, 1 + d), (1 - d, 1 + d), **kwargs) # bottom-right diagonal + + ax_bottom.set_xticks(x) + ax_bottom.set_xticklabels(labels, rotation=0, fontsize=10) + ax = ax_bottom # for labeling below + else: + cap = args.cap_y + if cap is None and not args.no_auto_cap: + cap = _auto_cap(values) + + plt.figure(figsize=(5.4, 3.15)) + ax = plt.gca() + + if cap is not None: + show_vals = [min(v, cap) for v in values] + bars = [] + for i, (_label, val, show) in enumerate(zip(labels, values, show_vals)): + bar = ax.bar(i, show, color=colors[i], width=0.8) + bars.append(bar[0]) + # Hatch and annotate when capped + if val > cap: + bars[-1].set_hatch("//") + ax.text(i, cap * 1.02, f"{_fmt_ms(val)}", ha="center", va="bottom", fontsize=9) + else: + ax.text( + i, + show + max(1.0, 0.01 * (cap or show)), + f"{_fmt_ms(val)}", + ha="center", + va="bottom", + fontsize=9, + ) + ax.set_ylim(0, cap * 1.10) + _add_break_marker(ax, y=0.98) + ax.legend([bars[1]], ["capped"], fontsize=8, frameon=False, loc="upper right") if any( + v > cap for v in values + ) else None + ax.set_xticks(range(len(labels))) + ax.set_xticklabels(labels, fontsize=11, fontweight="bold") + else: + ax.bar(labels, values, color=colors[: len(labels)]) + for idx, val in enumerate(values): + ax.text( + idx, + val + 1.0, + f"{_fmt_ms(val)}", + ha="center", + va="bottom", + fontsize=10, + fontweight="bold", + ) + ax.set_xticklabels(labels, fontsize=11, fontweight="bold") + # Try to extract some context for title + max_initial = latest_rows[0].get("max_initial", "?") + max_updates = latest_rows[0].get("max_updates", "?") + + if args.broken_y: + fig.text( + 0.02, + 0.5, + "Latency (s)", + va="center", + rotation="vertical", + fontsize=11, + fontweight="bold", + ) + fig.suptitle( + "Add Operation Latency", + fontsize=11, + y=0.98, + fontweight="bold", + ) + plt.tight_layout(rect=(0.03, 0.04, 1, 0.96)) + else: + plt.ylabel("Latency (s)", fontsize=11, fontweight="bold") + plt.title("Add Operation Latency", fontsize=11, fontweight="bold") + plt.tight_layout() + + plt.savefig(args.out, bbox_inches="tight", pad_inches=0.05) + # Also save PDF for paper + pdf_out = args.out.with_suffix(".pdf") + plt.savefig(pdf_out, bbox_inches="tight", pad_inches=0.05) + print(f"Saved: {args.out}") + print(f"Saved: {pdf_out}") + + +if __name__ == "__main__": + main() diff --git a/data/2501.14312v1 (1).pdf b/data/2501.14312v1 (1).pdf new file mode 100644 index 0000000..230732b Binary files /dev/null and b/data/2501.14312v1 (1).pdf differ diff --git a/data/2506.08276v1.pdf b/data/2506.08276v1.pdf new file mode 100644 index 0000000..2756eef --- /dev/null +++ b/data/2506.08276v1.pdf @@ -0,0 +1,7905 @@ +%PDF-1.5 +%Ώχ’ώ +1 0 obj +<< /Lang (en) /Metadata 3 0 R /Names 4 0 R /OpenAction 5 0 R /Outlines 6 0 R /PageMode /UseOutlines /Pages 7 0 R /Type /Catalog /ViewerPreferences << /DisplayDocTitle true >> >> +endobj +2 0 obj +<< /Author (Yichuan Wang; Shu Liu; Zhifei Li; Yongji Wu; Ziming Mao; Yilong Zhao; Xiao Yan; Zhiying Xu; Yang Zhou; Ion Stoica; Sewon Min; Matei Zaharia; Joseph E. Gonzalez) /CreationDate (D:20250611152430+00'00') /Creator (arXiv GenPDF \(tex2pdf:\)) /DOI (https://doi.org/10.48550/arXiv.2506.08276) /Keywords () /License (http://arxiv.org/licenses/nonexclusive-distrib/1.0/) /ModDate (D:20250611152430+00'00') /PTEX.Fullbanner (This is pdfTeX, Version 3.141592653-2.6-1.40.25 \(TeX Live 2023\) kpathsea version 6.3.5) /Producer (pikepdf 8.15.1) /Title (LEANN: A Low-Storage Vector Index) /Trapped /False /arXivID (https://arxiv.org/abs/2506.08276v1) >> +endobj +3 0 obj +<< /Subtype /XML /Type /Metadata /Length 13436 >> +stream + + + + + + + + Adobe PDF Schema + pdf + http://ns.adobe.com/pdf/1.3/ + + + + Trapped + Text + internal + Indication if the document has been modified to include trapping information + + + + + + XMP Media Management Schema + xmpMM + http://ns.adobe.com/xap/1.0/mm/ + + + + DocumentID + URI + internal + UUID based identifier for all versions and renditions of a document + + + InstanceID + URI + internal + UUID based identifier for specific incarnation of a document + + + VersionID + Text + internal + Document version identifier + + + RenditionClass + RenditionClass + internal + The manner in which a document is rendered + + + + + + PRISM Basic Metadata + prism + http://prismstandard.org/namespaces/basic/3.0/ + + + + complianceProfile + Text + internal + PRISM specification compliance profile to which this document adheres + + + publicationName + Text + external + Publication name + + + aggregationType + Text + external + Publication type + + + bookEdition + Text + external + Edition of the book in which the document was published + + + volume + Text + external + Publication volume number + + + number + Text + external + Publication issue number within a volume + + + pageRange + Text + external + Page range for the document within the print version of its publication + + + issn + Text + external + ISSN for the printed publication in which the document was published + + + eIssn + Text + external + ISSN for the electronic publication in which the document was published + + + isbn + Text + external + ISBN for the publication in which the document was published + + + doi + Text + external + Digital Object Identifier for the document + + + url + URL + external + URL at which the document can be found + + + byteCount + Integer + internal + Approximate file size in octets + + + pageCount + Integer + internal + Number of pages in the print version of the document + + + subtitle + Text + external + Document's subtitle + + + + + + + pikepdf 8.15.1 + + 1.5 + application/pdf + + LEANN: A Low-Storage Vector Index + arXiv + + + 2025-06-11T15:24:30Z + + + + + Text + + + + Yichuan WangShu LiuZhifei LiYongji WuZiming MaoYilong ZhaoXiao YanZhiying XuYang ZhouIon StoicaSewon MinMatei ZahariaJoseph E. Gonzalez + main.tex + + + en + + + https://arxiv.org/abs/2506.08276v1 + 2025-06-11T15:24:30Z + 2025-06-11T15:24:30Z + 2025-06-11T15:24:36.250178+00:00 + arXiv GenPDF (tex2pdf:) + uuid:75fd75f2-b182-4bbb-ac9b-620a88d7aeae + uuid:8d18d13e-fdc9-4541-91dc-efd13959bb18 + 1 + default + three + Proceedings of Make sure to enter the correct conference title from your rights confirmation email (Conference acronym 'XX) + book + 1 + 1 + XXXXXXX.XXXXXXX + 15 + 15 + + http://arxiv.org/licenses/nonexclusive-distrib/1.0/cs.DBcs.LG + + + + +endstream +endobj +4 0 obj +<< /Dests 8 0 R >> +endobj +5 0 obj +<< /D [ 9 0 R /Fit ] /S /GoTo >> +endobj +6 0 obj +<< /Count 11 /First 10 0 R /Last 11 0 R /Type /Outlines >> +endobj +7 0 obj +<< /Count 15 /Kids [ 12 0 R 13 0 R 14 0 R ] /Type /Pages >> +endobj +8 0 obj +<< /Kids [ 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R ] /Limits [ (ALG@line.1) (table.caption.9) ] >> +endobj +9 0 obj +<< /Annots [ 20 0 R 21 0 R 22 0 R 23 0 R 24 0 R 25 0 R 26 0 R 27 0 R 28 0 R 29 0 R 30 0 R 31 0 R 32 0 R 33 0 R 34 0 R 35 0 R 36 0 R ] /Contents [ 37 0 R 38 0 R ] /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources 39 0 R /Type /Page >> +endobj +10 0 obj +<< /A 40 0 R /Next 41 0 R /Parent 6 0 R /Title 42 0 R >> +endobj +11 0 obj +<< /A 43 0 R /Parent 6 0 R /Prev 44 0 R /Title 45 0 R >> +endobj +12 0 obj +<< /Count 6 /Kids [ 9 0 R 46 0 R 47 0 R 48 0 R 49 0 R 50 0 R ] /Parent 7 0 R /Type /Pages >> +endobj +13 0 obj +<< /Count 6 /Kids [ 51 0 R 52 0 R 53 0 R 54 0 R 55 0 R 56 0 R ] /Parent 7 0 R /Type /Pages >> +endobj +14 0 obj +<< /Count 3 /Kids [ 57 0 R 58 0 R 59 0 R ] /Parent 7 0 R /Type /Pages >> +endobj +15 0 obj +<< /Kids [ 60 0 R 61 0 R 62 0 R 63 0 R 64 0 R 65 0 R ] /Limits [ (ALG@line.1) (cite.asai2023self) ] >> +endobj +16 0 obj +<< /Kids [ 66 0 R 67 0 R 68 0 R 69 0 R 70 0 R 71 0 R ] /Limits [ (cite.aumuller2020ann) (cite.munyampirwa2024down) ] >> +endobj +17 0 obj +<< /Kids [ 72 0 R 73 0 R 74 0 R 75 0 R 76 0 R 77 0 R ] /Limits [ (cite.nsg) (equation.5.1) ] >> +endobj +18 0 obj +<< /Kids [ 78 0 R 79 0 R 80 0 R 81 0 R 82 0 R 83 0 R ] /Limits [ (figure.caption.10) (section.5) ] >> +endobj +19 0 obj +<< /Kids [ 84 0 R 85 0 R 86 0 R ] /Limits [ (section.6) (table.caption.9) ] >> +endobj +20 0 obj +<< /A << /D (Hfootnote.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 53.004 360.913 55.494 366.522 ] /Subtype /Link /Type /Annot >> +endobj +21 0 obj +<< /A << /D (cite.izacard2021unsupervised) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 188.421 321.946 199.679 330.2 ] /Subtype /Link /Type /Annot >> +endobj +22 0 obj +<< /A << /D (cite.lin2022pretrained) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 202.564 321.946 213.822 330.2 ] /Subtype /Link /Type /Annot >> +endobj +23 0 obj +<< /A << /D (cite.karpukhin2020dense) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 150.818 298.036 162.075 306.29 ] /Subtype /Link /Type /Annot >> +endobj +24 0 obj +<< /A << /D (cite.zamani2023conversational) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 165.19 298.036 176.448 306.29 ] /Subtype /Link /Type /Annot >> +endobj +25 0 obj +<< /A << /D (cite.craswell2020overview) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 220.739 274.215 231.997 282.379 ] /Subtype /Link /Type /Annot >> +endobj +26 0 obj +<< /A << /D (cite.zhang2018visual) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 234.667 274.125 245.925 282.379 ] /Subtype /Link /Type /Annot >> +endobj +27 0 obj +<< /A << /D (cite.he2019streaming) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 201.078 190.529 212.336 198.693 ] /Subtype /Link /Type /Annot >> +endobj +28 0 obj +<< /A << /D (cite.work-in-progress) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 215.026 190.439 226.284 198.693 ] /Subtype /Link /Type /Annot >> +endobj +29 0 obj +<< /A << /D (cite.wang2024mememo) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 228.973 190.439 240.231 198.693 ] /Subtype /Link /Type /Annot >> +endobj +30 0 obj +<< /A << /D (cite.yin2024devicers) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 242.921 190.439 254.179 198.693 ] /Subtype /Link /Type /Annot >> +endobj +31 0 obj +<< /A << /D (cite.shao2024scaling) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 75.586 154.574 86.844 162.828 ] /Subtype /Link /Type /Annot >> +endobj +32 0 obj +<< /A << /D (cite.wang2021comprehensive_survey) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 340.008 543.703 351.265 551.957 ] /Subtype /Link /Type /Annot >> +endobj +33 0 obj +<< /A << /D (cite.pq) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 394.996 507.837 406.254 516.091 ] /Subtype /Link /Type /Annot >> +endobj +34 0 obj +<< /A << /D (cite.hnsw) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 555.225 436.106 566.483 444.36 ] /Subtype /Link /Type /Annot >> +endobj +35 0 obj +<< /A << /D (cite.shao2024scaling) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 434.339 113.317 445.596 121.571 ] /Subtype /Link /Type /Annot >> +endobj +36 0 obj +<< /A << /S /URI /URI (https://arxiv.org/abs/2506.08276v1) >> /BS << /W 0 >> /NM (fitz-L0) /Rect [ 12 227.68 32 564.32 ] /Subtype /Link >> +endobj +37 0 obj +<< /Filter /FlateDecode /Length 139 >> +stream +xΪEˆM +1 …χ9E.Π1MštΔ… ‚;‡ξΔEΥ*.ͺ¨χ;*Θϋα{°L@θΡy€ŽΎθ‘9vΦ£ΖTa–΅Όάx―ω†L˜ΞΨιΟγϊΟΟ ξζζ#koC4ΙΒ’Bb\ZχmE±φ{&&=X›\B¬§ι‘)δ¨V³LΡΕ>m`•` o6£& +endstream +endobj +38 0 obj +<< /Filter /FlateDecode /Length 5551 >> +stream +xΪ₯[[“£Θ±~Ÿ_‘G¨O΄ΚΤ…‡gχΜ^lοϊ2½gνέ™pΠ-α 0γž_2+³ @h<=ŽŽ ¨K’••ωεEΡjΏŠV_ΏˆfΧ/ξ^όϊ+™₯+ kUΌΊ{XI‹D’(*V«»έκηυŸ^½όώϋίάlT­_εOΝM²~Ώyέ5η|_PΫέ€Ρξ“υš©νΫzW@ΟίΌ½ϋΓ―ΏR‘\I)²g~Xmbχ«JD’RZμεφΠη5 ρΖFλΌήϋαf•ŠΔ}0:‘"–ve„64ςΙχ²A/#€‚uή(ϋ>YΠG‰H&Ύ“\$5±Υ«ΝΈΨ-ψϊΠ3CΚ~ij£E”&‘ΛΣ'ρΒτ?Κ‡’τ+,.ΐυΩ όγ&‘λ¦ή«τ Wfέ/±;ND€ΣOa·ό~ۏσϋ*CΚͺqpχίεΝμXeŸΟ‘ςΨψ~:\]"Ž>Ÿι/aZfΏ“οō̈́V‘δ¨O]ΊAJž‘•pΖ Ÿ-§§}_ήζXd‰ –}Ezi§΅H­}ΦNλgJ&}ή‰fρ|Εp+wbF«4Ο’Υ<‹ΦoΦW ΛmΎH¨EΆΘΌF-ϊώ&]ϋ•Ύ+₯ΖF"‰νg/σ]ήyuσS~ΘΟεβ·$pΚ3σI‹˜…EώΠ΄Ει@χ―]Ώnκω±ψ°΄\–Š‹^m΄΅"Ϋ5pOˆΔβΧκŒϊύπ%-ϋEq~,ŽΘΫ§ΕΥ"‰τ*¨>q/ψ揋§Ϊ +i’pFύ‰3ΎΌz«όC3˜Ζ¬ν½°·2^Ώ+·E»΄”©°Z† ›gςκσw₯›ϋGˆ »k‘’a—`όά·έ9ίv£¬d"³Κβ€HΐόI8(+ [ƒίU0μUί(Xμv Ύ6χyKOH˜^·E~ΎΙΦΫ=‘pUλχε8>Ρ} P벦k~:αΈveSσ˜Ά‡YPΌfERΐ?”C_ Ό?Γi$΄ΣTUQοά<τ*―wAŸξ\’„½Λ›ΌίCΧΞƒ]φE]œƒ±o’8ϊΫΛmΦ_Γ­ δΔi&¬aώόh€YαcAeβ!‹’uw(˜@ΰ4ρZszά#Ώ‚#kM»’rT»± ΅΅ύι„“7ηn˜΄ε·ω)Ώ/GžQτ%‰°³«μΚW7΄β;$ +Α‘IΧnςβά6u~€ΐĜޡ!™Α;zylά—ζG·«8_M}Δt’/θΕ7 kΛb\1ΞΨ4±* α‚މΦ*/λώW΄TψΑx#Χ5sΉms:iOΤNcWξ~ΫυL4ΆδmK—ω°γ8ζ}Ω–(4œ€D­«τΊψ¨τc'ύ‰“~|ĝΖkσΠ5ήχ‡"oΛϋ#ΟΉλ zϋμΖtν’”)RŜ9”{<^&qCψZβο‘Θw‚ΏrΰnςΟ«, Ÿ}K%{$yΠ £ˆnΎώ―H§ηsώ~ +€Ke ΅#‹εNξ ,Ώτ₯»oIΘ8’N¨αšΈεΰ[άrxφωΓ$!θQ双–Τ(|τΈ+NG²§Νηd"2ιe +N&ŒΡp«κ?Π8GA-tvwύ–––ξΰŽ*܍‡Y‹mΡϊύ‘ΔMΔΧ‘¨:Y +–ί-‹3Zή‰GΚΚ1ξδΗ²{’:πps­·άh6β#iΈlΟ₯ϋ~:ΐa,jbš˜Λ΄Γ N“ΙϊΖtδ|ΛG–Ώ•@Ξjΰ&NIF2>Ίτš6“˜―œ/zKοU·)ήDΪlKξ +―O£vΚFφ˜žaφΈι'•;₯ΈFM<+ڎ>³.@ϊ‰-$Ϊ™Σθ*1¨₯©1δ5<:I·ΰϋ’²:uΰ»|ΰ£{)4&‰D”1›ά:’ΕΊmœZΩ›-X*0’e=θEθ3S£RΟ"tq\£[ΨΣ{Ώtβ  i©™3| ϋ Ά;ϋs~:LΝο\ρ‘ενlak8Ωᙣ”P‰&ZόΞ‚ήνeΏ%“Od0^·Nώ°―γ\{x¦—ρ―BυkΙα‹ζ\ξK6ΡvΠΚSγed$΄LG­ GT†Ι·b-™T―ϋ½qΖ¨Ÿέsιt ήΆͺ•3?xβ``Iν›σzΠ½™)Ρΰ.h«=%Θ·ρr°?']=αVΝυ±N­0Qκυ1Ir½cΕκρΌ› ’sςΞǝΧΧ …Jί'―[§ΟƒΟΚΊήͺόόΨ +Βη€ž‰Vθq5}ώγuάπOΦΔ{S0y„AΌŽΤ$Ι–£gΔ‡^©€ Εl«€Sc#–,Bh θροήε5‰/<9φΒυε·tύω“Ύo`Ύ lΰθLθPΙΔ‰ Ιv›ΞθΎ}ޚ*‚o·Σ)τυ%A%ʚυ{KŸϋQτη5Cp +] + S_X~°†5ab²˜εΎ.AωκmξάΒr}Η–¬OΕΦμΔ §γŸΙYFL’Šηk³ T–«ΛNJh2RbV|¬Šξ@Β…«¦ϊΩΫ nx&Kκθϊ6[xH²ign³΄h‘μtŽD^_3‰…‰ε΄[ϊά|λŽWΫ2;ςϊ‰^μΤKά<ψ¦3>§‰Yΰ›‚ο¨ ¨*T·ΤaΡy½υέwΤ队V΅ νŠKMKvQΑg)͟γΉ'-8χ­%ΡOο|Λ›tΐΔ½{¦(%7‘Gšιv„τ«9ρΟ=ζ:֚ιΙΗ4Fd*›x+θkοΨ±F¦<΅£OΨ5pZ₯ΌΟ]Q±γ6ςΘΘγ’]QΣtš\ˆ0'ΓCF\=ξΡ‘ΐ»:™•7·Ž€$ ­ψsΐ5`' .Hy|Α ˆq-§θL‘7t»€ήΙτφΌ²Q::‘ D3~7})7 ŸΥ]ΐ°Ά?vνΝγγl-¨y)3‘―°_D© ”χ±έΉ0&τΐŠššφΔ,λ#6έϊxeΘXΒΥα_ξϋcν²2fMv|P$h*ήPRΪ”ΰ‹·‚ΎμX>ς›#ΘKsjιδ,^ŸMΝXkBΌšhΟ—29#1_x™Yΐhp₯¦»†ΰ` ΤRΌmK]λ½ΗšΗžΞε»|˞|@hU<ύ£ŸΗΕ‡Y=ώš»ιΒ8ΊΧaμ ‚Ÿ g³ΜΛ'««Ÿέ:ZύWΓ­ύο†gσαoΗ¨f2 Θ$qΖL_ +γΩ!:C~άx~κ‰+Κ2Z"vEwl“:Ώˆ;φχNJΪνΌŒ%#m",ϋ‘δΫ%fšqς” 7μrCmNΩ@C2¦ΪΌ<@…Ώp©7Q€ώΔ–)€žΜN†°†υΨΡsTQ Ώ‘7ΝaŒ-ψκŒ›wΏ™½κΩ69±"0=™,N.,€_7Ej’iwgaεΩSSŒPσYKΉ·ZxΏέ§Žƒн hŸγš©γ‚°Fν’ΘSX“ΩPqQνϊ γ|N2rJΖ +‘;»‡Πe‚ΐ©‰­\·(†ΰMβwωXp8ϊGΒ98Αƒ»!,I + +Zθ”dιΔ B{Γ#fΑ#7Η ε‘@mf *z gŒΦΧ Χϋ–ό|4ΘdΊE j‹ƒQΨι+?.ˆcpή΅ ΗE±…-’κc§KaXέΖΣŸpΎo%.‘ŒσΑˆΉφBΞD‰HύAΌήΖ†Α™Y³:‘F-dDYmyυ5CΛݍE δξ«ό‘a nsx°^έ½ψεE`Λ28\VΆΥ‹ŸίF«4γh8|ο]§ + IRWΊq\½~ρWͺ Bζ$"C6P’.‹Ηs’ L-ΰ,Ζ +swRoΞ3|d0꬟ΗυuӁ·γ‡σΘ“τT¬ό±)}τŽ3|ι² ‚χ Κ2‰JeΰΘ o@FΖ}Ώ2N`Œ™Œϋ²9“Q!ͺκݐΘϋ₯ ¨>#΅WΚΝ™Szύ +όΉγo|ύφ‰*ώω>―χΏgΜ7¦£ΑΠ Žζύξ–Fφ}δJφ*΅L~ΏΗΩxpΏ‹yΩ1ΞsUΚ5¦*@[•Šψ/λ(M@ +οQκ1¨1GXt05ΎαΘ=ή’Η_<ς†“ %w U˜Ih“`HL|εda’! 73Sf¦ΜΜ|033Σ!ߘ‘ΕtY χβeησ ΫΌΊ0‘>ί‚aLŸBJα°{WžΝVͺ'`α†Φͺw0†Μ &›3θΒ9ΈTOγ'ΪE΄ΗX¨žQg +lš₯η[φ‡ΰ§š?‘Dπ‡ς–BΣc0}βI©0kίΫ>pwζ΄€‘°IΔυύ‰jKϊRŸ}’Ž£e/@σΑφ œ1—ν˜οΒL\ΊŠm +φœyπκίΰ nTΫ{η‡π!;8/k–;ζΜ”Y?PδΗ(-nc3$΅ΐ]kH_ΓνwM;FρCΡΆ‘]Ε±γΔόΰ@~Ёjv²8ί:¦ΓzZ?Yˆ ςφk!ήMOUΡε^ά“ΐσ“fΎGΖΈhΏ²}€ΟΪPe2›/βcγςq$°b2ΰ-''Ω]ή:€YΕ9Ώκ<§|βb’‘φ‰T]r=2Δ .Ζ³Ψ^;ά±V"υ§—Π:YοΛ½_©Ω²©Ϋφ ⻇όL7nι-)E·$§DΞφΓxτΠ·.hΒ³Ξ,Λ@“‚ΖH_ψθ2›§ώ²:ΛΤεΟθΠ΅€wΰ5U&lu‰1Cjc]%3ŠΤΙΤ§IYi šϊ‚WQJ΅XHΧ/=:XW¬Θd™š€² ±H39™xKkos&lf”œ™V° !Pι=vφp–¦αάv™Μ:•L$Η“γhJΩIΓE˜ΉέŸσέEuΣΎ’μ₯ΧΊ“^Xύΰί-«ς0g0'/¬μ]*˜t\΄:δτόBe‰ΟήRDAΔμͺJ6 –Fλ , Κ’…²€d, ΐŽωφρθο|3–#ΈGr}§»KΊ^2`vs~ΌŒΕ²΅t +ω½Δυ'–ε΄T Ά}©vςθΧOΐωΓyF}αΐ'`' x=2αΔΞ<€«γPΥ£*Π½„ήS=tγB!τ*κwNzšΪ)0qMύ%9WυE_;*]σ%lΞ‹q tύ¦MMrEΕ#Ψψ}ώ΅(ξ">Ύœi• xϋ7©β¨―ˆϋζϋΧ?’ΐ–g* ΄²J©)έ:½‰ΐš0ƒUϋa·CυΨΕR b¬Λ&ΐΪΕ‡yά5'.»υ+p6Νƒ+‚Θό5/«+`p +άί^έ©…Ž’iY@l)6jorΔ,ζΊ’’{aΈ9ξ w‡Š#ΉT³2fμ€(aώM$…ŽΥ¬jƒuͺ‰A.:ށΕυh± [μψ²4!f {Tn&h>&4?/ΰπΔι4·ŽΩγΡ>ο‡ΗΒA½Ρ$ΠKΕ‡Cϊ,εPˆΟ£–ΓΚιP2—-טΜΙKaŒOγy%ο“ 9$p-»Φ«άσ 0{qψ ZοΞ«Ή~ΰήkςχ$xοΊnΐπύaιδ³Φξd°S|ηvAš5Ζ±ΞΞ’ΙΨΟAo2&©‘–šZ.…έ½ττΜ‘θŽη&‹ΖNyΑ@¬9Πrr +1udŽ7Νx ή=Κ‘–4egβΨ ±c—Dπo½“X1όO]ΉθB'“ Ρ•Γ‰p8φΡ„KθŽqΙtͺΨ[js§A%kΠ'€¦ž¨•€njͺ}¦{ςeBγΛ¬O₯{Δvƒ +ΙζpE«LXε+wΑD°•σU’Υ!ψэiΛΰ›ΡA“ιs’ΰIψΊ›±TŒ ˜ e4 Δ˜hœϋ²Ξo²ΜΖSο φ€Ž*¨¦όύ–˜‘ξ.έHμΚ;΄«΅ΓT +˜—φ„Έq ™šΝ‰ŽάOκ<Ν (ψ£Η£?ƒ[_%ΖR7*’@Ρd3,ΒΤίέX½nΌέΩ1ΪΏ&•*Σ"ρ).Ζ^'eœΏ”‹Y6(Χ4¬[6Ηb‚t V’΅mΞ-hlΖΰΩ|«ζ7Ψ”Œ‰IΜιΟcA“rζ‘„)“Λ)Z¬Όε8£ρ¦nμKAθ-ΟΊs9ΒmqQ8:e3‘ϊ`θ”~΄#ΙhG,&Ρ’–†:g6Zv]κ‡&’²*ΫΑ₯‰Ψ•`ΗΌO[Φ׌­ΒŸXdaήΗ§A“A΄G· +Χ?¦o”>Ζc€,[Χ½?gοσΜ†ΉzΜΝE^{ Π€―ζKλΉΜ€KĝFΪωΚ"Μ’§ž‡AAc~lΟ0wό[„‘99uΫ=ΥyUn:.€\k‘ω Σ}ήυΙqŒ,Y”χΊl+-œ„Α]ΎwYχbοW4˚ΊreκζκAS +vΠηoMαο1‚ΚŸhͺ™ΰρМZώ9GY εŒI ρλΏάl`?~ §tΘθnGΎΌΫ…kϋvt(‰Θ‡Ÿ\-«—Χ‹mΛnG―Π‘“+ξ–IΖ™ιΤΐ­Σ&kzΖςΪ¦οπΑŽφΖυΌŒyέR7 +‘„ΐkγω7ΛΤ‚{ΰΏy Ё r•%2!P%φβ–šTΉ¬uζFμ~?Qfν8˜N­±κYΙεδ‚ΊΔε‹εΧΣ,ΈžzΘ‘ 6)6C} [ΐ F’εfρ†GϊΥƘ•»*Λ*‘|$ΕύΆƒτŸ$†VτΰΫˆξœFΖ§Ύυ(GΥdPSb Δ 79ύWkxΉͺΘ‡ί?,ŠE{)rB='Ga|l8²ιšGοή„αW•Ύ~|f­ΝΌD!,W“+^ΝEJ_‹w%ΎΘΫΰΠϋDψΠzπμšUοS*ώ·HCtΠ=:<>ψΫ€β#ζ&ƒΟ’ΣŸHρOΓπ˜ύz—Χζ#•αΜ,Ό'G'…Η‘=Α₯qΔΉ₯π Φξ ϋΧψC#’ͺYrLΊ[a v6杕σlφ‘Ί§8 +endstream +endobj +39 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F204 91 0 R /F206 92 0 R /F209 93 0 R /F212 94 0 R /F246 95 0 R /Times-Roman 96 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] >> +endobj +40 0 obj +<< /D (section*.1) /S /GoTo >> +endobj +41 0 obj +<< /A 98 0 R /Next 99 0 R /Parent 6 0 R /Prev 10 0 R /Title 100 0 R >> +endobj +42 0 obj + +endobj +43 0 obj +<< /D (section*.17) /S /GoTo >> +endobj +44 0 obj +<< /A 101 0 R /Next 11 0 R /Parent 6 0 R /Prev 102 0 R /Title 103 0 R >> +endobj +45 0 obj + +endobj +46 0 obj +<< /Annots [ 104 0 R 105 0 R 106 0 R 107 0 R 108 0 R 109 0 R 110 0 R 111 0 R 112 0 R 113 0 R 114 0 R 115 0 R 116 0 R 117 0 R 118 0 R 119 0 R 120 0 R 121 0 R 122 0 R 123 0 R ] /Contents 124 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources 125 0 R /Type /Page >> +endobj +47 0 obj +<< /Annots [ 126 0 R 127 0 R 128 0 R 129 0 R 130 0 R 131 0 R 132 0 R 133 0 R 134 0 R 135 0 R 136 0 R 137 0 R 138 0 R 139 0 R 140 0 R 141 0 R 142 0 R 143 0 R 144 0 R 145 0 R 146 0 R 147 0 R 148 0 R 149 0 R 150 0 R 151 0 R ] /Contents 152 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources 153 0 R /Type /Page >> +endobj +48 0 obj +<< /Annots [ 154 0 R 155 0 R 156 0 R 157 0 R 158 0 R 159 0 R 160 0 R 161 0 R 162 0 R 163 0 R 164 0 R 165 0 R 166 0 R 167 0 R 168 0 R 169 0 R ] /Contents 170 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources 171 0 R /Type /Page >> +endobj +49 0 obj +<< /Annots [ 172 0 R 173 0 R 174 0 R 175 0 R 176 0 R 177 0 R 178 0 R ] /Contents 179 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources 180 0 R /Type /Page >> +endobj +50 0 obj +<< /Annots [ 181 0 R 182 0 R 183 0 R 184 0 R 185 0 R 186 0 R 187 0 R 188 0 R 189 0 R 190 0 R ] /Contents 191 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources 192 0 R /Type /Page >> +endobj +51 0 obj +<< /Annots [ 193 0 R 194 0 R 195 0 R 196 0 R 197 0 R 198 0 R 199 0 R 200 0 R 201 0 R 202 0 R 203 0 R 204 0 R 205 0 R 206 0 R 207 0 R 208 0 R 209 0 R 210 0 R 211 0 R 212 0 R 213 0 R 214 0 R 215 0 R 216 0 R 217 0 R 218 0 R 219 0 R 220 0 R 221 0 R 222 0 R ] /Contents 223 0 R /MediaBox [ 0 0 612 792 ] /Parent 13 0 R /Resources 224 0 R /Type /Page >> +endobj +52 0 obj +<< /Annots [ 225 0 R 226 0 R 227 0 R 228 0 R 229 0 R 230 0 R 231 0 R 232 0 R 233 0 R 234 0 R 235 0 R 236 0 R 237 0 R ] /Contents 238 0 R /MediaBox [ 0 0 612 792 ] /Parent 13 0 R /Resources 239 0 R /Type /Page >> +endobj +53 0 obj +<< /Annots [ 240 0 R 241 0 R 242 0 R 243 0 R 244 0 R 245 0 R 246 0 R 247 0 R 248 0 R 249 0 R 250 0 R 251 0 R 252 0 R 253 0 R 254 0 R ] /Contents 255 0 R /MediaBox [ 0 0 612 792 ] /Parent 13 0 R /Resources 256 0 R /Type /Page >> +endobj +54 0 obj +<< /Annots [ 257 0 R 258 0 R 259 0 R 260 0 R 261 0 R 262 0 R 263 0 R 264 0 R 265 0 R 266 0 R 267 0 R 268 0 R 269 0 R 270 0 R 271 0 R ] /Contents 272 0 R /MediaBox [ 0 0 612 792 ] /Parent 13 0 R /Resources 273 0 R /Type /Page >> +endobj +55 0 obj +<< /Annots [ 274 0 R 275 0 R 276 0 R 277 0 R 278 0 R 279 0 R 280 0 R 281 0 R 282 0 R 283 0 R 284 0 R 285 0 R 286 0 R 287 0 R 288 0 R 289 0 R 290 0 R 291 0 R ] /Contents 292 0 R /MediaBox [ 0 0 612 792 ] /Parent 13 0 R /Resources 293 0 R /Type /Page >> +endobj +56 0 obj +<< /Annots [ 294 0 R 295 0 R 296 0 R 297 0 R 298 0 R 299 0 R 300 0 R 301 0 R 302 0 R 303 0 R 304 0 R 305 0 R 306 0 R 307 0 R 308 0 R 309 0 R ] /Contents 310 0 R /MediaBox [ 0 0 612 792 ] /Parent 13 0 R /Resources 311 0 R /Type /Page >> +endobj +57 0 obj +<< /Annots [ 312 0 R 313 0 R 314 0 R 315 0 R 316 0 R 317 0 R 318 0 R 319 0 R 320 0 R 321 0 R 322 0 R 323 0 R 324 0 R 325 0 R 326 0 R 327 0 R 328 0 R 329 0 R 330 0 R 331 0 R 332 0 R 333 0 R 334 0 R 335 0 R 336 0 R 337 0 R 338 0 R 339 0 R ] /Contents 340 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources 341 0 R /Type /Page >> +endobj +58 0 obj +<< /Annots [ 342 0 R 343 0 R 344 0 R 345 0 R 346 0 R 347 0 R 348 0 R 349 0 R 350 0 R 351 0 R 352 0 R 353 0 R 354 0 R 355 0 R 356 0 R 357 0 R 358 0 R 359 0 R 360 0 R 361 0 R 362 0 R 363 0 R 364 0 R 365 0 R 366 0 R 367 0 R 368 0 R 369 0 R 370 0 R 371 0 R ] /Contents 372 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources 373 0 R /Type /Page >> +endobj +59 0 obj +<< /Annots [ 374 0 R 375 0 R 376 0 R 377 0 R ] /Contents 378 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources 379 0 R /Type /Page >> +endobj +60 0 obj +<< /Limits [ (ALG@line.1) (ALG@line.14) ] /Names [ (ALG@line.1) 380 0 R (ALG@line.10) 381 0 R (ALG@line.11) 382 0 R (ALG@line.12) 383 0 R (ALG@line.13) 384 0 R (ALG@line.14) 385 0 R ] >> +endobj +61 0 obj +<< /Limits [ (ALG@line.15) (ALG@line.2) ] /Names [ (ALG@line.15) 386 0 R (ALG@line.16) 387 0 R (ALG@line.17) 388 0 R (ALG@line.18) 389 0 R (ALG@line.19) 390 0 R (ALG@line.2) 391 0 R ] >> +endobj +62 0 obj +<< /Limits [ (ALG@line.20) (ALG@line.7) ] /Names [ (ALG@line.20) 392 0 R (ALG@line.3) 393 0 R (ALG@line.4) 394 0 R (ALG@line.5) 395 0 R (ALG@line.6) 396 0 R (ALG@line.7) 397 0 R ] >> +endobj +63 0 obj +<< /Limits [ (ALG@line.8) (Hfootnote.3) ] /Names [ (ALG@line.8) 398 0 R (ALG@line.9) 399 0 R (Doc-Start) 400 0 R (Hfootnote.1) 401 0 R (Hfootnote.2) 402 0 R (Hfootnote.3) 403 0 R ] >> +endobj +64 0 obj +<< /Limits [ (Item.1) (algorithm.3) ] /Names [ (Item.1) 404 0 R (Item.2) 405 0 R (Item.3) 406 0 R (algorithm.1) 407 0 R (algorithm.2) 408 0 R (algorithm.3) 409 0 R ] >> +endobj +65 0 obj +<< /Limits [ (cite.LM-DiskANN) (cite.asai2023self) ] /Names [ (cite.LM-DiskANN) 410 0 R (cite.ObjectBox2024EdgeAI) 411 0 R (cite.Totino2025PhoneStorage) 412 0 R (cite.Xue2024PowerInfer2) 413 0 R (cite.appleM1Ultra) 414 0 R (cite.asai2023self) 415 0 R ] >> +endobj +66 0 obj +<< /Limits [ (cite.aumuller2020ann) (cite.choo2020k) ] /Names [ (cite.aumuller2020ann) 416 0 R (cite.azure2025vectorquota) 417 0 R (cite.baranchuk2019towards) 418 0 R (cite.cai2024recall) 419 0 R (cite.castro2024azure) 420 0 R (cite.choo2020k) 421 0 R ] >> +endobj +67 0 obj +<< /Limits [ (cite.craswell2020overview) (cite.dubey2024llama) ] /Names [ (cite.craswell2020overview) 422 0 R (cite.cui2022dvabatch) 423 0 R (cite.diskann) 424 0 R (cite.douze2018link) 425 0 R (cite.douze2020faiss1t) 426 0 R (cite.dubey2024llama) 427 0 R ] >> +endobj +68 0 obj +<< /Limits [ (cite.faiss) (cite.graph_better) ] /Names [ (cite.faiss) 428 0 R (cite.faissGuidelines) 429 0 R (cite.fast25) 430 0 R (cite.g5.48xlarge) 431 0 R (cite.gao2024rabitq_gps_ref15) 432 0 R (cite.graph_better) 433 0 R ] >> +endobj +69 0 obj +<< /Limits [ (cite.hcnng) (cite.ivf_crtpt:2023/1438) ] /Names [ (cite.hcnng) 434 0 R (cite.he2019streaming) 435 0 R (cite.hmann) 436 0 R (cite.hnsw) 437 0 R (cite.ivf) 438 0 R (cite.ivf_crtpt:2023/1438) 439 0 R ] >> +endobj +70 0 obj +<< /Limits [ (cite.izacard2021unsupervised) (cite.li2019approximate) ] /Names [ (cite.izacard2021unsupervised) 440 0 R (cite.joshi2017triviaqa) 441 0 R (cite.karpukhin2020dense) 442 0 R (cite.ktransformers2025) 443 0 R (cite.kwiatkowski-etal-2019-natural) 444 0 R (cite.li2019approximate) 445 0 R ] >> +endobj +71 0 obj +<< /Limits [ (cite.li2023towardsgte) (cite.munyampirwa2024down) ] /Names [ (cite.li2023towardsgte) 446 0 R (cite.li2024svdqunat) 447 0 R (cite.lin2022pretrained) 448 0 R (cite.mac) 449 0 R (cite.msjimmy_bm25) 450 0 R (cite.munyampirwa2024down) 451 0 R ] >> +endobj +72 0 obj +<< /Limits [ (cite.nsg) (cite.pineconeHNSW) ] /Names [ (cite.nsg) 452 0 R (cite.nssg) 453 0 R (cite.nvidia2024blackwell) 454 0 R (cite.nvidiaA10) 455 0 R (cite.optimum2025storage) 456 0 R (cite.pineconeHNSW) 457 0 R ] >> +endobj +73 0 obj +<< /Limits [ (cite.pq) (cite.schuhmann2021laion) ] /Names [ (cite.pq) 458 0 R (cite.priximity_graph) 459 0 R (cite.rein2024gpqa) 460 0 R (cite.rekabsaz2021tripclick) 461 0 R (cite.ryan2024enronqa) 462 0 R (cite.schuhmann2021laion) 463 0 R ] >> +endobj +74 0 obj +<< /Limits [ (cite.seemakhupt2024edgerag) (cite.snap_cvpr2023_tutorial) ] /Names [ (cite.seemakhupt2024edgerag) 464 0 R (cite.severo2025lossless) 465 0 R (cite.shao2024scaling) 466 0 R (cite.shen2024understandingsystemstradeoffsretrievalaugmented) 467 0 R (cite.skewmanohar2024parlayann) 468 0 R (cite.snap_cvpr2023_tutorial) 469 0 R ] >> +endobj +75 0 obj +<< /Limits [ (cite.sptag) (cite.wang2024starling_sigmod) ] /Names [ (cite.sptag) 470 0 R (cite.tatsuno2024aisaq_gps_ref46) 471 0 R (cite.together2023redpajama) 472 0 R (cite.wang2021comprehensive_survey) 473 0 R (cite.wang2024mememo) 474 0 R (cite.wang2024starling_sigmod) 475 0 R ] >> +endobj +76 0 obj +<< /Limits [ (cite.work-in-progress) (cite.zerhoudi2024personarag) ] /Names [ (cite.work-in-progress) 476 0 R (cite.yang2018hotpotqa) 477 0 R (cite.yin2024devicers) 478 0 R (cite.yu2025ragdoll) 479 0 R (cite.zamani2023conversational) 480 0 R (cite.zerhoudi2024personarag) 481 0 R ] >> +endobj +77 0 obj +<< /Limits [ (cite.zhang2018visual) (equation.5.1) ] /Names [ (cite.zhang2018visual) 482 0 R (cite.zhang2020learning) 483 0 R (cite.zhu) 484 0 R (cite.zhu2024nanoflow) 485 0 R (cite.zilliz2025hnswoverhead) 486 0 R (equation.5.1) 487 0 R ] >> +endobj +78 0 obj +<< /Limits [ (figure.caption.10) (figure.caption.15) ] /Names [ (figure.caption.10) 488 0 R (figure.caption.11) 489 0 R (figure.caption.12) 490 0 R (figure.caption.13) 491 0 R (figure.caption.14) 492 0 R (figure.caption.15) 493 0 R ] >> +endobj +79 0 obj +<< /Limits [ (figure.caption.3) (page.1) ] /Names [ (figure.caption.3) 494 0 R (figure.caption.4) 495 0 R (figure.caption.6) 496 0 R (figure.caption.7) 497 0 R (figure.caption.8) 498 0 R (page.1) 499 0 R ] >> +endobj +80 0 obj +<< /Limits [ (page.10) (page.15) ] /Names [ (page.10) 500 0 R (page.11) 501 0 R (page.12) 502 0 R (page.13) 503 0 R (page.14) 504 0 R (page.15) 505 0 R ] >> +endobj +81 0 obj +<< /Limits [ (page.2) (page.7) ] /Names [ (page.2) 506 0 R (page.3) 507 0 R (page.4) 508 0 R (page.5) 509 0 R (page.6) 510 0 R (page.7) 511 0 R ] >> +endobj +82 0 obj +<< /Limits [ (page.8) (section*.2) ] /Names [ (page.8) 512 0 R (page.9) 513 0 R (section*.1) 514 0 R (section*.16) 515 0 R (section*.17) 516 0 R (section*.2) 517 0 R ] >> +endobj +83 0 obj +<< /Limits [ (section*.5) (section.5) ] /Names [ (section*.5) 518 0 R (section.1) 519 0 R (section.2) 520 0 R (section.3) 521 0 R (section.4) 522 0 R (section.5) 523 0 R ] >> +endobj +84 0 obj +<< /Limits [ (section.6) (subsection.2.2) ] /Names [ (section.6) 524 0 R (section.7) 525 0 R (section.8) 526 0 R (section.9) 527 0 R (subsection.2.1) 528 0 R (subsection.2.2) 529 0 R ] >> +endobj +85 0 obj +<< /Limits [ (subsection.2.3) (subsection.6.3) ] /Names [ (subsection.2.3) 530 0 R (subsection.4.1) 531 0 R (subsection.4.2) 532 0 R (subsection.6.1) 533 0 R (subsection.6.2) 534 0 R (subsection.6.3) 535 0 R ] >> +endobj +86 0 obj +<< /Limits [ (subsection.6.4) (table.caption.9) ] /Names [ (subsection.6.4) 536 0 R (subsection.8.1) 537 0 R (subsection.8.2) 538 0 R (subsection.8.3) 539 0 R (table.caption.9) 540 0 R ] >> +endobj +87 0 obj +<< /pgfprgb [ /Pattern /DeviceRGB ] >> +endobj +88 0 obj +<< >> +endobj +89 0 obj +<< /BaseFont /SVARXO+LinLibertineTB /Encoding 541 0 R /FirstChar 27 /FontDescriptor 542 0 R /LastChar 122 /Subtype /Type1 /ToUnicode 543 0 R /Type /Font /Widths 544 0 R >> +endobj +90 0 obj +<< /BaseFont /DHRMAA+LinLibertineT /Encoding 545 0 R /FirstChar 16 /FontDescriptor 546 0 R /LastChar 252 /Subtype /Type1 /ToUnicode 547 0 R /Type /Font /Widths 548 0 R >> +endobj +91 0 obj +<< /BaseFont /KLFPWG+txsys /FirstChar 1 /FontDescriptor 549 0 R /LastChar 188 /Subtype /Type1 /ToUnicode 550 0 R /Type /Font /Widths 551 0 R >> +endobj +92 0 obj +<< /BaseFont /PEYUND+LibertineMathMI /FirstChar 22 /FontDescriptor 552 0 R /LastChar 149 /Subtype /Type1 /ToUnicode 553 0 R /Type /Font /Widths 554 0 R >> +endobj +93 0 obj +<< /BaseFont /DHRMAA+LinLibertineT /Encoding 555 0 R /FirstChar 37 /FontDescriptor 546 0 R /LastChar 120 /Subtype /Type1 /ToUnicode 556 0 R /Type /Font /Widths 557 0 R >> +endobj +94 0 obj +<< /BaseFont /TCFTCN+LinLibertineTI /Encoding 558 0 R /FirstChar 38 /FontDescriptor 559 0 R /LastChar 122 /Subtype /Type1 /ToUnicode 560 0 R /Type /Font /Widths 561 0 R >> +endobj +95 0 obj +<< /BaseFont /DHRMAA+LinLibertineT /Encoding 562 0 R /FirstChar 132 /FontDescriptor 546 0 R /LastChar 132 /Subtype /Type1 /ToUnicode 563 0 R /Type /Font /Widths 564 0 R >> +endobj +96 0 obj +<< /BaseFont /Times-Roman /Encoding /WinAnsiEncoding /Subtype /Type1 /Type /Font >> +endobj +97 0 obj +<< >> +endobj +98 0 obj +<< /D (section.1) /S /GoTo >> +endobj +99 0 obj +<< /A 565 0 R /Count -3 /First 566 0 R /Last 567 0 R /Next 568 0 R /Parent 6 0 R /Prev 41 0 R /Title 569 0 R >> +endobj +100 0 obj + +endobj +101 0 obj +<< /D (section.9) /S /GoTo >> +endobj +102 0 obj +<< /A 570 0 R /Count -3 /First 571 0 R /Last 572 0 R /Next 44 0 R /Parent 6 0 R /Prev 573 0 R /Title 574 0 R >> +endobj +103 0 obj + +endobj +104 0 obj +<< /A << /D (cite.faiss) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 226.768 625.24 238.026 633.494 ] /Subtype /Link /Type /Annot >> +endobj +105 0 obj +<< /A << /D (cite.kwiatkowski-etal-2019-natural) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 74.439 601.33 85.696 609.584 ] /Subtype /Link /Type /Annot >> +endobj +106 0 obj +<< /A << /D (cite.yang2018hotpotqa) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 143.671 601.33 154.929 609.584 ] /Subtype /Link /Type /Annot >> +endobj +107 0 obj +<< /A << /D (cite.joshi2017triviaqa) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 208.326 601.33 219.584 609.584 ] /Subtype /Link /Type /Annot >> +endobj +108 0 obj +<< /A << /D (cite.rein2024gpqa) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 279.464 601.33 290.722 609.584 ] /Subtype /Link /Type /Annot >> +endobj +109 0 obj +<< /A << /D (cite.nvidiaA10) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 174.56 565.465 185.818 573.719 ] /Subtype /Link /Type /Annot >> +endobj +110 0 obj +<< /A << /D (cite.mac) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 284.246 565.465 290.871 573.719 ] /Subtype /Link /Type /Annot >> +endobj +111 0 obj +<< /A << /D (cite.ivf) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 245.633 75.303 256.891 83.557 ] /Subtype /Link /Type /Annot >> +endobj +112 0 obj +<< /A << /D (cite.hnsw) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 259.472 75.303 270.73 83.557 ] /Subtype /Link /Type /Annot >> +endobj +113 0 obj +<< /A << /D (cite.shen2024understandingsystemstradeoffsretrievalaugmented) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 471.419 565.302 482.677 573.556 ] /Subtype /Link /Type /Annot >> +endobj +114 0 obj +<< /A << /D (cite.ivf) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 505.519 441.765 516.777 450.019 ] /Subtype /Link /Type /Annot >> +endobj +115 0 obj +<< /A << /D (cite.choo2020k) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 357.828 417.855 364.453 426.109 ] /Subtype /Link /Type /Annot >> +endobj +116 0 obj +<< /A << /D (cite.hnsw) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 504.771 378.004 516.029 386.258 ] /Subtype /Link /Type /Annot >> +endobj +117 0 obj +<< /A << /D (cite.nsg) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 467.724 366.139 478.981 374.303 ] /Subtype /Link /Type /Annot >> +endobj +118 0 obj +<< /A << /D (cite.priximity_graph) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 481.711 366.049 492.969 374.303 ] /Subtype /Link /Type /Annot >> +endobj +119 0 obj +<< /A << /D (cite.diskann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 495.698 366.049 506.956 374.303 ] /Subtype /Link /Type /Annot >> +endobj +120 0 obj +<< /A << /D (subsection.2.2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 438.742 280.126 456.904 291.314 ] /Subtype /Link /Type /Annot >> +endobj +121 0 obj +<< /A << /D (algorithm.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 372.991 240.438 379.649 251.626 ] /Subtype /Link /Type /Annot >> +endobj +122 0 obj +<< /A << /D (ALG@line.4) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 429.675 191.776 436.393 203.99 ] /Subtype /Link /Type /Annot >> +endobj +123 0 obj +<< /A << /D (ALG@line.9) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 439.969 191.776 446.687 203.99 ] /Subtype /Link /Type /Annot >> +endobj +124 0 obj +<< /Filter /FlateDecode /Length 6513 >> +stream +xΪ΅<Ϋ–ΫΘqοϊ +Ύψ˜s2„ϋŠ‹Nφd₯μJ–oβ•Žύ°†ΔΜ "Y”,}ͺΊͺέ 8#m’i€F_ͺ«λ^Υ«»•X½~&ψοΛwΟώπJ Ήͺ²*WωκένJfBΑ7±’+kV…™ΠΕκέaυΛzμ―6ͺTλ‘©Wεz{―r]o·§c½ύreΛuF=^ΦCs΅)Φ;zν;κ9ή··ά Νρj#νϊS=Ά}wMώγΗ?ύD=ΪnsυαݟYMεU™‰R1@Fο:mœΪδλλϋφξ~³kξ°‹ƒ₯‘Ο!¬ήvwΤ~w¬ξiθΓρΤ…φa<ΦcsχΕ#²ͺŒ`)σL*œϋz„ͺZ»ύU±ώδ–’Φ=6|ޜΖvߎ_¨ptη»|Ύoχ =.B ν »šΓ₯²²Ψ +“Ι‚ρτ^(qΊΉR%ΜY˜uGˆΓΕρ•ΰΗ§š'Ηηfšnlλ=½ήφGz8Τm7Β?˜λψ^hλ ڎν§i‚‰V<œy„ΐ\eJ2κρ~@ϊ±zύd  ΦΑ–ΎΫ‘>p0§ν”·χ-ναθcݍΤ}Ϋs[Ϋλ¦ΫήκγΗαω@BeΖπ^~ϊ+ŒƒΝ|# ›mΣ©΄LΡ­Zš¬H;Έ¦u؏΄σρ―Wͺ\ΏψMΠHc²τ<«°*πΑϊώJ’Ι)Χ5Zs‘Umb«Ϊ˜,2[©tςχB(?(6Ε#x€² Λt7υ΄K‚ΠινVΠn›ώvΚSG‚Ξ[Π‹LNΟ„‘Ξ±Zdσ`½½'vc£Έ fU3ΆΑΪΓφ=¬ΩmΏΠ‹ƒΈ< Ÿϊ‘KOsηšήtsΓΘ‚°-,οŽμP“] £ͺυ5K žυΗffψάφ{2ΐƒΕ‡RδζδΔΡσ„vη‡'V)³ +ΨΉγšΩ ηVy+,\cαάDžU *A°*Θ“Ν)W­ΡΈ;ά:Œ§έzτr₯ικ›=oEyΧΒ#ίγD‘·D[R₯σ•©@v㏘VŒ<\Υή‘ήY’}ηδ/΄μκ±&Σ + Uj;€Υ~π<ίΕλα(Αc)5ΣRχM½c+6hς–θmG œ=zx›­ςm'iU>@S +΅ρX0±•ΑS#“˜έ„kzιγS:γ‚\ŠTƒE2«œ¬ώ™<ςΈ²°›’J"œΠ’8Mο§γ†cύ™˜ouN`‡λ-ΠΎSRUŒ8XžΈͺέfμ7γ=!™ύΐ^¦'²ΓcλPMΌc7Ϋ1<)ϊ30t»a‰ήt3νγ‰^°“OνˆkΙχ`CV€ΉBκ<wχΩ@ΨφˆDwTΒ^Iΐ +Q Š‰™°”D” +Wbύ²ή~t‚ͺ?ωεΒΊιαxjo&>b₯©’ΑPS’Χ7ΈAp’9ξ žΰΰCdς„oήnρAΏώ}oŠšnf`Α‡5ψT?8 DυP΄#θΰ§Θ© “:«— kf˜IM^³(ΐΗF’(…c·ˆ:L™‘ΧΤΐΒΨ¦¦†['₯Άξ΄ Υ{o€ύζφ2ƒ,°cΥ(h›°k°"i΅p2θ@žF0ƒš₯(Nρλ©₯`,»~ŽΦŽή~xpvΣΦp©¬ +fβ§ F?2±‘#ϋ„ά‰ζΈhŸ1&6₯Ρ‹v#ΓL2!²) ΦocΉύΡι,KŒ/ τΩQ6 φπG“/ΈzpY.eΊξσ₯Ìc8 +δd©A!Ηρ,μyNv»°Sp2΅)ΌmƘu±)L24ŒE|£έΐΓ]ο”πδ6ŠŸzϊ‹ΎΕ³BΠϊς–Α”―f|Ν–%ˆεdΠ‘w›΅9?΄b…—‰7Kϋ6Σ•Ο υG¦2d3Ν Νϋ¨αΫ©aπλ©ρ~¦{OeΑδ°ΎΒf!Ψ~;›¨i +YιΜθpJ#εž\Τ|ΎΦπ₯ ρe:Ÿ§L0£"SΘ’Nβ|·¦)οq»΄ͺΙ„±ΎΗkί£Z=T`*μveAͺ0ΝΐΏ€^riYPΑQ―χJY@6 `qΦ2+ΑϋЧUί2m₯ρΙΰSΕO +„-·Y–Ούς¨¦ε•Ι +nk°ω"Χ5’A–)?oΚ³„6γση₯₯sΐg±BY’S―½ΐPΈ“ΐ°O)‚ufαI=%ξbF܏Πθ΄§ΐ‚ΚŒOΔ~ΏLL:―όΦw_fFύίμ=δ-΄ 0&ΐy£­„'ΔΆœΜπ”“Jd@LΕRN₯6%ςβ8 @ΨΨ‰wbiž―›μάzn@ΙΌΚ¬ŠdΙRRgUQztΏEj"Ίœa£r’”;o–φOU ά~QX ΐχYΔ"fσƒ<™ =ΒΡΙ£½ΌηK;₯sn€— +T™΄½ω‡Ÿ‰Ώ*c«ά£>"οΚ +]ύiΥ‰6a9]‚HΦU@ͺyΒμ15£ Άϊ ©ρ€h„bξf΅šmΡ†Y΅p“ΛήΒΞ’™ε$–Ώ[Zά°7…–ρQϊr½ηύGžΰΝ,Ν΅σΈ όLΕL˜dΡ2`”p4)3I8;ƒΚRSχΪa™Žaϊi'οUqAΏYLzj“ /β΅\WdRVΡΌ—Μ4―›ρ‚0Φ‘yQD˜Lωψa°Αb6ΩaΥ¬γ£8Θl,wˆEd€‹@›T³Ϋΐ«n&U1.m“ YΘwM׏͐Z^°›ΊΫ²@ώfι <*Z€χ +ޝκ#ψ8ϊ¨ν<})ΉTBϊΰ 4€·ΓO‘ρ>ύ΄ϋ’T,«›H½ l†ΆlζF.ΫΆ?ψcα`εθΌ»=£ŸΌνϋφ&ΝμΤ<Σ°­χtg)3 t  "Ξc7τϋξqˆζH4£{gΠe Š… „£Œ{ +~rώψ[Σ–&^L ΤηYΛ`EY ΌMΊ_γ‚9fπΛtΕςςŠ Ε ` ΈϋfW^uL†Ζ5/©1Š‘ξ1ΗzΧlάTdσέπ™ΧIr >€Κσ'Ρvœj‰lŠΰO9Ϊίήם‹oΒ‡ΫsoΤƒ“”ν §δL,+£β)Γ),€-ujžb‡)DžQ e†‘λyιYGρIθDΥVsΰή‹1•ƒ˜LͺψBτ₯ήίa>δώΐU lŒ_Ϊ­―8@‘υ0 JTœqλŸC@ωϋΧ>(δΌiξΛ²€¨Έn―ζr [άΚιΫJsΰ2ž}ΙΨΑψ‹Φz{hžε¨φΝdigh¨XPjτ’”š4v¬ΪUžKΫ拉oΠΓΆXm’nΞ€yφγ»gΏ>σ›7*wΚ'‚¦άφπμ—b΅ƒrάU>»‡•5•γΆφ«·Οώκ XΥε—°/c2#Š™ν™Ψ ―Κj•;Χ’Υή€σΔrΔ‘ŒΛίOϊ0φz •ϊ‡ο0χ}Ό›¬ρ3»:σTυ!‘Xpˆ°ΈM–εΉ1q©μ@.J˜&ψφ©Υ° CΨt1‚ρΑVΛ‘ Λ¬†΅€»°Οψ¦YfP=*cΥb†ƒTωEεγιΨMΡdΓΌi[%ιmŸBœ³‘4Ž}ΞBhΎ²ζ1dηΦ±F2Η"Α“­ 3š—ψ©‹uN¦¦#‰ͺψHδ +£ κΞ2OEϊΥ'<1oα1t&š^|Αhƒi~=‘lGAGr=-u zΔWGM’}&‹ˆΦzαDΊ‚[μ›KJ+Ϊ°†ΟAt{rRZ«rΙ‹¦=βG°Qφ Ή* [8 Oυ°0A½ίΤ§;L)Δb|š­3ΖΟθ$lVΉvφΝΟ/΄^Ώfϋcε’£©θ2ζ9₯1({žkŸZΟυY’η½’yj0_"T«5ΨνE +i,«Kχΐ †LΖ‰GcF¦Μ 8vιJ|{b%+\J2z"cΑp³g»²rQίbIš‘*ΚS`1­ΎŸ/;g¦`6Ϋκυπq‘ϊϊ+­Σ_Φvf†ΪΈ˜ͺ@ίV²t΅q¨Q©8ξ*W ‘XΑΎvμ‡τ+2’«3τ™*.Υ‡7L‚η9„|/@ν>,!ιŠΟTΩAδΧNnN§ΧžS7δuΙΓΚ)ύ‰5±IŠ₯SgI;sžeX\™A†Ζ›S»/‚V*Wν@;ριωjU*ΆΆωBΔΣ²²qυ²;ΆvδA‘νΕ¬Ρϋ˞Jvμ½—ΈΠ6Yˆ¬πež‘"²‰αˆ–›ŠσHί€υ΄–sώύτۍ ~wΕ΅™Ό©P(n-Š—!,uC+Š@ΫΓ4Cz«%ψj@1ν֍λAvΝ°…E€Ÿ”™Σω|)=.u>‡ΘΰΥ*†θί‰:’B>pί0ρftT˜Τ€‡©ͺdΏ€ŽΒS/fmΈε ՟Ώ’·o € #[9ΫΙ<C ΑB˜AωoΙΥG¬ών»KbΓ€ό­” bm)1₯_1΄tS3sΫ@νތιώ]‰+₯Χ βφ^aσχg‚zΑ9‚ΟΞ³μۏΝEφα[ωBκ?c¨iΞ|7ό¦KJiU™tκbœ‹ P‡'έ?\Σ\νσnΌωB‰ιž[H‰γηs †d^'Ί…y§Kτ>ΐ9Ξ|ΡΝ‹ΡΫξQΨgΒ20WfΖυΗϋΉχ@™zμ{nΖ«θXγx3Š•ϋ\/ +]ΩLψΒοΨ,8Ώψv&œΌ­€œ~L%Gd ,ηœΣ‹.ͺLJ†δυΌ†WVςq֏Xζ©ͺΞΗوσρ©v-jύǟήώZΎ™λ‰σ"]π‘0$2½Π*큁ιx·½£/’>ΓV^d^vΗ!`°7ηΠxi7ς’’±oά₯ΙKΠ·ΙΚκό~[€Τ…Σ‡qoŒξ: /ŠtΙό‘%+™U3ΏuΙ +,±Κ$SΨκς’xτUžt@G+ΥΩΰΎjΟΛI3?Jπ¦”`*eδ–Γ„lχ‘ΉL–š’΅ζΌnn²kΪ‘cHi†άΞ|ώŒ:ΡΝ4OhzΈ’`t‡+³l Βτ²©λΠϋΘ6Ι “{ΉMKW—©„ρHΈ€ΈH])θϋEΫTk™©ŠSF^zK•:\ήΧ· €³c QφJͺ8U3p¨J„vh­ωcˆ;.)fΑG]Ν€* +»p―¨ΰ{EψqΑπ)°J;_Ηηq§Τ %ΥE“Ρ+%Š"‘Yx₯8Λ]λ$NΑΩBψϋ°―Ωώ*Ό<-π&πˆ–£Ys/Ιη±° [X,ύ€ϋ‘Ψ{"Ό§1π_Ιt9ψ»!5—κΒp«αΌT:όΠvΎυ…¦F;5'/”+§OζO€ot• °Ε“qΧ΄¨“C·Ν1‘½₯ ?ΌRσ―§LeΈ—vikQ¦«lΑzkw ?zˆΔH%ΈtFf9HΛxά5ύφΑηϋΦEΡe9]-kϊύςΎnχ½ ΅b_ηJ&+l`‡Δ‘Ύu™’σrΰ|de''λυreTiΣοŸ$ΑΒύhH27ζ„»½κώ’NΖ'Π^‘Ξ\Έ2—†―•*8‡™K¦šψru>ΨΚkŽ-ΚX ΰλ€žά)kΎγ²Α[θή‘t‡ι-«―’‚}O’žΒ?ο₯Φɘ[§:.UU.ΧϋŽΔ±ρ°PMKAΊ}"ΩX 1ςΓΉA ―ΌYι(δRdN‚Z žο©ϋΤν'2Οξ\ή΄ue‘Ι„ί]ςQmΨ³tט’QΧ΄tΈimhχγ‘/·šΙ΄?8_O?ΘΓFκ>Ώ;s-YR7ζ―•³%ŸT2?&―Ež™²Šη½( 4IϋΈ4 ™LS³•@‰6ΗρQψ°L›x’οž\ΈξF[4†‘)=~ͺuJ;ξ²Νψx²kŠxΦy +ksw‰;AΑRDGš2“ώΦyήLš‰Σh”/ΈεTΙxw©"—;]Ο.σ₯·Δπ%|-φ"RŠ4›Z(ύˆkώρΖχΈ˜Έ“:§λ^Σ―ZT|ο© “ ^AS€)Β&ΌΎwWH*OνΠ£ +ڟ΄S,¬‹yˆdύ―zΦδ™E*ˆΗ1S«SΜ·€4*5l#<Œ/,γ½Εœ)rRώgdcž²Κd>+ž}d“nS•$β-}[]”Λι—E’Ξ­λ­Ί.‹±ιΠ…q·dΌi +ϋaά™RlŽfρυ)FVe–λ2…νχO»v?Ύ’ŒΉ¦ε0ΎrΡαH*d±žΒEε…δΌj}{b|bsPœΑ4’tυ :ž:ŒΈς AΦ―ϋzp)Ζ~ͺβ ι§ΌŠ' μ>Ε•θα‘>Φ‡flŽOiR€3βΝ!%Χ‹Άͺ ΐμi΅Λ΅!AνYaNΏfƒ?Χqp-0{TuuqΡhμ—Š2;ŠΒƒ7±;qΨ©τaόψ7ωߌώΗΗ!v>δ“–U`.όςuη·AΌβΕͺΨ`&Σ@yΉqΏ0„aδοžύ€{ +Ή +endstream +endobj +125 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F204 91 0 R /F206 92 0 R /F209 93 0 R /F212 94 0 R /F297 575 0 R /F301 576 0 R /F304 577 0 R /F311 578 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] >> +endobj +126 0 obj +<< /A << /D (cite.graph_better) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 137.083 345.77 148.341 353.934 ] /Subtype /Link /Type /Annot >> +endobj +127 0 obj +<< /A << /D (cite.priximity_graph) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 151.07 345.68 162.328 353.934 ] /Subtype /Link /Type /Annot >> +endobj +128 0 obj +<< /A << /D (cite.li2019approximate) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 165.057 345.68 176.315 353.934 ] /Subtype /Link /Type /Annot >> +endobj +129 0 obj +<< /A << /D (cite.hnsw) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 179.044 345.68 190.302 353.934 ] /Subtype /Link /Type /Annot >> +endobj +130 0 obj +<< /A << /D (figure.caption.3) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 218.443 331.488 225.068 342.676 ] /Subtype /Link /Type /Annot >> +endobj +131 0 obj +<< /A << /D (cite.work-in-progress) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 91.661 266.585 102.918 274.839 ] /Subtype /Link /Type /Annot >> +endobj +132 0 obj +<< /A << /D (cite.seemakhupt2024edgerag) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 105.825 266.585 117.083 274.839 ] /Subtype /Link /Type /Annot >> +endobj +133 0 obj +<< /A << /D (cite.wang2024mememo) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 119.989 266.585 131.247 274.839 ] /Subtype /Link /Type /Annot >> +endobj +134 0 obj +<< /A << /D (cite.yu2025ragdoll) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 134.154 266.585 145.412 274.839 ] /Subtype /Link /Type /Annot >> +endobj +135 0 obj +<< /A << /D (cite.ObjectBox2024EdgeAI) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 93.715 254.63 104.972 262.884 ] /Subtype /Link /Type /Annot >> +endobj +136 0 obj +<< /A << /D (cite.Totino2025PhoneStorage) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 107.85 254.63 119.108 262.884 ] /Subtype /Link /Type /Annot >> +endobj +137 0 obj +<< /A << /D (cite.Xue2024PowerInfer2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 121.985 254.63 133.243 262.884 ] /Subtype /Link /Type /Annot >> +endobj +138 0 obj +<< /A << /D (cite.azure2025vectorquota) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 148.157 206.809 159.415 215.063 ] /Subtype /Link /Type /Annot >> +endobj +139 0 obj +<< /A << /D (cite.shao2024scaling) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 161.449 206.809 172.707 215.063 ] /Subtype /Link /Type /Annot >> +endobj +140 0 obj +<< /A << /D (cite.zilliz2025hnswoverhead) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 174.741 206.809 185.999 215.063 ] /Subtype /Link /Type /Annot >> +endobj +141 0 obj +<< /A << /D (cite.castro2024azure) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 212.483 182.899 219.108 191.153 ] /Subtype /Link /Type /Annot >> +endobj +142 0 obj +<< /A << /D (cite.douze2020faiss1t) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 221.809 182.899 233.067 191.153 ] /Subtype /Link /Type /Annot >> +endobj +143 0 obj +<< /A << /D (cite.optimum2025storage) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 234.715 158.989 245.973 167.243 ] /Subtype /Link /Type /Annot >> +endobj +144 0 obj +<< /A << /D (cite.work-in-progress) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 229.826 111.168 241.084 119.422 ] /Subtype /Link /Type /Annot >> +endobj +145 0 obj +<< /A << /D (cite.wang2024mememo) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 243.018 111.168 254.276 119.422 ] /Subtype /Link /Type /Annot >> +endobj +146 0 obj +<< /A << /D (cite.cai2024recall) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 118.048 99.213 124.673 107.467 ] /Subtype /Link /Type /Annot >> +endobj +147 0 obj +<< /A << /D (cite.diskann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 436.803 430.229 448.061 438.483 ] /Subtype /Link /Type /Annot >> +endobj +148 0 obj +<< /A << /D (cite.wang2024starling_sigmod) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 450.777 430.229 462.035 438.483 ] /Subtype /Link /Type /Annot >> +endobj +149 0 obj +<< /A << /D (cite.pq) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 508.335 370.453 519.593 378.707 ] /Subtype /Link /Type /Annot >> +endobj +150 0 obj +<< /A << /D (section.6) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 353.261 261.053 364.701 271.992 ] /Subtype /Link /Type /Annot >> +endobj +151 0 obj +<< /A << /D (cite.diskann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 543.471 262.856 554.729 271.11 ] /Subtype /Link /Type /Annot >> +endobj +152 0 obj +<< /Filter /FlateDecode /Length 6061 >> +stream +xΪ΅\K—ΫΖ±ήλWΜζžpN†0ϊ ψ^Ÿ\Ω–l%ŽΫr²΅ΐœ!" εΙ―OUW5Π ‚1:Ω F£ΥυψκΙ―ξ―ς«ožεόϋώ™€ίόJ\}εD™Ω\\-ΆΟήΌΝ―–Πώη«Ώψςl0Τ²‚ω’χΎx*‘₯hΙK'ŒUxΕΘL€/όρ©Y€c1N’ΌΔήuυΔ +Δ«βΒ]‘‘ΜuΙJΠ‚Ι+§μBό +π&y/B±“A©=OήωΓS)Δ +£= ࣇηζ-ΠmΡR%|ωΤο·K%2gPη)@΅ΫŸˆH€σ’8Ά8#Ώ*Ёόs:J"JyΦ£€Zˆ0v·”e©Ε΄ +,]H!2r0Ίγ.2av€(žˆ€½ƒ³ώ—kΙ6kυβΊ›G†œλIf0…—§‹CU‹ΝνΑm€ν9,mmf”˜Žš\Μg’€ΣF₯ͺ½σA΄zΚΥΥ ξϋsσ!Γξ°o&ƒN β]OΦnχ0)G€”|2ήdώ;$ΗίΑσΛJpe/ˆΏSΌΰp΄§,χξ0ͺΰΈ’c­οšέν$cDο8ερfςήη°Ema‹ Oσͺ=§†0:ψ±€ƒ€bvzBΫ4K^G =˜-­E.΄ilίΡ―OvTΫΊYΡύΰΎ#ί'VΑH8! tωY ηλeΥ­Z΄e Fυφa]ϊγoΈgίθi€gΏ€UQS…³ίί & AizEWUΛ-[ΠΎtΉήmWόπT>zχΎ“υρ€ γ€[P³νφο¦π˜΄.³%/½ν*TρΘFV^ͺ₯J4a.P+)ΩΧ€ƒ €&ι©δδ(€HΗ0ζτœΰε…ϋ΄9%Ϊn›Žaνι9ζ‚Ε§Ν© Ά$CΈότ”Zgzάν j₯ΰz½ͺ~«½ ΄ŠA2!λ 44zε=[ κ~5Ε@Ζ€1ΑωƒxΏΌ˜4x"PŸ9Μ|gΣώ3P‘z4†=Γ΄€Β€ΖOšSbΜn4†ugHf`<ώήά‰Ÿ£a󀂯V[Ύκκ-ƒ_ίp?Λ’¬Άw¨ΆS‡ͺAGΜ9χ+‹‘!+Εμ9Ά¦­Ϊw-5ωΑΘ©Θ#Ζ‚'^­—yπ4ˆ=J‘6xΌ»Άd"φ=ʊW€Hήύ’b πξ(GγQ3<ωv0Gη¨―aχ7'ί£½|€ί݁gMΝ=lœOΐ3Ο +gϊαJ…9ψΟήMG—’j¨•K‘ƒSίΧ^KKοS| f‹7ά5ςτΰvQZŽ!”³ΓΓΤRrj΅nΟE "?UpHJi’Wέdτ5τA΅i_σ\>«.ν“—‚Rρ‹Ό% 6·κbˆ¬KhQΙπz€!“άCα«γξCU™iRΑΈΣ3:Ϊν“&¨‹)όδ˜ΞLX˜ %ιŽZ‰ϋKƒΎ3Ρ™m`Ηϋδ>B€†E‘c8ΰ Φ!‹ΐ)3ΟXΧΑΉ\x.—iI’μΫ±ΏΊ>4ΛΑdο”ΐ™έΧχΥν#;\zFxM³πΒ; έΓ:Ή[γD«(tev #τγθA>;4θ‡’‡=T7λς8 ΡΩ YR +•iπΪβEkω~ω’MKάύB‘`Y +€‘ρž™,KξDνMߊ0ψi+ŒaL„b χƊ aλ!†#Ctl!Šž (Γ?±„μ“Z‚ Ω±ΙΒ k@!» ;;‘εEo#ΌŒΆΜ¨έΒΞAλwtsι™PC -Ζͺ“–]N+‹{“]/f―Ωυ+ΐŒί―»©ύjΰπρSMͺr9ΉŽξ·Υ;ςΞqSέάaώvΓ―ψ°VN@A ΌσΦkήΒ1ΖQ΄h Κfΰ Σ¦%kBŒ˜ξω΄ιΪΖ€lN¦TΌ[z@et!@aΠ#(ͺθ{΄«^ƒšήΆcδθπΰ}¦έž+Η ηwΐ}~JM,HiΞ«B@’μ·αUΕ-€p< +zΆ$=l=―a ε’φ΅Wy +°ρbΜ}©h²Ζ±–”ΣηΰiKΥ–«ˆ]ΖQ<§Α¬pΪύ-νU―€εΠ™ ~žgJΎηp6&GάQRE\i —λΡ$§w‚9*‹wχ¦ώ€ RypG"’Ήœoz›Ζ‹ ƒΑή:σ—ρH`6½"†K‘O€(2eΛ@‘0OΛcΙI )Ι“R†ζj±X=tΥ-‰wFΏ΄Υm½!½ ·±‡Ϋεκ~_-ƒυΎϋ±€§86d›?¬W!½@RFΨ£Z ¬¬Ό†XP:QlMC¬w›ev6kόβuŸυbQ€‚IŸ},ΥVR,/ΊτΒ{άπΩ«­ΈϊzχμΗθ+₯ΠeƞGƒO|΅₯TαFΡ— ½¬ο&“q&-2εΜG~Έb|χSnΥSnMΩ#%*Β–Œƒ­œΙ!K€$Ϊ•ν-x+hB©‘›’;LΌy=†Νƒ1’±1βQόΧ-ΉœeχY/{†VΒωΚcνΰρ·Ά³‘x§ξ=*h3₯ΩtΘ‘šΔΓ2δX ίμ°|@χή$m9ξ.ϊ²#QλWf4Έ{qΆfSύ'S{Τ?h₯8΄ΘYΦ]HοRWοό8Ÿ=Δ,ί©›EGAκΐ₯'”W ”Sγυip΅$SŽ’ͺFάΝ—!”J-G˜W >ωηtΟΉ#Μ_…Το|…ί΄-j†J*a³„«ΒͺPNlŸ΅Αt·2Ψμ|P;Z'vYp1£Ώ!5°σD±Ox‡˜ +sφ¨Θ«|ίoώv=ΧΕμ:iEY‘XsυkC½/B‘Φb]―†Œςξ?Rd‘A-Π4`€~'ΓεStΙ•Gα~ξΏϋ3Η,diϋπ΄*Νμ;ΛΏβC/…π;Δ›ρnˆ–g'Š$ΖσλR€γΑώ―;Τ0J9DJηqΐ{ ΐξ†ŸΒ#Έ†ίoΏωάβ£ΣΠi”κσΟφ‘¨|r}\{?(±ίάΌέXό8Κ·zC)/ε‹Εa_-c +Νƒυυώα’γOr―­ΛdnβLŸ@°δ ­>³6>ι»P2±ΐ )7τD|sX’~ƒΫ»Γf3ψ¬nqθ1.ξΧdL&yI€ϊfΘψv |kφ€Ώώιω_9Αμ Iγl±’Π<ξΕΡ·§:ΚΗλY³ Ÿ[έΦ„'N)u­ϋ•&QMΒ/eǟ{Ϋ΅j~σ”Ϋ5¬5½3yv―β–“ήjŸ΄Γ8π΅»ͺ#w•£…?ϋΌ +^ ”'Υ†₯?«n½σQqΊVj“—e2Ή)Og΅Ζϊ~“τΏ”k#2ηT2„Υg¦΄p§Š€ΫA–‡Σh»Ϊξ(LτH ‡6d&O]`Ν9zϋ88ϋlΤΕlw”'’΄X†ΣPδjΉ Ύš>”δzœ}5ŸR,žr—iΙΈΉO―f”ψψΓΡCτΨ‡₯ΉZΕ*/ τ%ΆEƒX{S`Υ±­§fΪ-#«ΟHx\–ˆΚ?ΣNŸqή>(ψοͺ0 +ΩWΝ;xε†šϊ ’Tc/]·X5<Ÿ4”ͺΐ―Λ‚ο[`nςY_ ³_―*–γͺλ?›ψ†]r>Ζ‡ΔXT9‘Λ»ρ€ήήΏΌŒQ²½Œb`ρ†ZΨΑ•7Gπϋ·ιχBρ59~Ϋ%Σ΅Θςd\Η`­VιώoyUAw*λB5Δ±bWΏ£dJs¦'Έ€ŒjΤ Nx$¦\€c’ƒ›ΫΗtŒP„Φχ^m‡μ†―t’ ŸΙO2‘PΊ¦rŸj «Ή¬Λ{ο¨ΊΙΜώαΏ6 F&„k(.ΈX75°L{ +(e2κ;}δY}mΧ XήΡ5rΑΕ±δpgπjΉ―δν’¬'#Τ {τMw +!€7”ΉP Z7p6[φ—Πjlv-ۏ;_ +NΙΙεCΉ+Δώύ(•ψαqIBΏϊ?T|E–|6ΐ!KΎ?4…¨£δρΓΪ‘a»ͺV p q±―=½θn@ŒσSτ‘e > +zδ\%z?‡c³>ΟΥΠ£‘žnΈ°a[5«(Εζ«&ΤΒυϋHSܜ\Mαΐ+‚ˆ½c9>L*΅Δͺ:Ο«-‡J±­»Ύ4UΛ#GήdGϋaμ―£Φ±ΣMAT §—œZ"~ΟΦ—}† +ΚIkiϋΌ!·ιgΣ; t؎Ljͺ‹χ5‚«¬GAέξšBm€τ1Y"Ο{ΚοhΗελU_€ρλjεF‘΅7TMΘάφ +‡nι7Žšφu +45†ϊ»M™’³\^ug°ŠΒΊt»S¨­S"ΐάΙ o³IÎΥB+μBjΑΡQ NLΑ9¨An°žωώ~³ŠΌ ψ½­6όy±ολ~8υΰ­OΗ'T¦LΌ —°\μ§nTΕμbΰ~²h‘κuπžEtT¬Ύ”E&r&ΝλkΐLΎΰTΰαƒsψ¦‹©πyy*°OΧ^ρnι…†GxΨΧ;£eχ―ο¨ΡŽύ^|ζύ΅‚#SΆQϊo…]Ϊ@…Έ&χD…ΗP ΤΓ@1•ψ0¨€ϋͺ7Κ Ρδ!sΚ,ϊϋSφ ―Γ:ΨΈχg,Ηƒ-U©Γ-z‡ϋΆ›/6•7PΠ΄»ύ'Λ>8@ ~ΐ#ΐ‹ϋΧό–ΡΈνΓ΅ΠΒgύt§PŽ(e&Cέcψς"DΊπΊ’Ÿ£Ϊb›”ΰm(εσ΅bΟνΈΪ‡¦=₯z±,Tb”`σv8Έ\―Œ– +θΦρrξΒ2 wˆq‘‰ jΨ , +ΖυfΜeT9νΏW υΥή ͺ)0mXdJΪ>ΏaQ₯|ζͺλ#¦½'‘Ž>θ9šŽYF-ϊ:MR‰ώo.x-V…Š©τo½Ω{ςE5Z\dJ€ΑΩx…ΰNIζ1₯Dˆ΅‘‰χΟ>0΄F¬@8§^rSΕ# Η—¬Ιχρ!mΕ‘lX„κίΚ t~Jπ°ΐPΔΟΠuςώ‚ Ν₯›ψϊ»ΟΏž. νφœ‘€t•Έ€mxNωΪ51₯ΓEŽ5ΉL9N΅¦κ½39- '˜Ž7¬ν'{bœΦÍμ‰b ν9–JFΖ–y°α0*E±)–zζEόΔ)ΔΙ(&«,ŠΩ푦o" +η#혊Ί‘GόΙOαRžI0νˆΓ όί$F-Ϊ¬tͺˆ‹¨‘+―,~€^L}vϋβυ³£,+< +endstream +endobj +153 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F204 91 0 R /F206 92 0 R /F209 93 0 R /F212 94 0 R /F293 579 0 R /F297 575 0 R /F301 576 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] /XObject << /Im1 580 0 R >> >> +endobj +154 0 obj +<< /A << /D (algorithm.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 177.368 545.772 183.901 556.96 ] /Subtype /Link /Type /Annot >> +endobj +155 0 obj +<< /A << /D (ALG@line.9) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 227.264 509.907 233.796 521.095 ] /Subtype /Link /Type /Annot >> +endobj +156 0 obj +<< /A << /D (section.4) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 212.831 350.504 223.981 361.877 ] /Subtype /Link /Type /Annot >> +endobj +157 0 obj +<< /A << /D (section.5) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 280.367 326.594 291.518 337.966 ] /Subtype /Link /Type /Annot >> +endobj +158 0 obj +<< /A << /D (figure.caption.4) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 259.586 298.699 266.198 309.887 ] /Subtype /Link /Type /Annot >> +endobj +159 0 obj +<< /A << /D (subsection.8.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 120.789 226.968 138.951 238.156 ] /Subtype /Link /Type /Annot >> +endobj +160 0 obj +<< /A << /D (section.5) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 113.82 179.147 125.129 190.519 ] /Subtype /Link /Type /Annot >> +endobj +161 0 obj +<< /A << /D (subsection.4.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 179.83 107.416 198.17 118.788 ] /Subtype /Link /Type /Annot >> +endobj +162 0 obj +<< /A << /D (figure.caption.4) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 344.746 533.817 351.371 545.189 ] /Subtype /Link /Type /Annot >> +endobj +163 0 obj +<< /A << /D (subsection.4.2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 447.21 497.951 465.372 509.324 ] /Subtype /Link /Type /Annot >> +endobj +164 0 obj +<< /A << /D (subsection.4.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 536.343 386.131 554.828 397.504 ] /Subtype /Link /Type /Annot >> +endobj +165 0 obj +<< /A << /D (subsection.4.2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 460.082 362.221 478.244 373.593 ] /Subtype /Link /Type /Annot >> +endobj +166 0 obj +<< /A << /D (equation.5.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 391.263 324.125 397.981 335.497 ] /Subtype /Link /Type /Annot >> +endobj +167 0 obj +<< /A << /D (ALG@line.20) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 371.239 204.573 377.809 215.761 ] /Subtype /Link /Type /Annot >> +endobj +168 0 obj +<< /A << /D (ALG@line.12) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 544.04 180.663 555.344 192.035 ] /Subtype /Link /Type /Annot >> +endobj +169 0 obj +<< /A << /D (ALG@line.14) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 487.024 85.021 498.18 96.394 ] /Subtype /Link /Type /Annot >> +endobj +170 0 obj +<< /Filter /FlateDecode /Length 4583 >> +stream +xΪ­;َγΘ‘ού‚5( ΔaΌφ­ν9μ]{`»k1ž}`‰,‰h2©ωzGdD&“Uέm heF̌ϋȊv§]΄ϋα]tυ+ΰh'v"R‘ˆυ.*TZνŽν»½ ‘ΔΜπšfΘcΐ7nεξΫώέίαίυΦ»χΑΫόοΎω^δΩ.σD&»Ηη]B—–ΐδ8Γ ~Ϋέ?ƒολΣ<μ³ Ϊ€– χψ?ί|/#±½>Χ‘‚ΞjΏ|χώΗiύ‡ΧqͺZl«ΰΫΊ8 EΘγΉ"πθ¦ΘΰΨ·OuW4rOηCY†}Žηq‡Ί˜ŽΥ°?ˆ8ψTw'š»_ΞvΚά1\Ού@ΪΊ«Ϋ’!θ8υCqβC<χϋCτΣe¨» ―Ό³8U˜(ψΙT˜ε9]π₯žπ;qDŸ<<c…λK2ώRΌΞežŠ©ξ;):ž2½ΐœώΠTϋ4ψ„Σ‚U«Όύς‘ς΅+ΪϊH§b:žιzΠ£λA£ϊ9RϊXWpΣΧΜ(z₯ΎΑ›Ήθ±G·ώη(Ž>LΥe€8h€Cψ&Fη‰Η;‰£4&΄τ]cΎ$‚‚~F@vΓΝωi¬&jχΟτ₯Μ₯¬ΖžsΑ>a†»¨#,η§s8ΖfέYŽ€Φϊ`¨vΕΩ:…€Γ•ΕΣEŽ8ž,S'S[‚gq(β―…³DŸ“ζRΏˆτPε ό4]δ"xߜϊhΫ’Γ8¦Οƒπξ>ξΦύ}·ϋ§Y,V„΄ΩγΉiϋώΙ 3BΫ~ͺ‘Reψ:Œ"ι‘H₯‘H΄gάx,ͺφ vˆZe ΜΔμS0‘―™oͺ[ήe(`ŸΑFο0p±Mη ­S#©,ΜY@PΓEπ ›΅4Δ'By‰‚εeHSώΨwG›*ΰ§8 ^w zŠν·ΑA G₯ΥkM_”t)X -Rξ€Ό‡(ψΧ°0ό6 μΎ”p~~Ν΄Ο klhΆ$Tυσ+υQJψ^yκ],‘΄ϊΜ©`Gψ%Άp@Kτ-ηΑikZ£Vϋ…Ο‹ςCeΦΟWΪUοt–ƒ©bj'·oΡρ†…•6G·ζΜ£™NU¨#ήπ―© 08ϋDσΞ] GοΪ6»™Pq¨uΎή‘Δ wΌ0Δ©₯4T„ώωXρ$τΤώˆˆy₯6ά΄ΩY4ͺΦs\՝πœ ΗLσ0N؎ƒ¨?‘UΔ^߁™lqΑ‰ΫφGPfπw‹AY=έWuθ((Χ*Κ-­5ήϋ$ΧΨ:’Όͺ(ƒS£L™Αž~Ρ’Σ˜gΰp ½ΤL†ωΐ'οΚάAθ­XeVUτ3~(ιtX$γ7€<°‰i™ &Νμδ‡zA…ΓΠ²κFnzxΛoNΊ”w:Pdΰ1ΨΣ±Ξ4{ΆΐΜΰΒy,Πl«©U°-ޚκγάƒ΅€ΦZZ½άΡω άωΥ•HΖP=™]p)¨ΗΦ°ΔA Ω@ΗΑ­k²Φ*(ŽΗ~&7BYB‘U‡yυ©cσl§ΓέVΦ•U¦Ύ˜ƒΒIR‹’~*¬Ηγά0vEw΅HΡ^ΐI”?ΠQ“'‰ˆ}CKΓZΰ8ΖηθLt’v +τM*΅ΕKΐ++ΕΥ0RΟ‰m"ƒ–tBWϊƒa=ύeI~©…²Cc‘τ΅x©4 ΣDϊ’ŸΖΐ +br&‚Φ¦ ρβ’&N)ΰ$Ύ 6η‘6KhΨp87N”€1j˜7•TΑ4„󩆧Ή‹ͺGImΎ}η­bgρV±Χ2 +·„Z³ΕŒ‘’«Η–m’XΚE±‘~₯‘;Ύ6LΆͺΕLCΖϊBCό,Uͺ― -q”Ωx₯‘psΒϋΖu΄€˜!΅ήΰ₯Aζ}I€ΊI3"ΐ:|™ΚWΧρfΐƒγL"šΒΒWlΊu0 φNK+‹x‹, ―“Ρ72\Τe‘1KCcZ˜θλΡΕ ΚXoΐ«εf΅υΎ–*ρ6UξΈή}…―WΠ}]H +Φθ§}&Ao}ΡO8/ϋ%λmW½‚L¨υΞΒ–UW¦ώP™H/JkΆϊ‚™UwΗ~`­ < „ΫπΡT$ΓH'++π&·ώΥp:ϋάe=ηqƒ€1œΊ$hΝ ΖsΐvΜjώ…Ψ?όboζˍ€α?Τζ“έ††‘yF9γ Ήπόΐ±aN^=k ‘‰ ‘νΒ’#1 +ΰμ™ŽΤcεo97pϊε(i +ΕΨ΅Α„‹.Μχ9ΐ@ΡIπ4Χ Η +ύ|rΚg’†₯±[Τ{Ά`:4–ΛBgζ wμ6δT&I(2Ζ–±O‡ρ\5F+ΘΫάƒΘ•ϋ~Hs~:ΧME#ŒE„BΘ 6œΪ;Κ 8u=xΗ-’ΕΪ„Δ‹ΊH­oΕS‘Ύ•φTΈΓ<ЈRˆσ-ΐ΄Τ5:;΅4LMΨٚtΒR΄ %+Υyαy‘ϊρtnΨP°pγ~"·Bg+ˆΠΆΌMr„`²c„—KSϋ_1€YύΕz‡SQ¨DΌ² $beΝ!‘·―2T|ΐ©·²f5FΆφoΰŒ‘eλχΟ“q”tΎθŸCsπ‹Μ=±εςw„7γ°—γΥ6_B¨0ύ(£’§Ψ¦R@ε‹Τ:ΥJ$k)Q₯#<62‰γζ“°:/ΔΨΨt¦ηπδΔΉ/C)΄Θςh–2Τρ"\)γΖ{υ|RεrpΰΤΞΕnd΄te΄/Ό•Œ—μ€™2w‡-/2 Ÿi ƒgΛ:ΙΏΥ4{=Zrz ΣόϊƒEݎλδΐΥ nυ†HαDŠs ukƒŠ’ΔΛοzψ*/}“5ί—½›b3]>ξ€ΣΉ˜Φ;πϊγ–ϋ-β b4›w ·]kPgΘLΗ½ΒεΒ΅9qŽtŒ&”qώ(Ν0†χ +ˎΥσ&τc³‘Ψ€lθΖ‘tA[ξ₯ψ +1fͺΐ³₯‘Ÿ#η§½DΆ‚Ζέ|šc·±\›^q­ ’εΆ0ηΩΝ²›ΏzήOI#—‘ΖλΦΏΊ!*Z΄΅u•’{CΧωXΥσ–˜g „”ΚερΧGT”R‚£²ƒΐ€4ψΣ! IF`«€+γ=PΌ°ΎψGj—)‡MΎαδOEέOΏΥІ.=mΡ¦Σ4TYξ©‘««Χ-l°8žΩ2;gC8gC ,&R)άO¬ΜšΙ™μuŠΐb‹Ϋu,=Ž«α{ΪPkΖRYΩΌS₯-λεβ”ρΓ‡dj]Nι£—Zw—&Άƒ ‘ΐΠ©„ohόBί]½>ψaλiΔ?nr΅oηΪά­@Ζ΅bΩω3W³&.|ƒ?mK!Ζ€ „]slΊœvlδqίΔtΨh«ιLκ›u ρΜ(Ύ©½«¨±¨ Σάά΄Ά$ƒnFBδl£cζd½ζ ŒАχΝ§ΝΈ"Ύ—μΦϊ?Π]›^ίΓέϋ²ˆ%γΉošώ:όΙ’ύ<%ΟϊΰŽ*Œ3›³; žgzΧ Ε5ζˆ†=C3ˆ`ύL΅q“ι±p'Œ~Ώ2νTύύ79λW1Ώ½)ΈIMƒψΰ+-Εβ” zάƒ-’€α/Χο‚>ά{τ§Χ§‘fαϊΆ!Έ>V[2uEΣ$JŒ]){rοΡeDwVd&;œšμ0φjώ₯X=d‘~‚ξpšΩ”Σ5™WΧ3A2‚\nj?ΣoAίΗh α₯^΄v{°‡-ωΡ}\‘X 2’jŠα„/DL§¬&°ΟυR†dέp[°`£†@ητή6mωPq–ξT€€eϋ/όΊIΚ{oGΐ‹žMήŒο’Ÿ”2Ψ|†Β•ͺ%ξ·΅3Ιϊ[„}ΤOw5œΜ…αsήsU˜ψ>‹]RšζDΆΌ€@¨U‚0 όmς7Œ9ώζ] ϊiηfͺ¨ΐ!zΐˆήᣟ’‘Α©φ²qD7TΡ*L3ƌ‘xΡΜ΄΅ΞθΥ^Y°Λ•[_‡)―ξ½½8NΌh}Xζͺ[l‘D˜)α.2›ΑΞ2K@θ*ǐα”mΓ¦Ιϊf‘(΅ζ +‘iφ’ΎΩ²SRδaf£ϋ₯@έ¦Ι’; δ›·‘6 η^μ:ζ€+Μ­ŠΪG‡Hζk»ΆJqwΣxŝHŸ¦²άγ=Ϋ2ύχΜΰ‘SHέ»:Gδ10FΧ₯)"!Ζ:Υ…#μΎΨό‹\Ό=μ8ˆK?ΕlΥΕH][,ΐh$Ύ>O¦C[πε\.–ΏAŠŒwη^)ζΧΞΎ+τQf 72C‹ JsΠΘ~Υ AΨCΌ“’ŒϊΊ"ΐHeŌβŸφ)%EΑΧΔG/œοΠ1'“τ¦š€a0Α37­gpοy­N£H΄ώ0„“Ι^ΖΑοοΈ.ξΨŸaƒJΉZmό sN›€7Ρη«:«-―išέ‹ΣΙ>…’Έ|€ξ¦rΤ>η`—σZšY#^»χΠiΗWUΥ’7ξ2ΎΞ ΐφXζγσΙΌž<(Νς† §BκƒήΦς$σ O[1 ™–žJΕάβΎ*,@Ϋό“•Ε9<₯"Ί+B¬ࣃa€Aγ )ύpœAJŒ*~β΅Άς^Ή½ΑB‡†YΘ?έwŸρzγ8 ‘qό5›α:gB‚GoΓM«\bι=ιVτNΩ4ζa(Ί„a1Ωδ7₯@+ΖλοDŸΉ>ΗL₯X/’—WF“&Α/Σ@d΅*Ω4ϊΛ›ΘT:Τ™Ύs”ό2Unj>«Eυ9 +˜σgλEφ‘ΰο ¬σ3₯ό„»@yϋΦ½T’…Z₯λm>§gά2X”―Χrδ>5ϊΛ¦mŠ"}]RΟsΊ9ρ¨λά6Ξ³ό$Rίόmx–†ΚέΗf.ωο_VΖEX΅…χ—;Ec|Κ’|]Ρ eόω ΊU%r₯pΨitωYkˆ Τϊ΄ &Έ/ΏL‚'Ž5Ω‘υj?κkμ$NvΎoΘύgW±¦Χj#† δ.iο/NΏ{Β7v]ΏΌΤGί”RΠ2Yρwχ.d©!ˆS±ZτΥ–Ž&τΈΩ³5¦ΌJΎ”MymΉij±ΌFξlαΒ{ΨοQŸ’€νΧDΧNΒ<Ε—ΧΟ»ƒ€6MΰΧdm>Π_ςέγ»‚ΞΥ… +endstream +endobj +171 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F206 92 0 R /F209 93 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] /XObject << /Im2 581 0 R >> >> +endobj +172 0 obj +<< /A << /D (cite.douze2018link) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 476.44 637.196 487.698 645.45 ] /Subtype /Link /Type /Annot >> +endobj +173 0 obj +<< /A << /D (ALG@line.16) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 543.385 515.407 554.828 526.78 ] /Subtype /Link /Type /Annot >> +endobj +174 0 obj +<< /A << /D (cite.cui2022dvabatch) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 448.993 422.002 460.25 430.257 ] /Subtype /Link /Type /Annot >> +endobj +175 0 obj +<< /A << /D (cite.zhu2024nanoflow) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 463.161 422.002 474.419 430.257 ] /Subtype /Link /Type /Annot >> +endobj +176 0 obj +<< /A << /D (algorithm.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 436.407 371.945 442.939 383.133 ] /Subtype /Link /Type /Annot >> +endobj +177 0 obj +<< /A << /D (subsection.4.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 485.163 217.166 503.648 227.716 ] /Subtype /Link /Type /Annot >> +endobj +178 0 obj +<< /A << /D (cite.diskann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 434.556 158.989 445.813 167.243 ] /Subtype /Link /Type /Annot >> +endobj +179 0 obj +<< /Filter /FlateDecode /Length 6041 >> +stream +xΪ½\[sμD’~?ΏΒAΔ.κ·Pέtaaαη0ΜNΜΐb‚†Ή[ξ֞Ύ˜–σλ7³2«T%«»νζΕ-•JuΙΚΛ—9»Z]eW_ΏΚψχ—W~³+qeτU!ͺ4ΟΔΥbϋ꧟³«%΄υ*K‹ͺΈz°½ΆWRgi¦ \oΎυη‹›W½UyU₯U.σ«›»+£\ 4KΕΥΝςκ§δυf΅?΄ύz;›K“%rφσΝ_?z+aΖΰ­"Ν…†1ν73]%³2ΩΟΦ̊δWΈl6τϊχM}€ΫΕ‡ρ ys3ΪSfR£Šρž,υž’½α*‹΄*ΰV™Γyy•W:5YIkG “#K«JρΊ +ΈQωπϊ^fwμ?ž"Iψ¦Θ²΄¬β796‡Ω\˜δΡ½o;Z™ΫW’W?Ώ8Ÿpώ£ ―ρdμz7―½ΏŸΝ‹dίξϊ³Λ(uͺ΅Œόμβ2*8"‘O.™ ™κέ»v·β¦ΊoχηV!s‘*aβρ²K«9’πΜ*ΊγΖnήΟγΊ”©1Š^θΪߚɡ©2Ζqώ$'+W^6I :/φ˜αΨΠε¦Ω­ϊυΤTe™jQΉ4’Μ3bί˜γ‘‹°{™ ψ…uΪWεyžΡ7 &<Ο?Ž}Θβα‚TžFέ{™f₯ίϋb³οš§}ξšv΅Ύ…MdΙώΠQ[?yόp Uεχύω… + R¦γ «ŸΌ”ΗVaίΜζZ'%¬¨LΎ˜Ν•ΐk₯’/gσ“&ωΟ©ΩDžλ‘ΕΛͺF η%L\]˜ΈΠΑpJZ3=³ Ηsθ¦ώ tΛ‡ε85K Όgω ‹W2UͺdΟYΌD¦ΦνfR›I΄γΕΙcHqκΨ>™ΪU•MFtŽΧσΒυXξΟoτrQ¨X΄ΝyΡ–&•ιίL-²HuŸδΗΛ4―Ά₯Ίπ[”'p€?ε9ώ33Ωδ™·*σΝ4 (d`쌘:b€«žk‘„²:2:_‘]8`Šό’n(έπι”5u>°œ£HK\θ—Μ‚\€ά́άx΅r₯z Λƒλ«²bDqήΠ¦ψcΉš²Gyl +ω»η’xιFω ϊιβoFκ³β`Z–ΟƒΔKΙ}εΗoEξqι]{ΐ@CUpPΦΫύ²ή,Ž›Ί·>i‘ΤχχΦ#μi·ΎyΩv}½[4g}v--H†g +*»‰Bh₯5ŽΩmd}ΆIβ_8†Χ3ΙΓ!εΙΧ“Η£  9mΤ€t?…9.Zf™[„Σ'#•Μ]ΰEZ ¦ u™σ³βω›xwηΟ/Ίώy,|ιw"†!ΣR£ +MI„Τΐj•ŠΑ§`rΓ"5…ώ}ΘΏίίO ε€„ύΈUD‘ay1mςJεγ –λ+OρΘ^ŽoΏy6Ερ§έžΫέΤlC-Τ΄ΧrFyœŠ΅”ΉA*€mεAτJ?9Α B<Ε 1h•ƒh~0ea9Ε%°PλRŒ5w~^sΗ`αΛύφώΨσΑYΦτŒyN7W ©j€ίΣΚ8Ο#e ΆOKPΊ„Μ•—`§ˆ '§Ÿ§„³²ΞεJ”ž~e‘ +5ςsEq^whΤ3κ_^MkΜ2―ž=b–‹gκα|]|2¬UUώϋca'`Vqaρlυς}ŸσΚJΛ©Q *LŠ*ΐN΅Ϋγ—M‡™ΕR£ΔEδναvQοπB%44ΞΫ΅==kκΓ¦Εt4>’˜³ƒΦά•4s·,‹ˆάΊΰ”bo±ίβBt•lχ^jήφ~ sΫ΄/΄Τ.ω·ζ.anΤΏ±’TmΧt)5Ώž ™t<ύψlξ΅]&ΓΥaU©]/K@’‘<¬ιJaS»έZξYΆμπΚ‰œ₯@˜liωΠiq<Ψ…Ζ‚pš˜ΩήM€ͺR-™b…η d5ž†©Ό΄Ωš#Ο\ρ€Ngΰ‡_‡Oα"†ΘF³pAwHPΧͺͺβ—ΰδhΖ…ƒχΕ΄Θ0ΐ04‘%†ΘLG·ŽΈxέξΊζΠ³nα–ώlΩζαμ£ι.€«‚-b%ΐ“θνkšψaέ"νρ²c~Ά2ιVΝΏxvOw/+‘ζ°^R6υnΩ.IN‹’kZμŸ ΅ χh/Ίγ-1-ˆ•Π’"ΪέΧ»Ξήͺθχγ¬ ~„K=;fcΩšϊuΫMΘ Œ–ζFΠ’8f†RŠ‹²ΜcTυ8Ι»¦5ς₯ν±£_βΛΰΛ—’dE‰vΗ-«γ5 OΫžsΓ²\_^tf’yΩόεΨΈ +'Z‰ϋHsQ@  ΡΩaΤ}y\Π―—K, S“š‰sγ΄ύb°%2'ΘwOOHB™ΦΆvΤvtMm%‚8g{ΫξχŠ}{Ώ¬•H«΄(™ˆoκ‘.Jη ,I‹―ϊuGΎ­'ŸŒ†Βγš4K:βpηqΫ Φ3₯_Q”Κρ S,]‘ΌžžΟΛψ5έχλύq΅¦k +β t΄(Δo– ίΘtŽ !s€Ϋ^‚##{½UρΕ$_/Z’xFδΗ‡»zΑ/0ωK0ύ]Ψ捡05½[RΫωεδΑr2ΐΝ¬0v΅‡@N:·ΨΦξ„Υz}»aH‘[<ΓΗx8ξ|»;Αγn C ΣcΕ|B߈Κ3uάρ¦πšBΉ ˜=VΚ‚dS|rϋH-Gš›&ψ{„ΊΫΧD‘δe"ΐ2υζȜQ=…)(:ΗƒG\€G­:tυ暎‚™·Š˜· +ΧΒ‡ν€UpP’ifL Tl]‘ŸύnΓ…† O„c +¬=ŒOž ,‘R—~xδΘΕ1Ύ±8‹άV¬Zή]¬[πXM n 9A=xHυ0ΜΗ·ΈεΎ­½ΰ„*ŽiX:ν8kX‘ΛT€?μΩ€ζΒΧμϊΪ#];ζν#—όήΑρjΨ{Δ€Π Θ²[β«ϋ L@в’ιίZ£gT„Bk4‘²εst¨ͺΰ₯η€AΛƒσΠaDP«ψϊγ„2όUΕ$ψφ»™b)Ν§g€p– 7KŽGwpG`g±ή΅ΏΨzHl"ˆ¨3Kr\ΆυάCξΥ8Kh["έΒB©«f‡βίώVγ3'ή&ω¦§_v§Ρ!Ž$S"M#|(δ@ΙΑ«Ϊz ~η<jΫ τ΅Z‚1°ΣΤF`0cM9Έ"fΌ“₯Ή]N9 Ϊ†1iΓ +)φξ7;xJMήr '“ΐP šΠJ΄2 σΫzP@Φ,„ +θ4ΗͺFjνͺœš’Ubg…_@rοζp  ηΘ‘‘f§½J^žΣαΡO$Ί) n™2ζκέU|?―<Γκ’H5Θo΄QFς.\c –’ώ?ΣΤΔp᷎7Ξ]Σ-β$A +ό6CΧΞoό΅]zš*²q…υJΰfœk¬ο71‡YΌ_XiH©C(Έͺ—υ}_ίΆVΐlΛΆ~7 o9>&‘SεΌEkjlπf†ˆιιΧ=ψlψΊg2Ψu}¨—νjKQΓ΅ +Uφi½°Dι:zΌl”6ΣΊ%“©vήcΰ›:|u{\£rY6ŒS† B·gαΠξ»t*ύ‘aŒό.3$ƒ§šeΙWφtwυΆ]Π@_Τ=M§Φ¬…ί·μqΑε 'ψو½ωϊΫΩ τ'>W‹ˆΑ―Vœ3φ•ο™‹3f±ή„φ8ΪpM8).„ή ΙAεX&d‘Km!];G.;=ΗΎO†1:uZ…ΆΘ.w'·’vαά0ΨΒρήƒ+ΦΡ$Πc]”v΅jάw!γ˜ΔIΕb΄LσΠIΓxHNΡQ4„xΟ†/G1”œc(ΖΖPJŽ«γ}xί.Θ)ΐn8©y<}ϋK±p'›•_usJΐŒΚ‚Yš₯#gG.Ο·.¨s.T§BJSEΏΉ‘Σ™Ά,|‡α”sΎl|μˆξ6Œ‰0$hi·υ‰¨%nά"7SΖ|N{W™τΞqΖή’Fducγyx:'tτŒtŽ ƒίΨέ–‡ +α"ΥΤΈΑ@Ρ³ ΖOvL‘Η–ͺE―Oξ.Λνηwnw.ϊͺ80…AΔR)ŒΗχΤnγͺŒxI”2Dο?uΖGΖ>ΑvΊiι|Τ–½+ ΓΧ[TmάβνœqŸsAΌκΎ +\ΞΕήνΝ뿝zάFΰbΈ«ΫΝI“£K™–Ξ%•*ΚΓΈ†8: Η-έψ ϊTΟ^EΞ0αυ5τϋ£Γe€0ί~π +Fh?tώύΪ*]p8@+1φά€7ΒFΈ…б n ₯IΦ–‹—C-– veφ”*ΦSϋL1WΠu8’Ϋ2z“š1΄Φ4jŽ΅]dvύ|iW_Ϋ?ο)HrΛnΥ|LΟΦ.eSΈ|\8·Θ†Θ-η~7^Wcάͺ±°?wΩΤαϋcα«Φ€mbT,mbe­žsžάω.jπϊ ύ ίG0΄Ν9€₯J…σAλ%Ζ/Ι Ν^> 4Ε†˜XzΌ‘jPΦ Ρδ"ώΪ3β¦Ν―γώΧ/œ3―ΚρE~zΞB₯Π7ξ3m³X{›’tυZ΅πZiLc¬Ώ3|ή#0@΅ŸbfUΑφ3^ΒΝ °η\θrɁ—$m;Ξ—zUUMG(― ?ΛυUtrc t=κ–Υ£ΛB֜謳·αv +ŽΧο]ͺΡΉαυvΑ)L €π'£’!·=);9…‰’jΖ:]?Ηγ8tμ8„—”ƒ’²ΔO‘ΒβχLƒ‰/б½Liΰ9Π*+—Υ‘ΣΔ‡fPοTς9%“Q©Έ¨ ‹θˆKI«/]ΐΉTcΘ \TofJλΦB'ͺ…σzŠx΅f›e‡\Π‡7έi5Υp¦>P Ω%ΚFζrθ6Ώ΄ν[}6NO/ ‘γ\>‰MOΙVTdΰαε8 γ’Λχ,όΗι.3eͺqP&*ψώžρ¨A&'jk€ƒΰ^ϊ„¨©!`Ού˜!ξΗ«1χϋZε(άλwζΦ§Ψίj™ύ]¨Φc6kΔΧ."Uv τ”l½ΒpγXoFI–ΝI&J“»}ZO ΒŽ~Ω4O*Tΰ™‡θ=*;’ΆP>©ιφρΤ +£Ό;kFv‹*Μ·n<4EΖ6tΊͺ2YVΊ§FλfU ˆν›ξZ{3dm― ζlJτ’΄»ϋ{pςdλKΝbΓyπτf²NJΩXπΦ~ϋMΥC]?ΙΖωE„Ήu§ι‹„Μ `Jδ–ά•“”SΩχ(95D88zސŽσU5g|λ}> Χ€gNY˜yg5€§Π&ƒƒ>Έρδ7‘š.Α±ΓLΊOΉŠ$Χτ{9Ή…‹Χ"£ ~ oΙ6qΝ^"NtΊd`’σDΙ —εΐί²ι›ΓΦζώYΔD€Μι­§±Uaci• ψ\$ͺ…–ŽUi”§‡Cγp©σ“uΤμ£-Glλ]Ϋm]t°ΑΑε@‘X_…ι„g₯θΉP¨4CΝίΰ,ώσt +όdΌFP€­·¨tUNΟ²σ%Vλ+ŒΔOY{xϊγ¬ Π\“XΤ{a²ί}wf’ +,λEQ¨ž²±^σ…hHV‰hπ.Δίˆ*ΐέα;A .ΓΥγπ½Lc†•έ†VνβxH6ήT]ΰ0¦£YX+ζθ¦[ο7ΛΈ&_—Πη Ύ—·2ψO?τd(»as’ΌDQ΅ΑR9[7D +Βd‰ΟdGηzκK#ή…|ςΧθ ©αƒœ“~nTžΰό\ΜΛ¦¦«Ž\ο†ΈΦvΫnlΥ vφuIEαδΞEϋπ‚Δ¨ήR‡.όWSφ*¨U%D Lj8ς;Ά~Υvο,XΒΖ—ϊ‘ΰH#Γ)MuΪ£Σ:-,½Ύ¦Eq,aC؈¬ύ~X²ΰpje±#ήοοNϊIQ₯ik„€‡φž<š]aΡΥbασώyP”“΄ ¦ c±`)(©s΁Β+Ϋύ‘ηβdώΙzΒ‰%œλ v•ΗȜ[§ρ₯>νσ4Γ¦}ΗπρCNϋF”³!7’ͺz;i|PωGuν²'Έ*B(}μ\$°D"²DxΧ…ΆrW0‹8vݝRΩQ1…ƒs!DΆχažΣ`cΰΐYΐhk!«e»|?ZVερ§ΦPP–3§>ε~εΛ2R–ΨzΏΆNƒ}Τ·vUλΐĜΓ{œW/‘xS·Ϋ“D +«(,'J,Β°V±δ’I)MςΝG gžΓxΦά­2^Tζa”žέQπι؝>»°ͺΒ?ΌΆ­ίƒΚϋΥaξžR„Ν爑ευL( –6―‚?TΓΑ`ΈwŒ‰=–ϋ{λ”H,GxϊqPžV…*Cλ­2*ΰ―\§ΪE†Μψ“’ή4΄Χ +endstream +endobj +180 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F204 91 0 R /F206 92 0 R /F209 93 0 R /F293 579 0 R /F297 575 0 R /F301 576 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] >> +endobj +181 0 obj +<< /A << /D (algorithm.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 288.645 595.519 295.27 606.892 ] /Subtype /Link /Type /Annot >> +endobj +182 0 obj +<< /A << /D (section.3) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 131.491 312.169 143.016 323.357 ] /Subtype /Link /Type /Annot >> +endobj +183 0 obj +<< /A << /D (cite.severo2025lossless) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 69.279 266.585 80.537 274.839 ] /Subtype /Link /Type /Annot >> +endobj +184 0 obj +<< /A << /D (Hfootnote.2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 361.053 569.59 366.926 582.038 ] /Subtype /Link /Type /Annot >> +endobj +185 0 obj +<< /A << /D (algorithm.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 551.727 386.711 558.343 397.9 ] /Subtype /Link /Type /Annot >> +endobj +186 0 obj +<< /A << /D (Hfootnote.3) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 536.728 362.801 542.601 375.249 ] /Subtype /Link /Type /Annot >> +endobj +187 0 obj +<< /A << /D (figure.caption.15) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 526.941 326.936 538.384 338.124 ] /Subtype /Link /Type /Annot >> +endobj +188 0 obj +<< /A << /D (subsection.6.4) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 507.569 255.205 525.407 266.393 ] /Subtype /Link /Type /Annot >> +endobj +189 0 obj +<< /A << /D (figure.caption.6) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 499.567 219.339 506.211 230.527 ] /Subtype /Link /Type /Annot >> +endobj +190 0 obj +<< /A << /D (section*.5) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 443.925 159.563 450.643 170.751 ] /Subtype /Link /Type /Annot >> +endobj +191 0 obj +<< /Filter /FlateDecode /Length 5910 >> +stream +xΪ½\[“Ϋ6²~χ―P₯jk5΅#„Έ ]»§Nvgo•½xNε!ΙGβŒK€"Rv&Ώώt£ (ΙN=ΆHFχΧNΆx^d‹―_eξχ§W~³_δjaxΙtΖλύ«ο~Θhλ"c¦4‹ΆΧ~!TΖ2•ΓυnρφΥΏΒ<|xυω^‹’•ZθΕΓΣ"—LK“fŒ/6‹ο–_잻c3lχw+‘gKyχΓΓ_?#ΰΡ(Γ4W0§ρηζyKΏ¬ŸwΕ²Ύ[ό›ώiϊϊx·βως}Σ>SϋΧΗκ°υ]N-ΆΓ‹©_=LVε,—fΊj ‹Ή΅κdυΈΓJ?° 3θb‘KΕς¬ ΥπΧ !S†e¬,…£ΛΐΤγπ= K{8 ―η˜δYΖ +­‘86ΟM[ν+bωlδζΡΡ<ž+\ &EžΞρ}&ΥΝ—ΑD1ψΆœ^ُΌ`ZΉm;XφγΦu;TΟNΕρΜόvΑΥζ\Ϋ5Η©ύ'?›hΐTiΈRΜ€βYgΚ\Σq]m‚}Ιuα_πΣpIKJ”pίMƒ]Ε†VqQ)’EΚ‚EŠ΅` € κz–i t3U*§νo,•ƒ.2¦΄³ςυΩ •.Uκ“RΜRΛ©_ωηρ°˜2τs3@<ΌγwγZ2ΔΨευ</#žρyZΚ2₯ίΘΉ™$,Ω\]ŠŒ‰¦υ*eoUΩ¬ +“ ίFžAθηθ(Y)ΞθΰΙrr8? b‘LΜΙR:‘—|?7œ©"•"9eŸ-`Q―‹Œ ;AΥϋΓπ29:W€έΐ’€v;§)ΤΉ΄§z―όLU²B{‚Ζέx3ya%_¬’žWv(ΆD°Pd2dΰ’;Μ’‚©\'ZozŠr6žίΜ‘RX+βz -κ€—~ŸεΩ7Λξ4ΐ%Ώ¨™›v֞$ΩΞΛ{Φη|οςOQΚœ†œ%¨« -‚)δvΣ]_m,‚M«― ͺP Wϊμΐ&š[ΒΑ/FžΜ λλΚδ­έξ™₯ Ν―κ$ԍŠU"exεUd +v*Α +Κέ €§3‚4³sikε]§ΧOnΩΣDΏ£§hωπ’Π ξβέ"½Ϋ«ΕwvP*ΖW0FɌι›λβ,ΐEδaUΝΣI†^b—žηΦ%φΉe:u[ƒ%'Ί;’ΥWΕς ΅p·ΏJfQ€§>Yαύ-W-ƒ΅):υΗ‚χvcΠF™εΆ>›~hΦW…1Δ΅@,4° e9w^ ·χ+O7,ώΕˍυ–ςεc³iŽw%‚B\R„?6Ο™ #$†š΄W}g@η`‚δΝ_]bdˆDΝ$ŸΠ‹aKΒٞBΫΠ]£Λ–o‡ξT¬ γPS” x;Oλα”Δ'ρVΕΞΑτ +€~…Ρ)₯oΙ!Υ…sHαβαLjΫnυχϊΞ,ίcxGΟήz‚nICΒΕζ₯­φ`YμΝc5¬·΄ep·'#½­Ζ(Ή%A΅HpRΙv6ύޚL…ާ @vΥΊ¦& ν‘iδΆΪψτέJ/Χέώpͺ1˜ψ^PΘ[Α’Θ±…KπΛ άOγφHQΞ™,Q„λVΦΦΞ†0ΈΦΛΏυΕ7ίPs"=΅?‘_OϋH­λ\‡ͺοmp`έΨ΄nξT²)&eΙ”0q.„—Ή šΐ₯Υ¦*z€A{Ψ½Σ³{κš)‹WGϋfRaγK&-8Ζ±θΰ2d€’½ΫΕ—€W%Gll"@Πc°γΙ^Ÿϊήnε†ššφc7)3ό–‰>€\ΞτaΫ VΔnnΧ°΅ΒœQΧlzz‚ϋd‚Οhγ Z&KΟΰi/>[bwoƒνΦ–ΛΗϊ \&α ΐ dcO=ͺ~:9Φ»‘ βυL}οnI˜Κh?R (JΠ€`(,]£8`ΖαΤΥwφΑjŸOΝ¦vM[wΡG§n­Lˆ‚B₯Ά5ifθ;4»]6νzZε8CžΑ˜)'ςϊΣc?TνΠXΔ—•0²pω»m]m5Ύ±f?+ύ<μC²‚τCζi‡\/ξ qmκ~}lβš;ξB£SνGϋ–/Ώϋ8Α‹Πž0y:USYL²ƒ ν'oώαž”…ΫθΈ± χ:$ςφy΅λZ§`ΐΰu§–ΤJξ’PΠάYέ]„\žΜ~γOŸΰ΅“ぜ{Ÿ+Ρ=Αβινή +§…Χ4. wOOχΥ;Rβp]{ρq#νΩN^ANΞ/ΛΈOζFRž&3Γ^G[dΕ*9€(ξAσά"6– =έT@όΊ>Ά—N¦—³άρααN ΤRΑŒ› Yjk΄J{Vξι‘·lΨk·³@Εu €n+>AυMύΑΔO/n= {²7ΪΦαPŒΑ)Τ2μΒυ( ψΥΰ~ES;²x•KaΚh£—~»΅[ +W΄₯"†@xg«*μ•έŸ΅‡:€-z?° ˜E€”«‚q₯Ό*$fcωƒ;πυ‘έChά½«Υ―βd–½±>N‹—ΐΙ*"@jΖ‹’8–Aϊ`Β΄Z#’ΟΨ§W₯G?DΠ|ΕbΓU + =i2"M(&€γMΘl1šβaλ5)΄ΐS«U[πΟΡnηŽ=™d\8κηΣ±ZΏL4κΟfd|sΐOLŽ—ΖƒΉΆ―ͺzΛΝ 0ΐ“h­,Ζ²WAĈ\Y)‹ |gα%₯±¦h\€Ϋ΄ΐα 3"pvΡ–ržͺƒ’°i{ή―°rv?ϊα­ΤD”ΆR©:ΝχκΥ±wˆ ώ ˜lω§·v6]?Ξζr| f œOΉ―¬ζΗΐ'a3ƒαΰ©³ ΞJΘΣ+π΄ͺ;+© +ηίͺpεFΔ3Sbύ–X7 1ςΣ+pd(Ϛ-’Kϋȏs Ÿe/_v―ώΥοω.+?χ*š|¦€Q*eΦ :PθοΏižc›!Ωl«ΐBΒλwφάAY;|ξxχX=6»ΖgΧ}i޽‰9ΆYLΰχU™˜ΘΑ,>b0Ρ:"Tΰ­γΜ~ΓάOJχ.NδΚ…uϊ0Άq+Βƒc0AN‰ΣYJΒυ~χ™n»‰ξ}ΨΥƒi4Νπ άιWƒ― •ŽL‰dDβΫ†°—Κϊ` ΰΘM;mOΜ‘Ξ0vγc ξ\Έ³ŽΊπηρ”cτεΤ†ΈY9e˜FŒκvΧοΕ`O,!8~sοψXζF·ΫΈ”¬m< Kζu―s8’T<‡Ν/gZ1PJ° +Ž‹λ-hΰΊ}5ŒΥ­»lϊT…LΒy‘V1^Ζ‚π΄γqσ’δΠ„Ϋ ʜΑύ@%h„ΜGKΊZ()S£e-4z%έν—ΕΑž‘W·ήVi‚έΠςν]!¨ςQ’ΖέW;WS:Kΐ °ρψΖ²EŠiœZ˜ιΰοιΎr=Ÿ:.Ρ›’|‰‘’%ΕrέЊ~F7 nO †΄Ž0Π„Ε >¨B€€t‘#Pe“?ΫH Σ}1£ωo{MKh@τ)P}3QΈ"λύXτ’€P ΛNJΐ7IώΒπ £ΰ²„0καDH}•ΓJ +ŽMLŽ=C΅E{ afC¦ΔT0Rλ"ΐΒjόα…Ή"@+Dΰ˜F·5ΊN‚Δ¬q&lτίόΗDD3$ήΘΥ α#"ςW‰ u΄8ΩlnUL–ωεπrP`š‡Tρq£¨Ίˆ+.y³ +Ξ»X§ΖΗΊΑ<γΎψΙΒhοΧ`X½δ#Φ8hρgW.nΪΣ₯’ιΰ?\π°ΗWζΠ]2jS·έΰΗ΅'ί”dϊDρ°ΝzΕ“£ŸR3Β·)Φ/(υžΗ9ΉΝωŽβ€²΄#+χΒ5Ίh³wF]&κΩ5žζ‘^‘τ+TΫ ΏI*–ƒ«ŸΜoωsˆΈ4cΔU„q·]}mcΏΰhWxiΞ3<…ε ώ0seαŠcQŸNίSά¨FQ@œΡE:θž^Rχ&›{3fx:²~ΊυΎ‹Ωe:ͺqœ¨ˆΰέVΰp[—Ης‘^Ω%ͺχ]pρ”Λ²ΜlUηKĘTΔΛϊ ΒΡΩ:%Ρβ1B2MMlvΈψWT/ςIՈυ+.ωHŒgŽe_žŽξx™ελ~X½iŽτu’σ½ΞeχމYΊΰΙθt|”\•Ή56Ιϋ/“yΚΈυ–“Q„*°ΨαρM‘¦ΔΟΏT%wUΫζυ΄Ϋ₯‘΄f˜ύ(₯ΰ@‰ώ΄“_Q ΐΐ/Q―σz#ΐΊ'_»Δ&‰5Q†qΜs}Ί+΄ύ–(ͺχ”WέZ‰^qy(›9β? G‘Σ’P€VΊrΖ₯)A3ΐΎεŠΰgbŸΈqf†›‰}Β'VαΑ/)zκδz—3©Ϋ k§ώ±T€[Ό―B§>±F_±=K˜ΒΓ(ψ‚_-V΅6Ι #ΌΟΟl`ω·NAΠ›Φ›kΧυ4H“RΗ'0ΓŠήƒ%^%xŒ΅ΝpeυK©¦Rγ½>θa5τθ©‘ΗI U±VI0 η3 yœ(ŽΎŸ-ŠLŠρ“Ίύiη”φΌGΠS[ειΗE~»ΆB–,mζŸ>Β nc4% +Štώ%š2Ν€‘^‚’—ΰ…ϋT=ζ&4΅6lcΙWlΞη!™aι­* ?kI6‘Φ•=–$›ΑΨ5εA4)΄δјš‰¬ΐζ1Εέ¬lM@TN"OE€Ž!‘=Η_šΊ8 ΑD3 ŒΎ{;γεdJ z}>d€‚'ό–†*Τζ5ν Εο^–Ρ'-y¦œΆ}"\?KΥyΈŽίΎζ™‹¨|λςΖζo4σ{ϋέ―Ρ.υμξβ”;‚vύZ†¬«‘ίφαEP(ς‚ΩŠ2‘(ζTμ¦}φ\K‹ «‰2>όιTΩ°1ˆρ=΅TΎcΠΨϊρ•98=(ΝԌ*ΐΉΊΣ‘^‘ϋ¦'P +U/ξ=XH›λq‘=νΑΆŒ˜ Jζk§0εŠΣΜr{z€Α±ζ„™Nc™ wΧ¦φAΐD_”DΡ…'‰64Œ·A³»μ9VΗB§zΉΡΞΤκε{*ΣηΖ©£=ŽΛΗjύ­,ψB­k"άϊRdΏu#ccπήκλq«CΉΖǍb…vά΄PEqktέ.¨/Ό}ETKD _LC‚|dό ©,FχwZ!°Γ Μbξ]sqΙΰ_Α(Ήcl³©+[£’ω:TΌ<«³­Ÿμα yξA;8“MZΡ-•bΓ.0|wOΟ¬.ΖG G;3υyž”§ςΟD4=”Μ-–c:δή₯t ¨`«ύͺ££k§νυžβ$6ΰ€npYNŠΧ’o_τAΘ/8B³ lg—ΦKxΈJψMΡ•Bzeμ‘: 9ƒ…1&~2,ΟΔΩgγp)»‰‚‚τMρxi[ΟΎ…LYȚΣ0©υ°F(Όy€qε^ ϊ!ς§τ‘Œ«#8€wΠ{™aœGεψ·Zάζ(ΠΌΓ‰Ά&MoΑ#š~θ|’Z»Š-χ +—Λ}υBΝ‘K ξ6€Ι' ‚F^ώ€γΜ—P9ζZΕΑ©hΔΉ +(ρOFΘψ{"mΖRϋPel›Su€Ι1±(₯;8<²―6nθγK2—‰ϋΰz0j€"Zθή£ΪjΦ¨p}‘KΐψόΤΧcΊαœa9€Ί}qԘΉΆ2HO‘°ΚΒ…A=―π+ΟH΄ΐsUf±ΚιOˆL²c?Πe'’ƒ=ΜLvb\EΞ@u,τ"ΙNΘdχΛƏΌ£NBvΞμ§έ6Ÿ4MNΔ'Q―/ˆ /’π=ΦΊ‘)υ’·r4VGlTς›_ŒŠη!*žλσeqšc²ΗαΥ¬ΎΛ}’‚ ug%O“Δ,€Κ-»ς©΅IόZΤLώp€ž*θ²ή[Ϋ +endstream +endobj +192 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F204 91 0 R /F206 92 0 R /F209 93 0 R /F293 579 0 R /F297 575 0 R /F301 576 0 R /F311 578 0 R /F357 582 0 R /F70 583 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] /XObject << /Im3 584 0 R >> >> +endobj +193 0 obj +<< /A << /D (ALG@line.10) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 154.261 670.824 165.334 682.197 ] /Subtype /Link /Type /Annot >> +endobj +194 0 obj +<< /A << /D (ALG@line.8) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 131.318 646.914 137.851 658.286 ] /Subtype /Link /Type /Annot >> +endobj +195 0 obj +<< /A << /D (cite.munyampirwa2024down) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 142.054 613.375 153.312 621.539 ] /Subtype /Link /Type /Annot >> +endobj +196 0 obj +<< /A << /D (cite.hmann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 155.581 613.285 166.839 621.539 ] /Subtype /Link /Type /Annot >> +endobj +197 0 obj +<< /A << /D (ALG@line.4) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 284.985 599.093 291.518 610.466 ] /Subtype /Link /Type /Annot >> +endobj +198 0 obj +<< /A << /D (ALG@line.10) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 282.653 539.318 293.939 550.506 ] /Subtype /Link /Type /Annot >> +endobj +199 0 obj +<< /A << /D (ALG@line.13) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 80.732 503.452 91.805 514.824 ] /Subtype /Link /Type /Annot >> +endobj +200 0 obj +<< /A << /D (subsection.6.2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 194.264 333.052 212.426 344.37 ] /Subtype /Link /Type /Annot >> +endobj +201 0 obj +<< /A << /D (subsection.6.3) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 99.748 285.515 117.91 296.549 ] /Subtype /Link /Type /Annot >> +endobj +202 0 obj +<< /A << /D (subsection.6.4) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 237.511 261.321 255.673 272.639 ] /Subtype /Link /Type /Annot >> +endobj +203 0 obj +<< /A << /D (cite.together2023redpajama) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 229.036 199.836 240.294 208.09 ] /Subtype /Link /Type /Annot >> +endobj +204 0 obj +<< /A << /D (cite.izacard2021unsupervised) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 233.011 152.015 244.269 160.269 ] /Subtype /Link /Type /Annot >> +endobj +205 0 obj +<< /A << /D (cite.together2023redpajama) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 407.387 683.352 418.645 691.607 ] /Subtype /Link /Type /Annot >> +endobj +206 0 obj +<< /A << /D (table.caption.9) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 402.158 657.206 408.876 668.578 ] /Subtype /Link /Type /Annot >> +endobj +207 0 obj +<< /A << /D (cite.shao2024scaling) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 508.485 659.442 519.742 667.696 ] /Subtype /Link /Type /Annot >> +endobj +208 0 obj +<< /A << /D (cite.izacard2021unsupervised) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 504.942 635.532 516.2 643.786 ] /Subtype /Link /Type /Annot >> +endobj +209 0 obj +<< /A << /D (cite.dubey2024llama) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 531.888 563.801 543.145 572.055 ] /Subtype /Link /Type /Annot >> +endobj +210 0 obj +<< /A << /D (cite.kwiatkowski-etal-2019-natural) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 516.126 539.89 527.384 548.144 ] /Subtype /Link /Type /Annot >> +endobj +211 0 obj +<< /A << /D (cite.joshi2017triviaqa) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 341.178 527.935 352.436 536.189 ] /Subtype /Link /Type /Annot >> +endobj +212 0 obj +<< /A << /D (cite.rein2024gpqa) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 390.553 527.935 401.811 536.189 ] /Subtype /Link /Type /Annot >> +endobj +213 0 obj +<< /A << /D (cite.yang2018hotpotqa) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 474.05 527.935 485.307 536.189 ] /Subtype /Link /Type /Annot >> +endobj +214 0 obj +<< /A << /D (cite.g5.48xlarge) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 443.478 488.174 450.103 496.339 ] /Subtype /Link /Type /Annot >> +endobj +215 0 obj +<< /A << /D (cite.mac) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 505.583 452.219 512.208 460.473 ] /Subtype /Link /Type /Annot >> +endobj +216 0 obj +<< /A << /D (section.2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 352.699 350.356 364.224 361.544 ] /Subtype /Link /Type /Annot >> +endobj +217 0 obj +<< /A << /D (cite.pq) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 395.757 328.683 407.015 336.937 ] /Subtype /Link /Type /Annot >> +endobj +218 0 obj +<< /A << /D (cite.schuhmann2021laion) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 410.419 328.683 421.677 336.937 ] /Subtype /Link /Type /Annot >> +endobj +219 0 obj +<< /A << /D (cite.zhu) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 425.081 328.683 436.339 336.937 ] /Subtype /Link /Type /Annot >> +endobj +220 0 obj +<< /A << /D (cite.asai2023self) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 533.022 304.862 539.647 313.026 ] /Subtype /Link /Type /Annot >> +endobj +221 0 obj +<< /A << /D (cite.shao2024scaling) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 542.981 304.772 554.239 313.026 ] /Subtype /Link /Type /Annot >> +endobj +222 0 obj +<< /A << /D (algorithm.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 488.225 123.208 494.901 134.58 ] /Subtype /Link /Type /Annot >> +endobj +223 0 obj +<< /Filter /FlateDecode /Length 5587 >> +stream +xΪ₯\msά6’ώξ_1΅W©’ͺ4\β•dj―ξδΔvΌg“X›|H\WΤ ₯αy†œ 9R΄ΏώΊΡ ’ΰpdΛχΑ@ Ρθ~ϊJw‹dρζEΒΏ/―_όυ΅LΔ"s+νβϊv!βDdπ2Yˆ…UP­©HβD₯‹λέβ·θŸυΕRju› 6jκ’j6E½Ύ€βΓEqm±ίo©Έ~O”)YTΦ]UtΠ$Φ4ΜΊΌs/°ͺτŸp5νfyραϊοnf<1£6Οβ$“4©f»n±Ou ώ¦QέΈ±KΎ)ΪαcΜ™ZΑ"ͺU•mWνΖSΚ’j·Η§ζΠυͺΌIS·χ{že­pMŠ&˜Δy6š`fc!™j«b T°IΉΉ₯bρp‘ΣPλ㊫m±Pw7P *LΤά‹US#Ω³hΥUMέΛΫζ@owMΫράΐΝάDS‹”)Yΐ +emΨA7ΙUτ{€Ζ1mμ˜mxΆ%<}ιύTΠAƒ’垎H=ψχηγάIKd¬5sˆΣΩRš¨ͺYμ±tHCΈc ΅mΉ%˜Ρ±ςΐ ΝώιCgβ<‘αηf•Mpθ@‰δ“N_}ς€ζ ΦLΨΙ©˜εXŸΐγ r±Τ'D„WΟΧψ&σ’ψ”θ&O@2πFΏΪν«C5Ζdθώ\@Ο­γ(w›’£ήA΅Φa1έσΉ†cZ;1ιΪςΈ7 Ώβ6·τ‹sNόš4‹bΪM‰£’€ŒJDmuW;δΉ!ΰΎmϊ>ˆ$[ͺ£a‘ιŠ-u„DY&π M~i€;θοO(GŒΕcfXŽ0 2I΄`€ΰ€I’ΈeR‰€Γ‘*AΜά»@e±Z`=^˜<:Ω-Ά:ŒΡ±WbdtψΥh5γŽd0;’~FΠΐo¦ζ΄0…UŒ’„π+Αο±»k–§ΨφIΛΨZ¦@Ώ\†γiΗ :ϊ# OU L„-/M΅TΈ…qν[ΡΫg θwŠ‚Ο©£’8V™κα1>ιΘAb¬Ϋ™vΘ«΄ΕΝΆj7τxS­«ž9Š-u8Λ5|¨Ί ½¨‘7Ά³τYœšžήH·±‰6|SΔIΏ΄Υ9pθŸΥξΈσ΅Ÿ3‘β ί`ωτ–@]0ƒΒ^(ά.„€­†)τ[=# +ΐ#yξ—ŒΜ ŸΙ"κ” ά@uσ$φ`λ•…“ψ$Tƒ•ΪlΙƒbψζυ¦bΜ;’‹jW›¦Z1ϊ)λφH{BΟhΎF› Υ=ΉFΕjγΑΗ‰9FΤΣ9€`­i½ΥΓ*)ˆ°Πμ=ˆ:ΦUχΘoϊfͺ VΕ’ίρμκ73;©3 Έ—ι²ͺΣγn”φŽΜjνu +4¨‹ϋκΈ©ΆnΒXCβΚ MξΕ~CέܜσIa~Ši³q½σ@eαΔ#”–?mYΰW,Θ8ξΛOˆlm²8Oy½?4“'9kP‘£δ­Zͺ+ΆwΝ¦Έ£kGœ’_"ΊκžΜ½PΑχkwxΆ^’Pγ‚ΘξΉ­Π)°‰φΰήΡKΒ°Žά&z€ηu…zβζˆb+¦ͺοJφύ\’b©:ͺΕK…pW‚FΦ«ͺd• 5$‡D΄-weo.€£y)'6c½Ο6ΐ%Qœa$θM©ΔdyγζΐŽmYΆ=:.§ϊΉΨXφή{%ςκ@YV펚ΊΉΑοM±]žpŠ&”7ΊV¬Έ<Ή ό›+ /Ÿ2 9Ι€pwƒΜ ;#Dœ#$YΐA€Βξ¬αs·1I’Wh`ή 3#~r3LXe:^/Ώ­I`4ΗS…ΐvI/{Έ‡+h{_—Ε%ƒyΤ›p'Ζž‹²=‘ŸJΡοΒά†ΰλ‰)²XhΙY‡Χ¨N<“X4κσ…²`Τ{mς7Ϊt:::ψ4²σGτ§w«NωΩΚSΠβ]Ήln—°ϊeqΐ™ΜCƒ•L³@§*κ%'³ «*FεaΗU„z °Ηθ‘˜_·›;ψjl!OƍύlXۚ©2αΏdΊ'Tΐ.PΓή0x΅uμοΆν ­?SέFΏK•ΪXΞY"a°΅›t°υςtλεdmΤpγUnFOΌρXτœ!Μ±†λ+vT bζ#•$ž1LD7»šΜ@€aηυ°PW­Ž ΡHΘY€έe}–ΓlΟat”–3¬Ψ·Τ‹ͺφD―πχ0ήΌ^iΈΑ B–Βίͺ^γώ‰OΉ›©E‡«λXϊΥΥΗ’{96ΔΒGμόθΤy=ΰΏυrˆΜ°ΜΐZσΪ€g»/ζυ…Ό£β³?QέΨ σΘD₯F’=²‚z eκζθ€<”£ΖξPΓ+/j ˆ‰τf9K-ψ΅Βλ^Φ:UΗFOγ߁œο¬Δœυ‡·W1§κ ΔΟ—Z}Žπ€£mΣ“_J8’U,XU½/»γ>λΥυ‹?^x:δ ’J ζθϊΥξΕo’Εή!·+ψƒkΉ[c`ΚΫΕϋ?Q€.œŠHD,‘(…BAŒ9‰G­Hm"Uθτq ―ΦMpvbIΨ ηNlbω‰‘s^[šΪ·iˆ¦i2jΨ8w‘FΧξ°ίy¨>VsͺLn₯₯oωΫηnρ©όΑqηRHΓΘΑQϊΉx`TδDI€œ\eQjߐt +ϋ}³9ΦǍUšΚHλ1ΦΗ²žλχΜό·μ@ΐaP’[8M6a―J΅έϊΤ€ϋ+ο½ρr›³‡(cΰw({έΖPθ³Ι'Σyς}ΖLΦΥ :’ΎxΆ< +1Θ9ΥΨ‰9 •j§ρωQEoθΑm64υΞ@α›NΞΩΧTϋΓOτϋ\I +Spx‚)*q^ΙaRˆΑ = +Rόφ5*υCu_-ηYPζq’}ώΡO2žkJdŒy¬υζΗ/PŸ°Ηθί5·ξ‹Ύ`³yΆAοc‚΅F¨Xψ<*—ΗTΆέXΎΗŸ'*cay8`xIΙrt"JJ‡ {ή΄mWξ¨’H‘dΰ§ΖΖ5UwΉK±‚ς†ΞΚrN(d0ί„Οκzπ.NmΏ-:t±»Δ-ε‘)†0ͺΦΟ‰~Ρof’_ή~ϋφŠ*DΒ™^A{œu›¦’ή΄‚3gVƒΜyŸ,‡_Α½@ϋ_ίΣΣ¦ΘύI)WQΥƒ›?ΫτΠ`A€:ό°>{΅Ξέ–Ν‰wσ>·αzγ%<‡Ώς(·Λ[kgԘNΣΨ{hΏωρb Μ― ©]€A€‘τ<¨g‚ΰJƒxΑEΖψ=Iδ'ωΦΐΘƒn*Ξ_'q4m<Ή MτΛ;Ξ {ώ[<¦<»>Νm!·ψΙ*½ρ‹›a d]mDf>g‡ i"©ι— 1Ϋ‡jvερ>‚qXΛ&€9/cγc8Ψ§Μ7x|W¬¨PΦχ.bΫΤh\žεW£bΕΐvξμ7ύ½³3EΔ€Š?ΓΣ«o$ή jFsΘ䘱αιΩ`έω‚ ͺ'Œ(#δ·v\ ί½-‹Ž2Ξq*(%ε“ͺθΜΪθjΟ™‘)­ ͺώ΅ˆNUϋήι΄*Ϋ–<–όίΐTW‡Υ>σκwΕκŸοΉΜϊ!Ž]΅S·ίΎ9Υ§Aυiι“)%±J]νŠϋ$W/ίSεU³=ξϊΠιaβη'(ΰF>ξ„CΞ ‘@hΆ0ί9W\{NzDΫ,θ;Έ‚’ Z…nΠ@5z‡I#μ ƒT>hξuΚΉ]E‡Ÿ—nε2ρ‰G}ζhξ©"GΆMϋ5UŒΘtI5>Œj…―‘ν”#Dlι– +T!€ŽΈΒΛώ“s\§πΒƒυxζΤDr€Τ$η§τa+†Φδ=6QΫsα2aΣgνΒρG8ŸκapN¨ϊΉ7χG’Ο„:k΄+wΗD)δυΨςκΟυ?akŒsΙ”’₯IP2Τ―KΔ!ζc. +>ήQ:|½^‚Ήδ³‘ΆœrSn{›Θ§‹ι–κ|œν¬MHέ ^lΖn!Ό?ζ(¦B=ξ‡K &:ΦΕ}QmΣΫ_Ξ2‘gν…A°₯Ήwϋm%%Ή»?«’žž©7TnγΤ¨π2?ˆ@Ο Υ‚φΟΜ~Χ ~΄˜,Λ<Βdƒ\ό},ͺ$MΓ1Rsώ“ΚΈ ^ΠήY`’OΕΑ’³3±ΐ^ >Uνq‹z_ά:NάΡƒs,ΧOφ~3•y9ΗWœΖ’Jg}Jf1ΎΫγ>²§£α+‚ύΰ‹0xbzυ–dˆΰuB€n(Ψ»φς@S‰‰³LiΈ*Aϊd¦¦©Z„έΏζy:vΔ6ο½yζΊY?+k]Ξk0¬:“σΪwJ5ˆUv +³§pEμβ†Vƒ‹Ϋ­48΄tΜδΕ†Ο…mJ» ¬ΑLžp ¨ &Ά~ζI0ZΕy:!ΨSwξR«°ύ‡>E;ŸͺC™Γψ2 Τ‘>Υ5j¬kTL­ύ}ǎœα©ΒD)Ν^G.₯yu0½TŸG:‰«žΜ2@ΏσEΥΆ1Ζh| ˜ε폳ή5΄±”gΛf¦#£ΘάZϊ|ΰ$Μ【δΰdΛ'@¦‰c‘Α?1δ,ζ=ζγ°KΦg“Ι‰bΊSΔI†'3τΔ4Yœψ»αΦ튎­άΧ_½σξc$rπFΏζ7νj Ÿb’8’MΞTτꝯ-ϊTμ3ΪSj F|δj–›ž%ΩΜašczΓ- ε―Χ@ Ώ4­†A§°γ܌ΰΤ γο’ϊlNόB.½@·lWZ +νIΊŠ”{rε“lΰ\ϊ¬‹Ψ)>KPν¨šΛ1UsΜޟtRΚXδž`C’»υΊ)»¦Ζ ¬†]¨©δθgΗ‘…UE©ψ–φάΚ(HmΌ€.\ς²e:/‘€(’ύuΜκλΖqν’ 2b9&Η7†¬IŽΚΎυ1υ“ό[zpι‚„ŸE΄17v»@•ε9[ +ΜΓXϊ(– »p`n|#ΘΝ&‡mί{NΥφ·†| κ°v½VO.…Œκ.Ω―ι͐&|oˆΜ‚ύ/Ζ²šώΚN@4Ερό¨lι4κIΛy•Υν0RqΪeqπeΑRΖ<Ά(κa'5ζ,,κZ₯μ}sΗ.>U?,-…•±φ©Μ―›0EzΦLγt_ωtξ΅Ι›ΙΘΐ¦sJ˜,{;Ίδ^`6œmˆφFΪ«‹“Όbj‰ΡnΤ%ŊSm#Μ>±™¬;ώπΘ|»-λ;Ξ€ΡTΕή#h˜ΈL"s<Ÿ‡S£ρJΐΈsxa +#o%μNσϊU—δ³/ρ§§‰ώ―#€½J²|k’μΪαψ{Ρ’,Ÿ, F‚η+ΗόWm|βΛ4=τ‚.ΗY +endstream +endobj +224 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F204 91 0 R /F206 92 0 R /F209 93 0 R /F212 94 0 R /F297 575 0 R /F70 583 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] >> +endobj +225 0 obj +<< /A << /D (table.caption.9) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 548.941 457.93 555.473 469.303 ] /Subtype /Link /Type /Annot >> +endobj +226 0 obj +<< /A << /D (figure.caption.8) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 355.91 422.065 362.627 433.253 ] /Subtype /Link /Type /Annot >> +endobj +227 0 obj +<< /A << /D (cite.hnsw) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 157.187 123.123 168.445 131.377 ] /Subtype /Link /Type /Annot >> +endobj +228 0 obj +<< /A << /D (cite.aumuller2020ann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 155.807 111.258 162.432 119.422 ] /Subtype /Link /Type /Annot >> +endobj +229 0 obj +<< /A << /D (cite.pineconeHNSW) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 164.248 111.168 175.506 119.422 ] /Subtype /Link /Type /Annot >> +endobj +230 0 obj +<< /A << /D (cite.faissGuidelines) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 544.185 359.943 555.442 368.197 ] /Subtype /Link /Type /Annot >> +endobj +231 0 obj +<< /A << /D (cite.ivf_crtpt:2023/1438) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 396.283 347.988 407.541 356.242 ] /Subtype /Link /Type /Annot >> +endobj +232 0 obj +<< /A << /D (cite.diskann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 374.836 311.33 386.094 319.584 ] /Subtype /Link /Type /Annot >> +endobj +233 0 obj +<< /A << /D (cite.diskann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 492.239 263.509 503.497 271.763 ] /Subtype /Link /Type /Annot >> +endobj +234 0 obj +<< /A << /D (cite.seemakhupt2024edgerag) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 483.895 179.823 495.153 188.077 ] /Subtype /Link /Type /Annot >> +endobj +235 0 obj +<< /A << /D (cite.pq) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 406.159 132.002 417.417 140.257 ] /Subtype /Link /Type /Annot >> +endobj +236 0 obj +<< /A << /D (cite.msjimmy_bm25) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 356.321 96.137 367.579 104.391 ] /Subtype /Link /Type /Annot >> +endobj +237 0 obj +<< /A << /D (cite.rekabsaz2021tripclick) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 369.485 96.137 380.743 104.391 ] /Subtype /Link /Type /Annot >> +endobj +238 0 obj +<< /Filter /FlateDecode /Length 3720 >> +stream +xΪΕZmoγ6ώž_aτp8ˆUρUξ€έvΣnΫνu―Aϋa7΄Ά ±-Χ’7›ϋυχCJ’,'ξbŠ"‡ΓαΌ<3T2»%³οΞ’Ρ/ΓdΖfJΞt’Η{‡Ώ1Ν9ˆΞT_]}}Ιςl–ΕΉΦrvu3γ9%Σ3•«XK>»ZΞήG•œ3h°σλ«ΎΎδ Μb<KΨΡ/YbΖt«ΏΎnJ±$–Ήxφ¦Τ£›Pά‹ΣDΜdγLΚΑ¦ζΨΥΗGv•©Μoλm±Άεi±³Έ6³’8ΟϊνΚTΔ\ˆΩΥ$.«ΫΓώ<‹J¬›'‘ŒιχύΫ’ΪRσŸesX·Χ/zv& 3•ΕZρϊOE[nšΆή·n™v_,Λyύ!ͺ‘žn΅—ηBEίΡC±Ϋ­«EΡVυΦ Λς¨XfλΖΝΊ©ΑΌm-‹ΆhΚΦυΫ₯[θή §Ή«ΒΜ]ήƒύ.κ-˜­›]ς8&6“šΕ<ΝhWW+Μζ2ζΕηͺ‘v³ͺΟΣθή=΅~H·mσPcαOfρύͺ,–ΤΉ, [ A›ΊŠ#*ΥΏ<‰›Ρ«—?Lj»,ΑΑgz²{\cSŸΞσΘ mλΡά}qοx€τΖ+uΦx>Ϋ’ZΩ4gf~―ΞDώρœ³(&}Œ•"K‰™šέΝΒηΟfον<(/ι?λO@χG 8Kζ°h§XΏŸgΜnKΙθSat@EζQΉέ‘Ώ-φ·eKmwδi΄(ΦkχΊ¦_#΄OΕϊU%kRZz{€TχΤ\VF_KKj;€Ϋξ+G„:‹Εϊδ)¬ν;{P/Ϋυk΅]ΈΕ&³…ίmνqfπqνζxΞ_½εκΒΝi­ΨXœ$Η&E ’Θ@Ε¬S{K?q ›’Η¦Ϊή +‰Χ΄μœΊ²ϋΕX{δθ.‹ΕŠZΞ|μ6XΕΤϋhΞrY£ΒΖΞU]‰{Ϊ±Uoͺ–ZξΠ’θ—wσE½ΩYzMΣk[mΚ&fž/όγΧΣΈzΊ-‘„’nΗj·ˆΣ '1>W5œvt4ˆ±U½^Y,ΛfW‘ζθθ#τ² ιeAݟ:Bpσ…Ω–ΆΫΒK‹uiΰ2¦+»†(―’£ϋ-=‘τ΅qΪ_nh˜§Ζ†vΡρ›Gu»’'£NŽο­cξ—w^K‘8& „|r˜ΝΤ{κYZ'ΈmZ’ό†d<0±T^#½@9βΙ@6―E"βDϋx-₯b‚vί<±υ£{@·‹Ψ}¬Γžρ8u±z8Ζ)§ψ.pšΗ'βa} *_쫦vS­—΅ΧŸ‹…³Ό·Eλυ؏·vΥ‹…§IœfΚΕuFӚEM*K7φ Π8:(KΩD_i’―5’’Ήσ.’»φΝͺ³rlœΝ οΐs=ΰ θ γ9ρvg<βƒυΩ6<Ϋ°l€Ϊ—6:ΣN…AF-M`Έ 'f6"η&{Ι…€ˆK`0s†9B©ŒΘ,IΕ»uXͺ"Q3·Ž;`Ϋ>>4mΉ‰iψ?<Α^FτlόΌων1ˆίΐrΒ5q!γœ9 Z–Γy,VU^\—ωQ}πCΧΨ‘α΅U0ΌΚ“?_Pλ~eƒ»»r΄¬ω£ΓΈ¬u΅u½ΰ{B†œCΉ o+ΛΠ–`Q Bκ²’%†%«MιZ5 θ]·ι­,²CcUέΚ¦₯1ξpšΚ†Fσ:Ψj<φ>œ“q2όΎάχ‘h”8l²X‚ωΓτy—Pό΄.6ΕHwΞ^MΩ·BΙR?ΪGͺ.έ–Ϋ’'=oH/Κυ΄©Ά'NΤέ9giΈω κ#Wžeτ’Άwγ‰3ˆ@)αT<χ­ͺ “'τϋΗ‘ήiΖ²eZΖ‰χ>‡ΖΕΐΑžΙbαΝ+'2=Yš!2h/2iς „"Χ‘σD”w# vsΠÎ ½φy–@Zgη’Ρ”WNŸ›)κRΒ(O@3ρNna½t―)?½vΰςnΏi½{tnκυΪϊY’^Kš[ΡΝέ:sθdލmWsFf4’w¨½Σ'œf ΉΩAΖΗ‘ƒΔ,€ϋ!aιhόq†(έajX²Π²D"ηΤμϋŸύ"ΌqΙΥvΎ)7΅‡έ,‹Β|uš.S2ΦΉ Ώ2ΓY&ΛF4Dβ›`QδαΔd‹cϊ]UM€Ψξ+ΨβΓό0ˆ/ΐb-Μb>φ}^ZPJž:ΧRίΜ‘σboύT2ŽL¦«ΟΦΜΣ³₯€€•ˆpQ~,„N,–B…Γ/žΉ€V1—2€!ΣΣk¦2Vp΅Αψk™1*n3π”ά­m«±”&C[wτ8ΉΞyHˆΏiβ7εgs”—π[OT)όi% rI§ Υf·.7HιœC6ΜάWνŠZˆΞI‡E> €y–ϋ†ž{·[o6ζ²hy”ΖΊ₯³ )™γ£K/Ν>^L9EΏύ<5rίαδ―άψ<Žg±ΚΊ(υ7O2IΓσΔX'”tErJ†έ,U’4˜εΥ)ώ™„wα”ΞΕΟ<#  0:ϊϋω\(°’WΆυΝωΖχ“θ[λq8ϊ΅λΟ0Gts&$’a*YώΕα\Ήζ£ŒgO‰„#ΣN£‡Σ.|Ω AΘ[Έ\ώΖ’Κ0/rš¨ŽτΘ…uB:œ4'ˆdsr―€“Gf!² Œ²a4‰e…ΡΗ°q|aBΩ*ŸH2x φŒ(γφ,8Ξφ,2ΩγΖ7&HFΏ]’—˜Š4飑ΖΡ–°ξT§!νd¬ΆvgoΆzΪ•2$ukχΪ,ξΩ±KNxνΚ9€Βϋ 4Ζρ‘Ϋh*zμy$p1λBΊ― tέGQƒΡϊYτyΛzΧRχγžT%:ΦbΔΠΐ“Ύωνς”#5虏χ…ΰ’¨IWζΉ QΨΥI<^Ϊν‘εV‹.Gu&b‡ZΧHύΟ ’J*@2¨FQrΈ#|"₯'\S±nί2“<λ½ί\$0Ν}eΞΚ4)΅½3μΉΌ‹<΅Q.X„«#ήy§ρ:VˆΞΑψλ‹ž‘’ΨhLͺ`ϊ¬1νaγNΓ±mΰΎω]ΰνATΪ9΅Χ 9‘V½\€ς€Λv–γΧWƒbŒγ”—#‘ZlΞή_'³%^ώ`"fχvθf–!‘@c=ϋυμUdBWgHεXJ ŒοœΠŸN9αΖι`ΦΐWί―\RψH(@6‰fαό§W…/RY8Ιz»Š΅+Ύ+—Ÿ_ΩϋΆλ¬^Œ~mΥΒ’(ΫΓΞ π;ςΫ9>( b4ͺwH6͘ +ˆ‰L )t2%žCΊ¬ι«IοΑbžw‘iΌ!WΤ½_UΎΜ΅¨χ”3RΦΊυ™P―‡Αρ”δ\υž±θ―ηsψ ςLL@Ξ‚Ψ£!‹uΏ»Œε|rƒˆ…b:žŽ­ E@ΧεσΣ΄γΐD,₯S©o«ζΞ$₯ΣΪΨΟM’sΞ}JUœ‰ •eύ’™²Ξ)˜@©X1Žζ #ρ7·ϋb·FG3ζ¨Ϊ7ΐJ'c0€ΜœΚSmατv@WΦψ(ϊf UJMΓγmK–¦¨*•­hk‰¦χMKΏwώ—»ΖQήέ΄‚~~yΧΥnυˆ=žYruΊξρw.ΆΖΪ!td$€7λΊXϊΰy°%>΄JοΨν¦–Ύ M»1Υ|_ρtzηjΞΫSbγ@¦*u.}Yn°q?Hϋ=1νA3χ}φj†₯”N=βS%bužΚp₯“ N*Ÿ„σž.Π?¦θκδ)ό˜¦@βi8λ‰GζkP­`Ξ&Ájj˜ΩΘ—<νΖN€5ΜΔηΞM_LV²ΤdΦλlΣbΫ:}S_\ά8vΆ`π¨5‰θ3]θ”γŠ[Ήt9ˆθŸtwtf‹KγΠRέε2DΗvυυύ]τ…FŽCCeX4my MaΙ;+ό‡ω)0Κ%2*Μ Ό6 =ίψΫ_wJζ½±ΦuΩψ•<’4H, νUIΏΠDŸΤ‰A=ΓY”L™%MΑ΄,–CΨΙ¬+λvΰλ,œΟβT9™«Vέe\ψ)„Ζ“πa(l{ηh{?Oέ'ΊO`!χλλΩa’Ώ4Θͺγι8Νƒ >²έWc€™ΪeܐΏΣ“Ψ’©E„άUΑκ…― Ÿš¦CΔ »οΎ&4₯d κv·―όηh^ •ϋ|‘lJˆO·Ι>¦ŽΩ1ίΰ&N6γrXΞjλπ―ŠJΈ1υ/ιdcΏKz± Π*ΓiΟMgaζpΦ &Nήo `n•‰`ψΕsh“²€„ΜO―˜ζq*C―mU;7ΉƒΙ-λΖΆ0ί!š^ϋεαηώy_lο¬Ψtt} Ω6τnPΌkvS+‡ `ςƒž0adηΗΔfH`R‘Qe„Ηζ+9OβL8ΚΖ_ooC› +endstream +endobj +239 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F204 91 0 R /F206 92 0 R /F209 93 0 R /F212 94 0 R /F297 575 0 R /F70 583 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] /XObject << /Im4 585 0 R /Im5 586 0 R /Im6 587 0 R >> >> +endobj +240 0 obj +<< /A << /D (figure.caption.7) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 69.469 691.746 76.001 702.934 ] /Subtype /Link /Type /Annot >> +endobj +241 0 obj +<< /A << /D (subsection.2.3) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 275.967 643.925 293.967 655.113 ] /Subtype /Link /Type /Annot >> +endobj +242 0 obj +<< /A << /D (figure.caption.7) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 287.622 608.06 294.34 619.432 ] /Subtype /Link /Type /Annot >> +endobj +243 0 obj +<< /A << /D (table.caption.9) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 236.761 177.903 243.479 188.07 ] /Subtype /Link /Type /Annot >> +endobj +244 0 obj +<< /A << /D (figure.caption.7) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 80.074 141.016 86.672 152.204 ] /Subtype /Link /Type /Annot >> +endobj +245 0 obj +<< /A << /D (cite.wang2021comprehensive_survey) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 376.11 696.971 387.367 705.225 ] /Subtype /Link /Type /Annot >> +endobj +246 0 obj +<< /A << /D (subsection.2.3) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 429.027 623.059 447.512 634.376 ] /Subtype /Link /Type /Annot >> +endobj +247 0 obj +<< /A << /D (cite.ktransformers2025) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 396.909 577.509 408.166 585.674 ] /Subtype /Link /Type /Annot >> +endobj +248 0 obj +<< /A << /D (cite.li2024svdqunat) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 411.914 577.42 423.172 585.674 ] /Subtype /Link /Type /Annot >> +endobj +249 0 obj +<< /A << /D (cite.appleM1Ultra) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 413.606 529.689 424.864 537.853 ] /Subtype /Link /Type /Annot >> +endobj +250 0 obj +<< /A << /D (cite.nvidiaA10) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 427.554 529.599 438.812 537.853 ] /Subtype /Link /Type /Annot >> +endobj +251 0 obj +<< /A << /D (figure.caption.8) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 420.899 463.601 427.441 474.79 ] /Subtype /Link /Type /Annot >> +endobj +252 0 obj +<< /A << /D (subsection.6.2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 349.543 403.826 367.381 415.014 ] /Subtype /Link /Type /Annot >> +endobj +253 0 obj +<< /A << /D (figure.caption.8) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 411.666 367.96 418.291 379.148 ] /Subtype /Link /Type /Annot >> +endobj +254 0 obj +<< /A << /D (section.4) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 520.816 96.976 532.125 108.164 ] /Subtype /Link /Type /Annot >> +endobj +255 0 obj +<< /Filter /FlateDecode /Length 5942 >> +stream +xΪ₯\[sγ6²~Ÿ_αͺ­ΤU–Έρ’·™“Μl²™άΖηδ!Ι-Ρ6Λ©ˆT&Ξ―ίnt)ΚηΤΤ” Fχ׍†Σ«ϋ«τκέ«”ίάΌϊΧ[YW₯(3•]έά]Ys•ΛT€:ΏΊΩ^ύœdB]―eš¦Ιϋͺi―ΧʦɏuΪ =ΦτσaθŽΥ}M…ͺέΓ7ΥP·›§λ_oΎώΧ[•Κh TΐΈ)ό“8fVZ‘–ϊκfcΎmξvy• +kα=΄^=^MΛyuυσZΙ41Ԙ'ε*er8^IέΧ­#Z 5=τThΆιΪώ΄? MΧ[’ήΤνv=tλšΚ0λΧΧΪ&οp¬+)΄Œ¨/΄Ί κ;ΥG`™Mž ŸΙ“σΐͺ ’Υυ=w;zΈ­ϊzΧ΄5•ϊ§~¨χΎ#Ύωςυ·ί +zΎΙ0!*’#—BΜEš?/Ta“ώ‘»Ξ“X0ΐŒj jχUͺlΈ%q +*ΊvχĝEώ5υ54Δυ:OΆ§Mέ/dK‘3fδ;~€£ί]νxQdψΩ–κμgτΫέM聊csߴՎJΗκ#χ¬abπ|ϊζOnύρ‘Ω—@ΦβΥ2ΉP9siR=ΐ¦½G‘U4§ͺοΪκvWS•_A[&+ͺOoψ‘x€Ϋ¦ίœϊJE²₯*Ψ2Ÿ*ΘΠϊ₯s%τLœaPΗΩhΪ +-=[OD +ˆ.oΙjσΠ K~ηI₯I™~FaΕ6$zPΥρ~χύΧͺL^SΙουS»EqΖGE?=υοΪm/ Ί#—2‘T@aΊ‘ΒM :VTt›Ί; πΠΚ Š2,(ŠDcA₯&Ν‰¬QhJε…¦”Ι/©Mσ φof’wo $WτβΝλ»ϊ8Κ<Υ;A†O8JαχŒD'ΠЏ‘ˆ·Pσ"=ύζz.ω―ΗYΪ²Ζ2σ?4νΖI§a]ϋzU€2ΪSQ[AΜΉ’₯_β§…MUνωΥ/©Φ„ωCΕΆ*\CΏ ŒΣeΤ`‰Ί"¦δ5 qμ8Ώί½Ύϋ©yl¨Ζ}ΎV‘asd’Fƒγδ–,ι―erΒ‡ B–•nσ~.­HNn…5Μ,”Ύ‘j VΥ6K*ήνΗ*Μ4K ξΊ£ΣΤΨƒΜžυ& s&`œD/¨θ•7’Σ»ΨC΄ ŸοOa,νΥwοWΖι`¨χ·~βΌ +ύd½u²­ΫžωTοoњ³oA?τΤ¦Ϊu +ζόΡ"Σπc₯ϋut}lΤ6&γE[t¬\ΧΗΣf`²Α†ΨΦΥ–tφλΈνιΦq»©ΨŽ+ +oq²ΏγgŽλ₯eΣ₯ΘςŒHz€―g%X‚/šώΡ™3 ϊDtμιEν?Χ»‡ζώΑ­Qα‡ΕtΔ/QέφTΣRλ>6π—Xύ­λcU.rΕΌͺvΝ}φJ&a%z¨A)ΘδΛΚιm¨n;Χ¦ΎφϞ›‚Θ¬θζ±;1°θ :|ΰ|1=Ar\ΐ +VZ‘g™,›z…Θ ν€ΟνΣPχ¨ͺ'€Ι«ι{Σ]ΣT,ΘRͺmkβcwμWA±©˜ž₯Ο±F%”~1kΐЊΌϊόx•fQξS€"18δ?.9βaΞE&ΜN§θzAθΚ°εΪΏ%|\’U¬w0κWTtΠu}8υτ0l86·§‘ζŠ]3 ;aj¬Ε’.3Z +U0ρo»#-ό›χΚ²χAGMN/:oυPχΝh―-o@xTŠaGξΣνΙmvφσQ* ίCsœΣΗ$Θ“Ž9¦Ί =}°‰χ>ΈN½υ,ΚPlMškΉπ0Π*άs έΏ’ΆαλwΡζ Τ’»N!μΠ=Φ-ξΜ ?dQžζBη LΏj ±€x= •« * ~8ΆΌ£~•Ή₯ ͺΌKΤ²Tο–~7έώPΩ6.VpŽ΄ti…QΚ³ΣΑa°Jn0ψ₯Α°bŒ@iΣ'g +ς4„> >ΖΞΨ‰τΈ +˜V΄? έΠlΈeμΑmΈZ-/lεs* +ŒUώLtΨ9ΫΡ OŸ³†€‚dŸ§€mŠ*ιn‘έ8qάxσpjϋiŽ©ΉX/ύ³%θԐžDF˜¨<‘δΑνžvέ=(ϋαaο£Γάcgž‘pa³ΩΈpκβι2™ΆœuZ5 1pl΄‡XuFqΥ?·¬ν ’šΛΤƒ? 9[Ψe₯rΝ²τ9v_Κ§Y`ΕΏ6Y‰_W1Ασ”lڝΌϋωΣl₯Ÿ•Ξs!ΊΙw3;΅‘1`†\NۊΈ†ο08[7$.DΑ¨HΙ4ψTυΤΛy ³=¦ ΓIΕKξΤξ²pΆά˜‹F•§[n*Ί=ԝΪmO厷kˆ`a΄υλ‹δδμsε›pϋ.Ӝέ|,ErΔιXpΑ3\±D^*<,¦ ΰ ·€π šΎŠ°+:~a3±DŸΙσ«,SBIfΧOŒΎ35Ζaϋ«-acw{n0N2Ι~ΎŸ$djrv(ηΌ±€ε}JR_!BιW©2>ΰTEi3ή₯τrˆ\¨kG=P‘4ήέυΏ›M}œγ>χ€16sʎ>"œξ›ήb”qΌ{}Δ³9B_ +νΟΖsυwρ‘N+άΑ^Υ΄ΕFΑ¦±2:qΓτ7o΅άο…„ͺ1Ν-#?ΐΎ$Ν Ύu!ΝνsϊϊŽŸš(έl½ 0Ÿ tΔ 'ž±‘Uˆ«Δ첚Ωε²όΐΕXχ`.όΉ4Ÿτμψ€ˆŠ§=GFu⏖š=Ÿ&©yδbzE€+‘ή·ι)/ΗΜYδ‚ςΎ³ΔέΜ~Λ–\Θ~ •Ϊ³IΖo°n½ƒ‘Ή'˜o8ήΟͺξ’„ΪTαYΖρK?OS$>Ά9fΡAε}έΦΗΊΖ·Ρα,«Gί=ŠΈΡgZήƒδ¦η~Ω/κ:›Dτφ°#ΥIGEξ—`‹ πAι₯V§„1ΩR^΄:x>jm|υ²!D3?ύ„6—GΠ/σ…ΏhΆϋκΡyFΪCXνΠηx‚€£­s‰½ Ρά†ΡΡ|b¦oΘ~Μι„΄Μ‚ +‡ͺy-Ό₯„GgΗjH5h;¨ϊwΦε.Qbπz;>YΘΥ9ώsπ7‡^^Λ”γΕqЁ?P]ΘΒ lv0"›Π ž°g;ΙI‡K.RŒΉΑ]ΤZMΗy6γΚ‚JΛ =肃€΄(ΐΗέ–}ΕΈ“6P=퀟ε‚.Α S/ζ¦JΙιœg3.\fbf^Κ… Z0`–±χcΘ‘SΪ¨'Qμ³QWYƒ‡χΥfEO!HZPΞ_ΟΥ…*ΗΌUξžΣM=Έ9^Ι,ˆœNEΞϋζν7ί}΅φyd4¨₯Ÿͺθ@›©Ε$σO‹b₯‰93ήΏ +bϊτ6 `.°Ζ ΠAŸ† %ϊΒ…’Ι}’ΧΧR%›Ν 4>6ΤiςΕ’DΓπυ ЎΖ[KιΦٍ{sν‚“Ÿ ˆU hΦ25>…#ͺ…ŽŽ_ψ’Δ‚AΖ •K#ƒ§jœE‘ +ΛY…ˆ>\4‹N:ηΉo^^ (eΜ³€3[ŽIpftΕ9ΘQˆΩZφ°ν £”6±K‘eόNwœ$ΨΩ8ΑŽ) η՜ξοΡΠX½mS„ŒWΦL‡΅&p·ο踈(θΠ™R΅™@3 +Ό˜δ;ΚO1Q”2ΰDχ;ž„Ž©m‘e48ΓeP˜Σk~ftšιИVΛΕwτͺL?S vό >"Εfγ9e…΅}ψjΩ0ν)υ—Ψ§1ρ9²εsd'e“cbδR5ΐtθΪ.C›Χ;ΰδιώJΟ­’,Α/ζςιVιde΄ GψŠβ\bv<ΈΦ‘52›ΜOYUN17a5½t’©εKόh~ ޏ;Σ€τ-dΊF―΄q ―εyΒ ΦΝOI).εξμθs rς@h~Ύηη‰ιQJN#šετ"BŠWsRCYζœΡΒβS.)7¨=w”ρ¦PPܜ–Bƒο=fΎυuπP8SXΐrOQόpͺ7Bq<Φ₯8CιΞd&ͺήΩφ†€χ' >J³2_¦εΉZ\ω£€Nt• §63zAJ1]Jϋ;<αBΎδύaνhb¨P²+ββ»Ύ!Š%>·< Tτ·ΏΔ₯=YRΪ_ά!εZΌ¦»ƒj:s(GGZPΪ5ϋf\¨Έη€ +<:~…»%ρΖΊ(ΘX}‘WΪΊσ²Šΐm—;κ“εRPmMο΄ΫηT$ό–κωe£τμ.SΚΖΪΊXΖs«α"ƒ[ŸϊΒγ19|†ο,(ΐ ?έΥ{™^h8Cΐ ψΕθ‚Ž‹1ζMYΝ|˜†J²αEKτ “ΑyΚx_SbύΡ'@φ.X™aVΤx•₯$Η/Ξ³Ό°g/,fSYŸRŠ ΏTΡY8Λs\šπn›JΠa%°λ,WPjφA—:Λ‘MρIlροnΈh0*S!Δξψ+‘² άYH>Sǚ½*8δςZ°LY¬‘φΒΎχ€ŒιB:tτ]|Δ_Œψξ’ΑΗT«,›ΔhάGi7™μ<Ν„#:{LD]? ΝΠuσyC’Dξ3~3Z—cИi•Λ8˜θ– ΔD΄ΑΉTπ8;·.C‚>6s9uLS©η΅Υ:…BpJh– ζSεώƈύΚ)7ͺΉQ±o1Pπ•#jγm +ŸΚ©UξrόΊΈ©τ9-₯§”!kfΜlh;ŸψΔ{Ξ%ΥΤ>XΜ―<`4gxΖ‚§ΟrωžE€… }4Ÿσ¨/|jΓ²nοœs\g5;γ[Š7ΚxίE€–ν`zfΩ+^ΨxxαQI+ŠrrΏΠeϋ+Žτy VυΡΗfϋή'L{+„Ι ¦ΐΚ†ΌfΖ²˜Ϋ}ΩΣr&3©₯φΩd'ΫΤgαmΞ1αJ˜ΙΗwd7GςΘktδ‘Οί³ρ…gK {fΣψδ†E'zN‘Μr‘z› ΓξτλΫ]ΘΝΖΏΗpΪ^ϊ» Ή@gWZx½ χ΅S£Γξ§Νΰ ŒβAzϋζχρή;­ρ¦ιP9u‘ϋδρ*" +‘πiΫΤ>Ν₯»ΈξςIl§?λΩMαΖεγ󐀑atKŽnΧέ?y¦”yΘόΗ–Ψ?σQb₯2Bi&0fΌts|ŠoΦΛ8½ΊtןQir« Rp£ˆ"e`©τŒŒ›λ ―šΝ³O₯€#XN‚ώoKόs)OyF/ΕόGz{υ‡‡„λ‹$UhεsΝθl ,„Ώΐ…Ϊhό» A‘δ9£ΦςŸσp~9(mείψ£ ζ°}œŒžM¦(„ΞΚ1#?ΖΧγΓν‰1 ΞCzš+―ΘόΤΔ[ΓΌ>€όŠΟgοήYcd`ž- +Bκ moήϋω©ψsw>Τ$γιϊͺΕ[5„))9f&Ιq&Κ@Κxj_6υΨΚI—/o^ύpΌ¨{ +endstream +endobj +256 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F204 91 0 R /F206 92 0 R /F209 93 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] >> +endobj +257 0 obj +<< /A << /D (section.4) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 213.123 580.237 224.648 591.425 ] /Subtype /Link /Type /Annot >> +endobj +258 0 obj +<< /A << /D (subsection.4.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 226.869 556.327 244.707 567.61 ] /Subtype /Link /Type /Annot >> +endobj +259 0 obj +<< /A << /D (subsection.4.2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 115.698 543.136 133.86 555.56 ] /Subtype /Link /Type /Annot >> +endobj +260 0 obj +<< /A << /D (table.caption.9) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 164.221 352.353 170.897 363.541 ] /Subtype /Link /Type /Annot >> +endobj +261 0 obj +<< /A << /D (subsection.4.1) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 277.432 268.334 295.27 279.522 ] /Subtype /Link /Type /Annot >> +endobj +262 0 obj +<< /A << /D (figure.caption.11) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 175.522 85.021 182.184 96.209 ] /Subtype /Link /Type /Annot >> +endobj +263 0 obj +<< /A << /D (figure.caption.11) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 489.4 541.947 495.97 553.135 ] /Subtype /Link /Type /Annot >> +endobj +264 0 obj +<< /A << /D (table.caption.9) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 349.085 352.02 355.803 363.208 ] /Subtype /Link /Type /Annot >> +endobj +265 0 obj +<< /A << /D (figure.caption.11) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 390.24 256.379 396.958 267.567 ] /Subtype /Link /Type /Annot >> +endobj +266 0 obj +<< /A << /D (section.5) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 547.472 256.379 558.996 267.567 ] /Subtype /Link /Type /Annot >> +endobj +267 0 obj +<< /A << /D (section.5) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 529.801 144.797 540.951 155.985 ] /Subtype /Link /Type /Annot >> +endobj +268 0 obj +<< /A << /D (figure.caption.12) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 386.257 120.887 392.818 132.075 ] /Subtype /Link /Type /Annot >> +endobj +269 0 obj +<< /A << /D (cite.skewmanohar2024parlayann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 471.409 75.303 482.667 83.557 ] /Subtype /Link /Type /Annot >> +endobj +270 0 obj +<< /A << /D (cite.munyampirwa2024down) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 485.357 75.392 496.615 83.557 ] /Subtype /Link /Type /Annot >> +endobj +271 0 obj +<< /A << /D (cite.hmann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 499.305 75.303 510.563 83.557 ] /Subtype /Link /Type /Annot >> +endobj +272 0 obj +<< /Filter /FlateDecode /Length 4154 >> +stream +xΪ₯[K“Ϋ6ΎϋWθ’ZͺjΔ/>²'gσάέd“υ€r°sΰHœ+©ˆ€Ι―ίntƒ(JφΤ–«†ˆgύuχΧt²zZ%«o_%³§€ΏΙJ¬Œ^₯ΒΔyͺWΫγ«?^Ε™’"·/½’}εΊpΕηί³ΥWν«Ÿαί|Τ »ρΖύςώΥη߈"_q‘ΚtuΈJβ"WΣ"LabxάWo£oκ§αΌΞ£j½‘FEiLΟ·ki’Χ‡²―Ϋ†ͺήτΓξy-²θ·/ΦΏέσσod"Μ!2›B½9­7NΓvΓ‰Ζ-·ϋΊZgΡ{ZΌ‘ϊ‡gzξκw‰2•]dΣγΤ+'ΫN³ε†¦iO}}¬βu«4z4Άϋ¦ώc¨:ͺΫUέφ\?¬7"ηT©‰κΗ•ΔƐπcaVΏ―Βίz΅zkGy'U¦©Ÿ Ώψ°―Πn¬< eΟΣ,,?qͺxυ(λ,[8[Ϊ•}ΩU}‡ΏLΤ·Tk…‚£_ύΎ’BW+―ΞΈ-κ{¨Φ) ω@mh23u-»>-άCνίC7^έσpύ‡z[Νzγ­cQykK²@Ζ.­|b…—˜€ŽIfwTUΪ…Η&»κi<‚ +Ϊ‹Pω@”Ξέ2ΞΔ„žζ vQ€¬ΉΞ ‘ΕϊύσΓΉήaΞΤΉlΆάnœ8ƒVtύ\„§gjόx³Ψ?UΓ°Λ’Ή +ο›;”IΖ‘ηΊ:Ψ ›£ +ρσύ:Ώ»ΐeKΩeΛs!#i"©L„S +Χ)]0{"—q–¦awRκΝ”›XΘ,μ§ΧK/MUθΨd³.ο’D^1n*™ΐe39ή‘¨œCF` +""Qel¦ol@¦ y™|¨$tf²‹JΒ‘2‚~©Ύ%*™§±I‹—‹ͺq‘˜Ή¨L‘D_'c².ΏΞ&δά [6OΩd΄™Œ4_x ­Ρζ{ϋΗPŸΗa|Tuπ₯ŸSπΚΙά*pU›ςα@CΑ―Cύ΄έ‡j,Pέ€ηΆ=Ψπ£σEΰ7’_;Ψ¦9,Λj=/Y)Dt4ΏQ9(0„Υq ^@/wά gŒΦiςΪδhQπΕ롐SCo€έsSΡΡΐΪτN±;©Μ[φL ϋH„T‘`Mτ8œHIφυΡ€Φ!" †0η/ΤcθλΓδ‘kwNζ£ΦS λhFλiGeT²qmIΙ·Q’½ ,΄.:[«Ί₯ΘΤvϊ¨ξͺ<Φ*\ΛmΥ―LmΠαS4Wƒ_š½²τ¦ή‚ƒ'T8Ν'hm*bfA·;™mn9z[©wyf}^ςΆΚ?λγp$'hΩTψέ˜χΒδας¦­`+”{\c0’I‘Ž“X…Τ±’σΉΘ2˜K%ΰυgaǘ„ςϊΨΪkŽμ‡υc‘ΞϊwT]ΫSΐΧΌ©Ώ¦F¬τ Έ3J§ηŽΒΧ »νy(Φkm)ΚPθZδθZΠp¬ξώ¨,kdΫχυ8vYŸ°N#ΚyΛ3| φȝ›x}MΕ}{’‚·c@ψމKΞΝZ₯±Θ‹…ˆR‹Ρ'ΡλC_›\Π‚Eτ­+mρ'9 EτΓΖ!PŒŠ”φ.KΕwB/i§d +KA"φIdΣΪβiœ”‡'+ϊύ‘ήρ 4±Πh +byυ―)θ°tΡΟά|Œgμ΅3.Ί±gXαΦE/6Μτh™Ζ‘AP—a³x!Α₯S’%β;† +ν[ڌ hG ϊ}ΩSι8t\‚¦|n`+P1vΣLQΑσβΒC=žθ’6n©‰wƒςO΄‡(oY’ͺ1φΌΒ#-HA§$§zι“ξψb΄zG―ΐφЍό.vаXwό¦5ΪRΉ€GΧ[·Η΅εΰ,ώ\6h}X4νΩs{`h6ΛTιΰΫD &VIOδ*EœόϋΡ»s +e_‘l :yθθ'δΊΗΦy_,ω_₯Otœλ,\b—`Z†ϋ/θ9QnΦς^μ$)˜…όΙRW’ cO@Π“ τΘͺΡΩΞ{xφξc6η‘I>cH{ ΘράzH’άΘ+,Ψδ=Uέ5έΣ Ν2˜’άA*<ε/RNo˜l_΅›Fk1ˆΝ‚iPΤ’E £Ό9’ΫΕ>’κBΖ9Έ¬ΑpwΤΧ 1GΦeR”žΥmΝ|ο‰/jΠ¨yk&'r~‡μvq‰A…σH‘“E¨Ω—‡Ηω»v¬©u“_2c!-XΧxΞ–9%Η<—‚0ΓSς$H?$z²PvΒκG'›7AWOΙ(r¬αΥBςώš΄ ¨`š9 ƒ9¬ό₯9,ˆ’₯T0ŽgmJ;N +λg'…M¬₯ƒW]6,Ό&2 +χ=oλŒάœ<7©8ΣeΊΖ πŽΙΡαάM™²IυxDά»Θ=υΚdcμ>R¬»a[]ά ·@%bm2?i’cώΊΗKΕΐŸάΛΔ3ϋ£<|β­ΎγQφτaˆχκ½U!~I{λώΦ-_wxΒβΓ‹6ζF>ο«r_\2wν“,6:sQuf₯¦RAΡ«5Αͺ”ƒ•ΚˆΣd`\Δc₯½πœ}σbλfYs‡1Kξ«ΔΟn +—₯’ύ3Jb{Žή?SΣOα§7”’M£ηλΖ~€}:θP5Oύώ‡λLαΜUœ*—³™€ˆΜG ‚’9θ€ ϋ’&& Τΰu[o0)μ ΕΊ]ΥWη£Ν•βO?Ό‡*y¬ . VΨsHςYπ?’z2ΏDA^po2ΤυAHCkHΠg”Μœ‘P§Q·C7£ ˜ΐ–θβ%4΄βφ“16€cΚ§ +I†§xaΈ£,†+…δ0 +S­ƒ<Ί•Ή™¦)Ύ’hηNuοΎοΰο4Π%iφ9σξ9/#dΖ΄„οϋяρvδΉΡ A“³0Ρu3y’„#SA‡¬8Ν…ΜζΙoqώ:Υ±a”8Q&γ΄Θ‚nds₯‹"‘ΰœ_(ώ0Ιqσl~~ڍŠ€K­IάθWΕ=˜‘뙆š›8¨ΛΒ·$>M•ƒ'X„og¬TžΕ7˜λ†ΠΗΉ +ΆϋΕ$“_ΧΉˆX^ν±ξΣ§Η“0ƞA*Qξ₯‰‘’;ϊ³E/ΒO™1a”DEϊΩΗΐšω„‘ +ΡΦX;ςΚΔώρk\š]už ­™―t,1ΖykμztΰeΪ\ΪΞ‹žΡ±³ *bpόb#f?LΖ·Pώλ*£%R°B~-7Sθ8%—±zηΙΈγ–Έ°<υΊzίl₯“Q°ƒž©²+tζi‰ΆQp&π΄Lj~ΎŒVQΎH ΓxμΎΜ§}C̝γ:TΔ(Χ ¨C}M‹ςd²2vSή%St3;±ύ$]OBΊ£78N³δ]ζβhΨς7#Xη’Q¨φω?xυXΦnԎRWσϊΙΌ]KΘή΅ξ³JƒŸU֜b΄0ΣmΫ)6ς²…ΫsέΧ['«?~EA?ά +Κ,ϊŠΊ#…i‡ΣUAϊ™inρKokυαo«ϋ‡SΆ4£―{μjθ­QHσ)»£ί%=π;f7Žξς_φΛOΘ•P«.ΜΊ‘‚wψ•Χ{Ÿ`~₯vΆΙαωwχ1φ§]κH—7ϊ·kωu7ΜΞo!‰O.‹Lεhΰ6HFκnΠη@c΄C—2 ϊ|}꣝°œ +endstream +endobj +273 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F204 91 0 R /F206 92 0 R /F209 93 0 R /F212 94 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] /XObject << /Im7 588 0 R /Im8 589 0 R /Im9 590 0 R >> >> +endobj +274 0 obj +<< /A << /D (figure.caption.15) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 161.693 463.601 173.137 474.789 ] /Subtype /Link /Type /Annot >> +endobj +275 0 obj +<< /A << /D (subsection.6.2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 191.011 427.736 209.172 439.108 ] /Subtype /Link /Type /Annot >> +endobj +276 0 obj +<< /A << /D (cite.li2023towardsgte) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 128.603 418.017 139.861 426.271 ] /Subtype /Link /Type /Annot >> +endobj +277 0 obj +<< /A << /D (figure.caption.13) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 287.895 391.87 294.427 403.058 ] /Subtype /Link /Type /Annot >> +endobj +278 0 obj +<< /A << /D (section.3) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 236.477 316.134 247.983 327.609 ] /Subtype /Link /Type /Annot >> +endobj +279 0 obj +<< /A << /D (figure.caption.15) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 254.783 168.707 266.041 180.162 ] /Subtype /Link /Type /Annot >> +endobj +280 0 obj +<< /A << /D (subsection.4.2) /S /GoTo >> /Border [ 0 0 0 ] /C [ 1 0 0 ] /H /I /Rect [ 196.347 120.887 214.832 132.075 ] /Subtype /Link /Type /Annot >> +endobj +281 0 obj +<< /A << /D (cite.ivf) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 407.958 182.899 419.216 191.153 ] /Subtype /Link /Type /Annot >> +endobj +282 0 obj +<< /A << /D (cite.hnsw) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 523.634 182.899 534.892 191.153 ] /Subtype /Link /Type /Annot >> +endobj +283 0 obj +<< /A << /D (cite.hnsw) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 500.509 158.989 511.767 167.243 ] /Subtype /Link /Type /Annot >> +endobj +284 0 obj +<< /A << /D (cite.nsg) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 543.021 159.078 554.279 167.243 ] /Subtype /Link /Type /Annot >> +endobj +285 0 obj +<< /A << /D (cite.diskann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 355.869 147.034 367.127 155.288 ] /Subtype /Link /Type /Annot >> +endobj +286 0 obj +<< /A << /D (cite.sptag) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 422.973 147.034 429.599 155.288 ] /Subtype /Link /Type /Annot >> +endobj +287 0 obj +<< /A << /D (cite.nssg) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 432.316 147.034 443.573 155.288 ] /Subtype /Link /Type /Annot >> +endobj +288 0 obj +<< /A << /D (cite.hcnng) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 446.29 147.123 457.548 155.288 ] /Subtype /Link /Type /Annot >> +endobj +289 0 obj +<< /A << /D (cite.wang2021comprehensive_survey) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 495.712 111.168 506.97 119.422 ] /Subtype /Link /Type /Annot >> +endobj +290 0 obj +<< /A << /D (cite.baranchuk2019towards) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 357.883 87.258 364.508 95.512 ] /Subtype /Link /Type /Annot >> +endobj +291 0 obj +<< /A << /D (cite.zhang2020learning) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 366.805 87.258 378.063 95.512 ] /Subtype /Link /Type /Annot >> +endobj +292 0 obj +<< /Filter /FlateDecode /Length 4162 >> +stream +xΪ₯ZK“ΫΖΎλWπβ*le cž\•ƒbKŠΛ±£΅}°tΐ’‰iΤjύλΣ=έΞ`A­6)Պΐ<{zϊρu7²Εf‘-^=Λ&ΏώΟbatšebau–ζ&_¬vΟώx–κB+Χοžh¨λρ³Έαλοw"[|·φ3ό›Όδ₯—ΑΪ»yφυKQ‹"-­Υ‹›χ‹¦™’ +σTdΕβf½ψ=y›™μJΘ€‚qυξζ_Ώ”°y’)ΜΙhτsΊZŽΥꏔΌΈ‰…I…˜;§ψƒŠ‹υ‹?rR)ŠΤ;sΤ%ΰφ3g-LαϋC5Τέδœώ—6,a¦΄8s)ςK½»^fx΄,βχ>[όξζ ]8·’h―L‘\ΣͺwD΄ΥŠmI!Ϊΰc³i:81I΅€X₯²2&m΅ο†cγl;.œΥŒΰX +ƒB’:ˆ9e!²Χδ.Υ±ΪΥpΫ½sΡnΔ©]GΣ}©<αzo₯Κm*§BεxԐΑ/Ο,i›ΝvpΒ…sŒAS­‚ƒ‰2’™ςκζΕέχO0N,‹Τ‚ˆ&GV[Bψ’CސK‚S)ΛxIeγc‡$U€…5ρ„wΧ΄ωέΆq.·UοAω‘(ύšΞw•RΓoW +·Η&ΰS0,gˆ M|₯ΫU—SFδΛ§OΎ¦ί5£ΣŽμ9ŒŽ°ΑC½W₯Iμ΅DξpώD€`(`ΐW ψθ>0ΨωmύE€A‚;&^3½=Σ7]‘ΞθbλSŒ.Ž/'κΑ°$4M„JT‘RiUˆJrK¦?΅ŠJn|šέhΚΐJ½€ ύ<ΖΦ«sς£p²Κη,’œϊ¦ˆμ+j’žE„ώ„f.Ο?/}2Ήojwf\^Έ*βΝ ,y΄΄ψœwZ¦E6™qΙ»F[{vr +ΞΑ +ŒqΔ{]p°Ρ^V=άkΞΓςM!Έ¦°XQI΅kΩ6 cŒ…χC¨‡‘¬ƒυ/‹Ο‰³*€ί:Ρ"-ΏJ©έ΄’‘ ΆqCν74γ†μZΣ“Ε€ΰΪόœ–ατ"ΰ»΅Γw^ΓF +#)Uκ“\ΰύλžMΦ‘ΐrΞs΅…b…±šUhΣ²ds¬Ϋ%:tο +Π‚+²8}εCαΏ;θ1¦Λ θQΘ9θ ¨`JΊ ι$“ύmΟͺχρ|,θQ³—ι¦PμAœR`8j~j)PŸ nΉ£Φσ₯σ–Κ±~βΣE RΖτΗθh}b”βνχ[:°š]½mšΫ–G·ϋκœ‹M4 6¦qΘŒWΈX"Ηΐ­Ίσ-η +ίΌωŽ/ϟvivΝ9‘ ·U·ΎkΦΓvUΰΚiZψΥƒ«{4λ\Ο-Κ 2η1F‘SλNŸvOŸ’%x˜u +7ξη ©°°ηi| XgΌέΣjί±ΨhΓE΅<θΆ(-MaώnγӘA +!›ΠE*΄OmœD$EN+*#¬-ΰQ6uΟ@iΒ·ρgˆΓ(Ο•ϋζ˜Ζ ²”n‘ϋ~.«%”…“ˆ”œ΄šΓΖ½«8€l ₯zΐΕ¦τϊ’rP#{φσk΅!°³!0M;@›C[ΣΫvθη(‘:•Š™γR§ΩΌθ]S—KnΒ/ ‚Υ±GXŽΖ›Ώšβh„¦ϊA. +½;φΎlŽύΐΫϊΠ97²|`+„LeΞ<ΕΜ G9?ύLr:ί8ϊΐz`§nι°,“”Τέ5ύh5Ft=¦ΗΊ —Ζ]³<Ξ²TIζ1֜ΰ 5ϋu‡Yt~uyθ •γH‚@1XGΖ ‡«;λΊσι†fžWŒ|P E‘*Λ j:ΈXd„rΐέέ">χ”8R Ž}‚fγ'7%pGͺQΈŽ—§ΐφ[αά‹:Gπ_cΔ―ά¦Z¨ψ"=.γνσ쒝̩Ό·”ΆΔΰfΰ<šΝνκD•ΖωbΚLW‰άω#@^Α–p7·²δ +ξψx©‚+/VpύβΛ`υ±‚;›Ηπ3t©’ƒΖ°²jθ¦tάiiϊ+­ž77’ΡFηR+,5ςΨc(x$ …Wƒ.{rΏ#ιxVk|φ|w@˜E)N›œΊ5e6AͺΌA jAj§Q¨lεΤ—ΰ•ω9GUΘ§4δΈ +φY˜'­Κά*_aTΙ§%„ψŒiPΖ•Ο–`’cπΕΌ€&εjcτδ?%pŽkΪω—‘.3₯Λ`lΰyCŒe`‘'5“1όМΦ.”ηZ’WwAΖΗzXœ50ΨΓ:ιάO7Δv€M[οζωΐgˆ“:ΆφOΦ8e1ˆU£ΖYξ5Ξ?^8υΈΖ«Οh\`>Gr –PΎ”¨œ(-€&ŒωΛUΞ*‡M¨\°“VΠ oΡN£Ξα>όGxΈQΕczš»@%ΛTgΗqχCορΕΘcήμΞ_ξTϋ-D·²0©φyΓη-ΦΞ]0¦υ¬™Χϊ"­μiΓ0υ†©°XΌΌddRγσ‡AIVv΅[mΘ}]SΔpϋSGεMμsRμΘνWΤβΠEf}~Σδ /€»ΔTΦ$§)rŸμͺf€ϊ6“Ά?T]7rκϋ―u%Ήπ©“oρ‹—"ω½7‘I€‡WΎοΜa?]9Ζ4‡ϊjJ.S«δLΕΈε΄φ9„6bŒΌΪκp!Χq$ΧμΈ€žϋΈ3ŽΪ°όA¦εm¦τͺ!n–>!τΫU^ϊzΫ”\’—33ΫΊϊxώiΰ”y€ ₯ω3ψ +‰κήψpΒ—0Ψί?ΑEŠ’£;iΣάςWV9ς!Λ\Ί8Š»*2\εBήy*‘pζ]ζ«ΊCv ς“Ι―tnbBΫŸZμ+ ΄fVΗΛÊeχΞ+†Ιzx%5kπΆU&€Smλ RIΓ0&υξΦ%η±H`©lΊn6»ώ|—Ιχx™Ι―/©χ‰…_6S“όwxxQ€RF£ίΡΎΤ­LF‰ό„1ώ=υ:›ΨΣ€'Rh δΒbRDaq‘B£TZ”6&1pΚ›©’Š\H’=υXœ¦vΜλΰs[ƒg}&CΜ}”§R΅‹  ?jͺ_©8ΗZΘ(*Χ³€‘U ςc%-Ÿ ρm²«ΑAΈχžΓκ₯rβΏψζ7jyκmd`d3’3s#υpuοΎ€}|σŠHy*Z;Ϋ,)Εg0*•€Xb +<“C7 Š₯>~ECYνͺŽ“-O€S›v²¦)RκΗ[Š(ζαψwœgA½Βς`?€%Δ$yΌ~q‘- @2~ύΔA•˜¬!³Λ{j™š<φ„L“5΄ΈΌ'\Ί‘˜ν>οΦuQͺ£‹ΦV\D™„KF> /Pattern 97 0 R /ProcSet [ /PDF /Text ] /XObject << /Im10 591 0 R /Im11 592 0 R /Im12 593 0 R /Im13 594 0 R >> >> +endobj +294 0 obj +<< /A << /D (cite.diskann) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 280.166 685.016 291.423 693.27 ] /Subtype /Link /Type /Annot >> +endobj +295 0 obj +<< /A << /D (cite.wang2024starling_sigmod) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 75.624 649.151 86.881 657.405 ] /Subtype /Link /Type /Annot >> +endobj +296 0 obj +<< /A << /D (cite.fast25) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 133.353 637.196 144.611 645.45 ] /Subtype /Link /Type /Annot >> +endobj +297 0 obj +<< /A << /D (cite.tatsuno2024aisaq_gps_ref46) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 279.742 625.24 291 633.494 ] /Subtype /Link /Type /Annot >> +endobj +298 0 obj +<< /A << /D (cite.LM-DiskANN) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 115.668 613.285 126.926 621.539 ] /Subtype /Link /Type /Annot >> +endobj +299 0 obj +<< /A << /D (cite.seemakhupt2024edgerag) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 280.29 601.33 291.548 609.584 ] /Subtype /Link /Type /Annot >> +endobj +300 0 obj +<< /A << /D (cite.pq) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 243.921 517.644 255.179 525.898 ] /Subtype /Link /Type /Annot >> +endobj +301 0 obj +<< /A << /D (cite.gao2024rabitq_gps_ref15) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 170.627 505.689 181.885 513.943 ] /Subtype /Link /Type /Annot >> +endobj +302 0 obj +<< /A << /D (cite.work-in-progress) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 236.776 374.182 248.034 382.436 ] /Subtype /Link /Type /Annot >> +endobj +303 0 obj +<< /A << /D (cite.ryan2024enronqa) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 251.005 374.182 262.263 382.436 ] /Subtype /Link /Type /Annot >> +endobj +304 0 obj +<< /A << /D (cite.wang2024mememo) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 265.235 374.182 276.493 382.436 ] /Subtype /Link /Type /Annot >> +endobj +305 0 obj +<< /A << /D (cite.zerhoudi2024personarag) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 279.464 374.182 290.722 382.436 ] /Subtype /Link /Type /Annot >> +endobj +306 0 obj +<< /A << /D (cite.yin2024devicers) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 215.298 362.227 226.556 370.481 ] /Subtype /Link /Type /Annot >> +endobj +307 0 obj +<< /A << /D (cite.snap_cvpr2023_tutorial) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 69.987 314.406 81.244 322.66 ] /Subtype /Link /Type /Annot >> +endobj +308 0 obj +<< /A << /D (cite.nvidia2024blackwell) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 541.775 411.465 553.033 419.63 ] /Subtype /Link /Type /Annot >> +endobj +309 0 obj +<< /A << /D (cite.nvidiaA10) /S /GoTo >> /Border [ 0 0 0 ] /C [ 0 1 0 ] /H /I /Rect [ 495.356 399.421 506.613 407.675 ] /Subtype /Link /Type /Annot >> +endobj +310 0 obj +<< /Filter /FlateDecode /Length 5314 >> +stream +xΪ₯[K—γΆ±ήΟ―θ]€s$˜ >–cΟ#}γŒΗ3;χ$½`Kl‰·)Jɞt~}ͺP€¨ά΄³"‚d‘P― +Εθfwέ||ρυϋ»7ί}yv“‹<‰“›»Η)’ξE7ς&Q0ͺnR‰H₯7w‡›Ώ-Ύ”ν±?/³Ε¦\plΪξ\TMΉ\§‹νr­"½ψe™f H›ξΈ\ΛΕ™ΖΏ–…}l/–χwσέ‡8’σ_U&βDŽ?ϋ©?”ψψ±oιuεγί>hτM’g"ΚbzζρxξpΆ2‹}ρΌL‘*μΥΑ ]€R©d―§ρ~Γ3»}IχεαλfρBw8σ؝ΞUΣΡΠρ‘ζ>»χlf)Μ!c^Uwdζ΄sl]Υ>­Šv`¬Z§Σq,6ϋ²₯‘Άίμ遒|ϋιέώ~M$ŒoBš›§›qOo¨ŒΟ€I<&Ρδτ˜ΡFε‘Π0o4ή­9RΑ’S-dΚΫβͺ-‹b-€«0rO݁y0«₯±’ΩRcw.N<δίtVKžvlθΊ¬¨Y—ΐ0ϋΚs±«”Ζέܞ$±ˆ%/ jΦγέ6z±9Nτ©aG`Έ<υ4Ά­š]K7νΎ³hŠηjWtΥ±4π΅+Ξ³ba"zζP o‚δ―ήΑ4Έα»}u2#²ΜŒ§ίΣw+ZοΡ±―₯αΫο~’FωχHιMU6›°+ΖrMΌͺΆ%κŽΪkWvα‘{Γ W"Qhg2‘’„(ψΆ―`ΫΦ .‹} œqώJύWrC*-”Ώ=‘cvΔH ΤΘΙό{ϊrΩ5rA¦€‘νΦΘt‚vΥσ2_0Ν^“©ΫνΙjνφk‡M P—6dQp:HRΡ9A“i΄θΫ’hcπϊυλ»eœ/VΤϋασrm’Ε_–±‘!I:ƒχ>Ί{ΆGƒ<ή +šόΆϊϊv μgšτj³‰TeγΥ$ΡU>£Ωˆ’ΙκοWήn„Z!asފΌφΦ-Φω«₯A‚r$fόN\h‡ΎŒΘΥxώ=}ϊ±?ƒk@‰Φͺ¦:T$3”/ή}yϋgΊΡ·Ε€Α‡BΫ·φ’`‚•F±ΠZ:„κΦbΖξΰπ¬έ‰M‚s&IDϋYΏΠ ΅‹πj¦ ζϋνόb·ό# όOgɘjc.·ά{n`―GxW¦Χ3Β SɘέEMΌu°μxbp κ½SIˆέpέ• XώŽψ©―1N#ζŒ²I3a”τF™ίVΙ½°hhθ/‹_>„>οWΝπAΣώˆυ›₯“qΒyi²ΕŠξV=ΤvU]σP³ιΟLcΫ?x"#P™$Β€Ζ9Χ’ιͺ’čd:ޘŸχea%IΤα›‚μuτ驦πuEΥP―.ΞπKΫfŒΞ©lκΎνJ»Žˆύt”[y€‘’u#›n0|xΗn$ά©:~x neΧ¬ψ9 ΒΫ'KτΌΡ‘h6₯°@ӌŽ­°”mKπL['[…Ϋ€MQσ^Ϋ γνNˆšq!μ«έžZ=’Ξφυω4έ…ΫΝυ§ΨO²ŽΌ‘^H’ΐ‘`y܍EΝΙΠ€– ρ‚†ίςγE ϋN¦zvJc§:'3‘ζ‘‚½šΚڝΚH―h<°_ΐŽ=D [?ώωηίg…΄9؎…q~i…άjΰ[BBt0zΰžΙ²ˆ90:ušξT”ˆ4ΟιΉaΓy R°Mݞ6―₯Ίz*©υ₯x¨ΊŸ©ύZΗ•JΠ€ΙΧcuιΈ<‡œeωψ»Lψ8ΐ+Λ~h-„93­Ώυh9ώI‚;΅…„ΣtE¦5γ΄ͺγχXm`θRvΥΖZzh₯εL=ϊ}³mόοΐFΩ­ό[Ϊr–‰•ζέ.gl΄Ξb»Xާh/Ceœ’‚+ ΐ™•Ώ XΧΒBXθeδAΏα‰ŽL(haΏσ:)SΜ—]Ω΅μfozΗζΨ@˜άv,i?Ύ' cΠ"thΘ»‘6kΰΓ6&yΉζλgLˆ6™ΘΣ Α -‘Μcή2lx‘ΛCŸ…χœ!YΉξζx>Ρ³+ΕadꬽΠ:ζθΑΎ:r,•σΧ%9ρgΆΚwvJίψq2ςƌύφδΣ/kth°™\\ Bh7* L%™C»γΨq`ίxκ,† άE4ΖqήyΡ- žmcœ= φ…³':VBJEŸύe™ΕC|KθΫ&FrŽŒŸN5θξZΛ Ώ‘λ{KΆήM‰'SB°mυ&c*~jΦ~—vNm]—μsk•s¨’Άϋ!.HӹzΥZ_n·‹Ν­k]Χ όEš|±ΒύKSy.βˆ€–+΅x /›βΦ΅ΊͺδtO±±–§εξΆb‘p  €Ε‚†.X2V0•₯hγ˜GΞR.Ϊ€6‡–:; +φ¬ΔB·.š]OΖz2ieΝ“­y‚+c–φΨXΓ #[;sΣΠ³Μ°"5‚j6"FΆ[C'™‹‘γ$χ¦τ ξm‹ [ΌE4όZί«R‘¦ρ˜_Ο‘UPιxώκ•ί4± ΟFο0κϊ7ˆR’ς›cp}£w$ΙυoΒ< ΡβυMπί:™|3ύ7ΌΝr‘Ζ“oή 'Ζy`%εrͺŸYθB»άΰΦ!Y4½^ΦΝ«“11„ίqnΖ$—`ΝSCψm’ρό{ϊτ‘θ6μ*ϊ–|vΜΠtΞ=+#…rIa°Κ)O“CΔV £Φ•€ž²ΑuΧ8ˆ¬Α+eŠ>Vή±βΟXutV%4+*·TΦdM7 +!‘ι!f‰ƒ(ŽnSόe›—¦ |€ηj²)QΏπ{}?εˆKββTh—΄­`Η€°GFkΎ%δΧ:¬r²VύΕυΓΕΞo+—˜Α5έΘJzhψu H‚HΈ<-ΩΡΧ火ejό.s™­ςίeDΛ?šΟi sšs=Ξ2'rž°ι +Ώ?f`N©°a’α$]w¬$΅Wδ,\48|)Πή ;†Ο”Fχϊp„HD΄ηΣc9ρΰTρς\ςs +§βΒ—&vzΕiςξ—9‡θ‚?˜§e<βŒΥ„Ό₯δΖΔυά¬Ab3°1w[ )CρŠ"<Ωτ­ΌΘIo΄ψΠw>gέ_—|ωότ*ζ=ˆ„H“ΤρήΒ•Υλ Φ$]«π N(šš¦ρ–YσBχν†ΰ΅?Σλ^/‚P‰Θ$ΛΑΘzθki Έc“(˜_ΡΘ±οj&cχή†o¬ΌbΩCΕ’»Ί…CΓάπ Ζ‘ΖHŠE‚΄q>A5²©S₯ΐΖxΎΩxςiyΓφ‘#D΄Ϋ/$ ΐGb41ΦD,ι0> ~Όξ ό*―HΓDe¦E$½":sμ)‡m§—ξ›κ·Ύ™fE*·’6D[uΏυΑΟ4T#η²I2δΣ5CψQΓ£έοƒ *Φ”€ΓbB>Kΰ0 ›†MOŠΰ(a"YsήW&`ω@lZ³΅(Χ#₯‡ΘVά?Σ‰-ΆHRύ£N]jΎΰ}ΫυAMΰΞ$h¬tΙ‹3 sN6ž₯—±0:%A-Ψρ3 ‘^Άλƒ˜s‚ƒΣηβPz~H9Ξ€HPYι²,Υ”ΙΏ2xŸπm8'ν[nόρΣΧ_9PbXPπΓ†\Ο¬ΐ—ηoΨΌ4ͺ<₯!~ =ΘΗiλΆ{™Ν­₯‰Π.gΐΊZ±©eΛΩ Š5u³ Fž(νs’αΟ?―)œςqVBΫ—_;Ξʐά–Ν† J¦ˆΖΥ"α!'βΝ½Evv:H<²ζl€ικŠ +†„” +’M°šl¦#oT˜βλτp–€( Ρem_Sna# =AΗΑ‚Μ!?ͺΨ TwΣqo†ŸΧΘ +K7Όˆγά/8vϋ#'ϋ7ΌφΗω₯γd P ηπ•%‚-Ώ”Έ ȚIΆΞ“֍œ©Ž£ybορρCžϊ„Ύάρ6<ΦΊF&β’ξ―eυnΦ +KD<Άˆ[όθAΡ,PΈYK-rt:ψ˜‡ΦΏυΥ`ά"¦Šχυͺf©¨IŠΆu!ZĚcEOy*1 ΚJ½}ΆΒJ/₯Λ%— P|)}πΠW5ϊψΑΚρŠ‹ΗΞEβξˆ!œ:^ΙϊͺbK:4†ν‚šwϋ2@ίEΠ>€}ͺNAV#•`Ζ”aθψΓκŽu¨$ep-6Ou9bΐzœEP£U)’KX±Έ&RΖgΪeΎ­ΉJΓθ²%pΓR§|L±ζ£kΎgw +&3^汆ZcηΟ4άΨ›«Bo΄δ‘»|ά"4œ9ήaDS₯r˝ ,a„_V₯‘]2αq·.bΦD$&*…Jυ(Έe>ωΓiˆΑy¬βlΰ¬ –^΄[_ήX:7*-“ŽJh9*νμk\“9 wπg9%³™“θ–nxα•Ω…έΰlΑd6ΌΞ1^ζ΄RBλ΅ί‘²9 +§ξΛD©Π‰I4z’’[OύΑ #Ψ ϋΓ +£U„χ!Ξαρ·ν‚2 +‘žs―:7ΒDΉ―–=Lœ₯sΝƒΛB23©ΨCώΎ,‹§‚6eXd+ζ… Ά8[Ό`¨cΝ»rθNa~΄γ*E_šfΕQΈRή ΐ4»Υφ;`f.2.ž6ˆαTž&Ž‘q˜M'Γ&Y(>+|’Ž•-3Φ7―m1 +έΓΊ=@aύΒδpΥP΅Ξΰ”‹yOšFxΰ§…ΦΜΉgXΉM™“2Ε|j¦tf_ξώJ ’ˆZ€l7]±d*νAWεΥΠΗ-βΎ‹D§>CC¨3W½c,Z\άΙY4ˆv>Λ„†ξ– μXΣΊJ|>΅•—€¨hθΐdπό?ώτω+΅_™BΖ’4•czυe‘­_ Α“+NFά9ςšuӍϊ3YΙϊΉŠ-ΉψD5h·οnߝ·2’;ΈnφιnέζΥ•ž:O¬6ŽˆΣκj₯§‰ŒHd:žo—Νψβ–!'¬KύκΥEΦίρCAHlR†­Ηfwτ™$qΗeF4fύ\-γ(,r’κQεT>γΣ£ˆι)[n-Γγ$R-g²PIb.$‹/ώ9°·φ₯ƒ 6½CΦD ·r™έ~#/Fƒ0(qυ©Σ@Oy’mU›MYe’f0…θP\°λRpeγ«»¨κΞ¨‹μ5ωŒœcžš:‘Jι ~IλŞφ-τs'0ε耨ΊF/ΎgδΗΙλΖ%±Οφ,x6H.Ξl¦•T:jΒΎ UqeγO­ 棑Λe#ώ¨Ε–Ί*Β‘H:œxΘάFÐσLsžYΕ™Θ&ιτiM~θ– a7-±±EΡB ―–x•ˆLσBΏG)}Α·6φ/ψV…g.+ΊmΩ‹ ΰiޞώ?‘ƒ˜œι‹ΖΰD/Δ Β%BY€υ ςo·.ϋxΛμ\„‰›b?ΤxŸoY‡h… ”Άΰ2ά>N0Μ8Κ8EΜL‘K{φτνœ žοραUᏳœŸt°ΚDKι +3w‚Μ:k1xŽ‹ονΉ«w3Α 䙇pΘυ ζ‹ω6ηνΨ-h«]2֎mm0‹ρ‘(νŠφ‰6>χα蜴ΖI>”|ύΦWΓ½„χΈ„K9NXcΦ²BŒ q({θ‡ž`yΓΉπχήTΣ±ž₯Α_yvWωaR%CX`-J]=‚Ν8”ΪpΛήG6θHs›«΄Α‹ ΙΞ6βsMΤΜΩYPs!₯«…Νgψnu‰pΟύ;·ςυ”%»¨«’G.ξμnψNNύ@Ypδζ—η‘z.dVΥP֎υuεŽkŽωˆXJφΑR|Mτ§@ƒ8Ψβ’ώIizΗRΔ3ζΧeš»Κύύρ„ΰή`L}Žœ^PߞͺΐO,.&šΓ*5¬‘vάΕu·GΉIγMsεΛvΉ*–«°faKCi7•-\Όm‹2֌8΄py{ŠRrΔ=λk‚,σt¨ζ]ξα₯ΆκD%τ·_ψb±EWd­±JψOD3€χψ_ΗdV pfP›6§ΰ23B©l¨SΑP€A#JŸ‚€#ή‘νΖ³ώ‘ΖυC|₯A4ψ?η(ς‡#ž‹ΆWς¬sά ΨΎ‚“ξYp’Ζh©T²Π¦6RΑώ…η°YΦε“² ο›λ[―Kzΐά,ωbψ±r™Dξ€ΕŠ yΞήiaζ`ž¦„…ηnCηπGg>­ΤY€K¨o•KΉο“­TΖ?ΊΈšŽ ―€πτ­ν‚_jT˜κUς2οO=bΆšό›ψJFnΝΗwbςάEΘω¬„'ruFιθ[F_ φqΉ=₯Ε1Ό―˜K§‚ŒνπR:@ίt1B4Πχu +x0> +ϊH!‘〨Žμ0ΎIo άΠ–XŽ ίί½ωιnίg +endstream +endobj +311 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] >> +endobj +312 0 obj +<< /A << /S /URI /Type /Action /URI (https://aws.amazon.com/ec2/instance-types/mac/) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 195.914 491.999 295.041 501.464 ] /Subtype /Link /Type /Annot >> +endobj +313 0 obj +<< /A << /S /URI /Type /Action /URI (https://aws.amazon.com/ec2/instance-types/mac/) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 481.997 137.108 491.501 ] /Subtype /Link /Type /Annot >> +endobj +314 0 obj +<< /A << /S /URI /Type /Action /URI (https://aws.amazon.com/ec2/instance-types/g5) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 195.914 472.074 295.041 481.538 ] /Subtype /Link /Type /Annot >> +endobj +315 0 obj +<< /A << /S /URI /Type /Action /URI (https://aws.amazon.com/ec2/instance-types/g5) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 462.071 128.532 471.576 ] /Subtype /Link /Type /Annot >> +endobj +316 0 obj +<< /A << /S /URI /Type /Action /URI (https://techcommunity.microsoft.com/blog/azure-ai-services-blog/announcing-cost-effective-rag-at-scale-with-azure-ai-search/4104961) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 108.617 392.333 295.041 401.69 ] /Subtype /Link /Type /Annot >> +endobj +317 0 obj +<< /A << /S /URI /Type /Action /URI (https://techcommunity.microsoft.com/blog/azure-ai-services-blog/announcing-cost-effective-rag-at-scale-with-azure-ai-search/4104961) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 382.37 295.041 391.715 ] /Subtype /Link /Type /Annot >> +endobj +318 0 obj +<< /A << /S /URI /Type /Action /URI (https://techcommunity.microsoft.com/blog/azure-ai-services-blog/announcing-cost-effective-rag-at-scale-with-azure-ai-search/4104961) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 373.101 130.868 381.752 ] /Subtype /Link /Type /Annot >> +endobj +319 0 obj +<< /A << /S /URI /Type /Action /URI (https://github.com/togethercomputer/RedPajama-Data) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 189.956 282.744 295.041 292.101 ] /Subtype /Link /Type /Annot >> +endobj +320 0 obj +<< /A << /S /URI /Type /Action /URI (https://github.com/togethercomputer/RedPajama-Data) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 272.821 148.601 282.138 ] /Subtype /Link /Type /Annot >> +endobj +321 0 obj +<< /A << /S /URI /Type /Action /URI (https://github.com/kvcache-ai/ktransformers) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 242.893 220.181 252.25 ] /Subtype /Link /Type /Annot >> +endobj +322 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.cpu-monkey.com/en/igpu-apple_m1_ultra_64_core) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 236.137 232.938 295.041 242.435 ] /Subtype /Link /Type /Annot >> +endobj +323 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.cpu-monkey.com/en/igpu-apple_m1_ultra_64_core) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 222.968 221.897 232.325 ] /Subtype /Link /Type /Annot >> +endobj +324 0 obj +<< /A << /S /URI /Type /Action /URI (https://github.com/facebookresearch/faiss/wiki/Indexing-1T-vectors) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 212.136 83.491 295.041 92.995 ] /Subtype /Link /Type /Annot >> +endobj +325 0 obj +<< /A << /S /URI /Type /Action /URI (https://github.com/facebookresearch/faiss/wiki/Indexing-1T-vectors) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 73.528 214.088 82.873 ] /Subtype /Link /Type /Annot >> +endobj +326 0 obj +<< /A << /S /URI /Type /Action /URI (https://arxiv.org/abs/2401.08281) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 353.9 677.264 391.001 686.769 ] /Subtype /Link /Type /Annot >> +endobj +327 0 obj +<< /A << /S /URI /Type /Action /URI (https://arxiv.org/abs/2401.08281) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 418.466 677.264 523.863 686.769 ] /Subtype /Link /Type /Annot >> +endobj +328 0 obj +<< /A << /S /URI /Type /Action /URI (https://arxiv.org/abs/1907.06146) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 477.234 567.683 514.265 577.18 ] /Subtype /Link /Type /Annot >> +endobj +329 0 obj +<< /A << /S /URI /Type /Action /URI (https://arxiv.org/abs/1907.06146) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 539.045 567.683 558.996 577.18 ] /Subtype /Link /Type /Annot >> +endobj +330 0 obj +<< /A << /S /URI /Type /Action /URI (https://arxiv.org/abs/1907.06146) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 557.713 420.299 567.058 ] /Subtype /Link /Type /Annot >> +endobj +331 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.14778/3303753.3303754) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 536.81 527.833 558.996 537.329 ] /Subtype /Link /Type /Annot >> +endobj +332 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.14778/3303753.3303754) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 517.862 450.227 527.207 ] /Subtype /Link /Type /Annot >> +endobj +333 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1145/3589282) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 389.431 478.011 493.353 487.516 ] /Subtype /Link /Type /Annot >> +endobj +334 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1145/3600006.3613134) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 445.313 378.385 558.996 387.742 ] /Subtype /Link /Type /Annot >> +endobj +335 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1145/3600006.3613134) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 370.256 354.371 377.19 ] /Subtype /Link /Type /Annot >> +endobj +336 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1145/276698.276876) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 419.12 318.609 543.326 327.966 ] /Subtype /Link /Type /Annot >> +endobj +337 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1109/TPAMI.2010.57) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 514.876 218.975 558.996 228.487 ] /Subtype /Link /Type /Annot >> +endobj +338 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1109/TPAMI.2010.57) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 209.02 418.06 218.138 ] /Subtype /Link /Type /Annot >> +endobj +339 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1162/tacl_a_00276) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 432.442 99.423 552.503 108.935 ] /Subtype /Link /Type /Annot >> +endobj +340 0 obj +<< /Filter /FlateDecode /Length 8369 >> +stream +xΪΝ|ΩRγΘΆθϋώ +^nl ε ”Tη‰jj¨’ Ί{χ‰­Βjd‰Άdhψϊ³†LY’m¨{"ξε§rΦΌVf΄w³ν½ύWδ~__ύλπŒΔ^fFš½«/{Q˜₯P퉽Xο%" +#•μ]-φ~ šΆ^Ϊ›|’)=-ςͺݟˆTUΎZΪΣ*Xξ§Aή.‹|? ξ9SΝcΣζ ΠΞ­kVR¬½6ϋΏ_}€ΙΉ}2dadΔpF7K{7Ÿ`Λ=FB―'n²4ŒRΙΥm“οO’`Ά?‘:ε9βχ΄^ά­ZΫuεFlEΏK‘L˜H5μ7δώ^?β/uw]TEuΓΩvۊϊ}Κ( c“ϋl`vυd½9%χΦδη=»ΞΛ›š{”ΙήΒS +ιφgY΄σΕΆ₯υ%ih9lψ wHΧ.ΛBcΤ°έ΅mi†± pσ6―vνpΏ;a’Π {;ΰžΞN>~䀝΋ng7Pρϋ―UΎ‹ƒG‘κνO’C‘ΈΝΎΓ-­iŽyΣπΙΕ­Ύ^΅όPί•΄σœ_VeΙ©|q £E Q³YW΅ΙېSoVKh·μŽ«‡_ΖΘP +·άE͐ αΚe<0€Ji‚’j»ΙΞVS—k_ΐ‘ΰt8ΞΌΈ™sσY~Σ!CΞC–δ m‘ ξy9”Ώ"Πή~Šk,aΖkΪ₯mσ›ξ8DΦΫ€8 +₯ρ°_3"usr …,ΒtN:2„φfΏœηvΖyσ’tΝφΝvH™MWΫ †JC‡7vϊΈ§ŒΰIp΅odPίδxŠ˜}ΐ8Ÿ§e@ŸW@_Γyye―yiΒΨΚ-²Ύ£]Η­ι#ρσQ‘Qbφ7T±θ‘$ΐΞ†@ŸˆiΕyραίϊ‹/Ι]Ζ²Έ)*[ςΧΜΆ–+4Ε“«ρ[$cNuΨε€XΦυωsΧ}Zζ=MΒ$Κ†Σύ-Šδ’Ϊ§VB‡*3ώ²?Q*ΐ‘ΚEŠmΧEœ‰;ŠΏDqЦ₯ΩcΖNŒ¦α ^*¦Fz³‰d¨΅[ΐtεmγH‚­f.ΠDH_#ήςς ‡@¬ˆ%!Λœ~˜E€3εsώ%§vΥ”Y`Η›yγ’0Kΰ*Ψc³7ΊάψWρϋ IsH™‹γTρ’Žnν²€υν 4Ά8ΰΟ_h?*Vψ©ƒŸφ₯Vόρsρ4―iK‘βOϋqλΏqeGϋποΎXξψϋ²(]΄G˜υΞV•­ςyΗ¬Ρ czπΞώY4σbωT8)#©Bμ' +.σςΛΰγ—œ?\Ÿ !ΥψϊΔΚnςбmΥδ8%’§ γ]Τ0G kL₯ζDˆWHD•]€‰! Fd`#M@Kσ–ΑϋjM£Χ§Ω_ +80δ +ρ{ΏΪšA| όΞ}—mΎ¬HnAΔΖjΗu΅$Ξb–ΫΑ·_Š‚Q²˜GnβηάŠ…’fΐλ™'Y¨#Αp€“L† Œ2A„WJεΛP +(‘(ηv (ΜDςh_©`΅ψM%Ί,ΔχtYάrΧ°s" +³¦©+WΞG‹]ž›ΰoψFž†9o,RdΰN~ ‹Β“Β$S{1¨Δρ1 α“Χ°Ώσ…]ή6‡*Α“\!S Θn‰όΤ%}O {w‡SΑZΑμŠeΌθΜΆ“‹t˜Fn.ULœΕN ˜A$$― ·Aφ²>·χLfΑ’/΅Ύ$}뉛$nι[¦ 7ψ-Š#ά9ψD$ΰ Ζ !b£‡0‘F0„A Χ«3X―Φnο^υΉ ™/ΑΐqθHΠΒ>ρ"Tpz,Y|σoQ5­Œΰ9Α†1ΔQ@8Ω»έ~$W'QoαύI‰Μ„™ΓYΝ[Τe‚;ΪόmB‹ΚNΐσA8­ρgq˜OρWێW§Π±Χά΄'@LdΠ>²¨Π.μτπY&aβ0kܜΐz‚_‘ΰ}ͺΚ’Κ»c[ Bt*δέ-‹r‚»ϊϋψψ@=@|œžώŠΣ@Θ@ϋμτϊ³ϊ_==ΰ³Ylž=½›ψΩ³‹M½όο(Νΐόιμ€;»ψEj¬#g™ρ“EΡzu +$'ΌΆKXά|u‹ŸžΟCώΡ’t|Μ{m=9Ό­qώ!gΓr3HšD8‰Θίƒ#ά\£)E 2JϋΈƒk DOϋGI8½ΈU»Q±Ϋ$ʏ\8c玿ˆΔHPŸΞ¬8BAιΈaψΓ$ NΖ.Sάs?ΜEαPͺ­t\+hf^³W`Τ^!ε6ϊΗ‘© ¦»"CΊ»qάi˜=§]U׌;œv΅QεφΉ4Ψ¬ΨΠ–9.ώk»žξ»n³μ¦ΡλήvfΌΜiW«΅θzFhο:ΐδS·še―Ρ‚―ΨΦΜ [°X€φάιόP‹HgFv¨ιΡδ€ΩΩεΙ LΞ:ƒν*KΠ€&Ž<²PζŒΛkk~\«xTνγHƒ*=Hί; έe…JfΓ9ͺ˜MψΠβ‡ΦNΟ™ pF^!όθΌ:˜ήΠ/ΆΞ½Ω)ˆΨΙϋ‹ΛΘ#PjΩ&γHό^λ]@.ΐ‹yΆ…MŽ €8qΔήΞZzΜfΑΊ3)ΘZM[ί9λΫεͺ²+WφaUήΠzQ/Ϋ…­ͺ!ΨιΰΗί”Σήσηη7άEhA$‰½yœΝ­ΰv²ΘmΥόΧ!Lω‚¦‹N{ο|Υjσ»†σ‹Όœq’„`K@ uμΩ$…%; j4G1βΨi#‹žζ3 sކδ*ꢜΊΠ&L³€§σpŸ}φ…,ΚΠh r²aW0 + !J»"z‘τIβΜ!ΐΐ΄?QΐΙ)ΘΆ&$ΐ<(γδΞΩ…ύΣ.μ+.>ͺΈε§;΄ύεη^Φ+V0ΈΠ ¬Χα*m½Cd•ΐˆtpλΜr}Ο3{~Δ0x΅/£`Ή6UkυΦ’ιυζΞ}”ΑG¬†σ΅TζΓΓWΧ /[Ώƒƒ‹mTG4ΖΙρ΄Ώ‡Ÿs·ΜqΞΟ²ΑΔ„±R#ύψh_€»Φˆ_y|τ&©ƒŽψΔΖJΠD’x@[lκc^ y&‘CοHΎϊΏγ#ϊξ4$1W§ˆ$ν²Έ^΅υ²qδά?β$ψΝ\Υ yΝ—hˆΐVG\σ °ΙnςΏ‹Ξ•τfiωd›¬${ ½…‰χ`'y‰zz;Ϋ#$NvŽ'`_¬ +`ξρͺEΏΙ„w‘œ0{vvΞ‰χCλ2δ|Ί}©xb;ρ7 +ρ£]ξ•$ιN »EƒώH‹3¬ΨβπΆ₯Ν‘Ω'ξΰ³$t +!Ώ„πœ&Q<›MƒΪΧHΝ[¬άQ6Z!°ήT8$8Ύ€α³ΰ‡Ιy]έβ?:stΞB…μη‚(a͜4zrά1ψ~λzά'=βΥͺnΞ¨ ₯‡“¦cA3ΝY…ˆPNοV»τo πžΖΚϋΈύbρHσκ°ΈΑ–£‘_•ω ρΗ +—ψ‡ΡLauλ•;Ο΄ο|>šEb8 ²ΔCVέ!§ξεϊ£] +:l0λΤΡΎ_…z™Ώτκ,¦ˆliv㐇Lšΰυά6·vΙΕη¬ω€Σ§ bž˜όΉ(φΙεŸΨͺΐpκΞ.Ψ”ΡΈήPΩ€ ˆTy&^}( $™‘šδ,φRq¬’ΰΌα¬…EΕ΄~ş―ž l ˆη? 63§ΌlΈδ,ΘG/΄φGΈ € gΖΙͺ₯]ήδοœ¬·ή‹άΩ10o»ΠοYθ(4©ƒϋ!š†Άœ†»e‡ΉXGvhΝ~v±)τˆ€ ό9\Ύϋώs·Ύ©drRΖt,+‰NVr·upO,ΌP)Πpι£€κ;g¨„μΒ΅-†bΉΨˆΎΪ"rυ§*Qwρpn»El ΉΛΜ< +Sm¬Ψ³?Γǁ€ji]θGθΦ;ΫώίbŽ›PlhhB§eIFNώΰωIΖ U/η \Δδ‘ΘΛτQ|"εŽγ܈bήΣρ~|@@bβΈ -ŽN½Ω‰Μ/E&T‘S±Θεη ζ:Βu+€`ˆο°νNΠν"LA4BθςΝ³pΜG§£αn^Nj` +rΨ(δ΅ν”½Φq¦ e£ΝAjΖνΈ<ύψώ?˜F +€‹ͺZQp8”]ν›8θΒ'§>{d^Β–ΘY ₯|w[ψ_Γ«.θΐ m8ΎSθδNγc Z‘Y‘DͺΨς”ŽΉ˜–‘‰GΒl ‰‰„ e©Δψ §Ά:!τ€^=­e]ιΕŽυš±€m;2WάζΗύΤΉ&§l]ψΦ έ#Φ@QΣά’†³Άm§ωυΆaP §z±ht»\‹·‡_lΡ4‡ΕmqΨ[λσβ +‹J΄¦”yιB‰·XŒΗΏ˜ˆ†ωͺΠu§–μ=ΠN\8X›I“υ±πχΫj€Ο/1Γwšί[WвΉΦ©)ϋΙ1ω<°άΰC=―6l;έL"˜Iβέsι›$x›ƒςT>β‡ .ŸŠςή6.RΛ/Š|Iˆ49],l΅BAσΟν“]ώ¦%xœ·,,χtV/ς²8Ψ5,m)GΈšZܝοjP±‹ +eό$™ΗΐΌ#ιWχ4"•}ΐδM½ +ωΣΖD*5‡?bζN–Ευr'°ΌΔΗ Œη•ΒΌt֏ψ*Μ@z₯βF©L‡ξΈ½_‰όΊ/՚πμνοί„j©΅κ€dXvω7ΘAq„υςζΠ^7‡;f± &Θ&b/SwΦ¨t 9ŠΗ[M€Ι&A’Ȧȁd³½ 2/νuiI$έ¨qU;Gά;―οπ‰“on}βLζDκβΐΊκv'¨ΨRι83wo²`κl―ψσ Gg›΄βE|2ΕD.GΞC₯.bΘdύŽwvκœ&ly`₯Όa½Ί7VξœcΓ‹!ΏJ+81luρœ₯θ"«ŠΪ«Š:xzzΚEΞΎxϋΦΈΤ»3θλΎπ‹ςκdήΑΡηΛj;:Ζ&3‡N½‹S7U1ΌΤΣ TQΐŒΉ)œ1™”‰7μ©± ₯σΣμeΚlHέΚqͺ£λyΤνqΕ㝬χ'h;4Αc5œQ­Κ:mχƒ]Ν—…σ¬K Ϊ…e7mζω-Wn ΫN6‚Ϊ»ΉW‰ƒΩYaΩ_Ο|aq稜œΨy9@³"oζΞv–£ŸΡ7»Ξˆ:_-‘dψ²:σΟεtž—-jΈ^t]£Όξ`fZ8ωμˆ,›τ£ +ψ‰»ωρΖzO'‡s©q8h%±φnL@Λ 4A9¬ζB“―Ψό¦‚²΄ 7Άβφ+9ΥΩίoY[Γ]’iά™p‰0Ff›ΗV€&«uΪ3n±‡θ(ΑλM©Ϊ O˜HŽΜ!z›9oΠ%#?‚Œ^ό8Cωήt.|ταΌ‘­¨pυχŠ‹ϊΧ6„30b>Š!œ:Άxη ½58‹)&ΒU,yΥ°%ΤGH˜ρ΄€υΖ —λPNrN\φο/fž&cmσ²,ZηΔΈtwEΙ‘δϊ78υͺ(ήΜΚό-€KΠμuQβ0„ͺ»π1^ηΓs»[?T…BΧΧC³ΰϋυύAηθώΠ~”Œ\ +&ϋ‡Β…Θf"#΄Ω.`L/\Όόϋ7Λρiή#-KfqcπšjΔΐ]γΜό6dΗΜw $&LΕΘΖ'ΕW@50C£βT£Ώ“ ZgΞ’†Y)(£‡uλΣ‡u/bΎƒulΓ°NW?($9‘N€Ψ ΪθZπ·FνΆΠžΑν ²ŠŒ―hΰΥΕ>˜ ΗΣ±{_άΨΆ3­4L‚μŒΤ€ΒG Œ0q”ξ­*hI²ΞK‘Μš–`υ{θΙ!!wυγΩΙkNV3ž₯ΰˆν–ί·Φ"Lαμ„<ΰŽbώAκψΑVnΦμκh#PdΠώJΙEh¬’0…SνΟeŽi:θώSŒ(€ϊΏY€­':ρΘ2« B:G’€‡Jͺ«υ?G˜Ž ΘέAΐέ±Γ°I{Η?Ÿ ³KΖgmό5(ξs0uVs<œŽ»ϋu¨ ΄Nϊdž*l†±y£ιŠγL₯C‘#\Ψs‹™! Ρη{dΐ›²πWg{+8#O +ΎfΑ_@₯wβ‡~™ΖNˆ½¦Τ œJ +ΩαKΈ]Γ3Ψ’#Ό;—ϋ’ vJ—ΦΘI˜’ΈVΩWΰC\žCšΤPξ`Ί_ν₯ϋ=ZΆΕ”φ*α\Β₯U•oktQ€•—»TοΪ–t7Ίξπ†oC΅8υX΅ν#‡Πρ‘ŠΣ ζωu8‘ΘPoά#T/3No₯Η8γzδ’0Ηα&N`I₯„Ο [΄ίΏβ|τ"΅ΕSα›ŒΔέ<€«fžŒ° +½.©μΩξ8sΝ ¬»vΞNEρhΩ4Š ~JζΟS^Χ+Υ±³ΫοΔΪ(Ο3昏-X›½œJ‘‘»‚S“žδΔb0Π³ͺ.ŒBR>ό:Ά˜υP +K+ώ%Τβϋ;[AψO€ΔzCΔ(Ψ…§^Ύ{ώι„ΏΝ2ϊΦ t8πvHCζΧΊτχ”έο3Ρ‚l6b»‘ΖΑz‹g\¨ρb…œlύ3K8OsλIϊ»N‹€η +πώ+G ιu\ZΠoΫω£±υΌhΉΰbi―ηφή–θRχϊ ή[wχ| +rψC't'€»)­€{L<Ν'ςΉΛΈHyOΔ>:ΰŒpR3pΥBi™=Φ2(Uΐκύoc4>b†•œ„ά)ΈlY–ZΈΑ₯`(S5›΄υ䔐ς.ο:ˆt^‰νΠ…7ΆόmνΟ#«Ώΰ/ζžΧΧ…‡‰αE‘έ$4ΙζF<›±νSΗG——[ο%τφΗΠcTJR•ΑΛcχ|š…q2–Dβ-’H6ލvΓΏγΠ3:Mώ]^!έ½Κ8[‡Πν<Α TήΨA΅{ +˜ΦΎ\‚P\MήΧΧMΟ™ΎƒφͺL„_nΐ8’Ί€‹/2~ΙK`αˆ‚Ι΅JξΕ²Έgq^βε€ΡΧόΡΧ^e'«@κͺΈγ;ή9>¨ΰϊ9ζΗS‘¨¬ΧοžDc,Eθ]ω…³2 ΅γ±ŠΑ d–rβJIͺ…Vι·ϋuζ~»l0#δγt!–x9¦?Η€γι˜T&‚?LAfClΖΛ*Π½C‘„ϊ’ρ8Hš—%ce`<ν$γ‹’&~’’3νρ–“$`β³ύ“BsΨμ{ΜΖR·Ά*B 2tioΉΤ5ϊšχψ¦W»7‰₯–¨υΫ7ύΫ°h €>uοAιΓΤD0]-g‡χ!l³΅˜A–ŠδωΠΉΎz²»ΘδpF_9'» Ώm+쯚\Ν `Ž9!N† ΨΉωΫGΦΑΗ壏Ρ*V Ξ"ͺ +Ώk‘ˆA\ψ…9vΧeƒΎ:¨Γ(Ρƒω‘Šƒ€ϊoθΕ@χ°\ν―ŒΌŸ ΰ 4δ%ΖφαΧ1•‘S?G“=ͺ(_Ϋ¦₯ϋHXϊΉ gfyΉΣU«ΠCh²5” ½Ύ΄‘ζ–ά ) +° VΈ!ύ‘^•Eε +ΊάΣY½"aΞ}ƒpΨρ©³œtΞΰ‡ͺY†gc!J§6ώvwΗΫ(F²χ„ΥUžΨb^ε拃˜ιΛΒ;ΞU»Δ »_;.Ÿ½θߟ'šύt’†σάν;u<4ώm]Ύ!deB€»|ιθͺ?_šΫβ;HF>3™Ύ ŸDgόΝqP fF«0ς ™ˆAϊtU5τf €η΅Οξ’h!}Ι?‹YΞ\ χ„π½Ν9υKήΆ%s±Gށ]»ξ·Lwβίη€­IH'ͺΏYa²―8(φΘEΓbθ8'ύνKŒ˜%ΫP‹QT²Ϊ.Cΐ…ΣΉΕΠ^ίˌοNm?`8‡ΔΫΤ;ω~m‘uI=₯Ό9³γ­`†%™ƒ+’_›ˆw0 WQ*FoD$ΫGβcdφω―p·J΄ηϊk̝sŸ± +oyηώ +TdXʐ‚ΘΌ1C“ΧΌœΞΕ,δRXω’ΩGπEΈ]²ΏT4ηΰΫYl±‰{ΊΓΛ>D656u`•αΟΏ‘Ν (ΔplVΠ°λ«})θ’—ς£Z”Λ3‚eυœούΈΥnA@JSZΗΞΩς±)šΡ΅ηޝKE7Κ²Έ‘p€d΅V‘Ib0RΞ#-ΈCP’³B ‚ „LΏύš‘ŸCŒWfθͺPoCvΎ–ύ…gΗπ£μπκβθό}σ…$_Ιπ +_π{½¬i@7aζω±*°(~2αB–w«Ϋ9qQ|*γ΅]–ΘΧ±όΣΝκ逓—Ή{‘„ 2Ξ»KΊ +AυήqζΥ/Χό,wΞπr“jΰHρΤ +½―χΈŽŒδ3‘Η}ΰσtΆͺψIM6aύΏ +~ƒΟ…Ο+γ]ȐΰπωjΪgό\Μέ“}.Œ1I4:ߚυ] 1‹TRD{Ο€iψ}OφΟ#Φ.ΦdΧΩξ«ΙIo5rιχ+@ιξ>ΜQΥtοΌ„/ή#2#ν`0«Σσg²o¬1ΊͺωW3Qv^rOΙ¦=²Η$†n8›$έ$Υ°;€BΓ‡ΣΎΒ“* ŠƒΞψ2S†a+€Μ άφδ>,ψWh΄ύ§R45”°«]ρ§y/Ή+ΠΏEJ9ΞΞ `£yΉ‹,‹δiε]»@š*R™’Žθv΅δδ ;wωt“ Α”ξΠ])‹"Δναλτis–Tαλ}IΌ„z¬ΛΊqψηύVύγΛ(ŠTψ7'>Ψi}Ν6NΆΣ9ρ”₯οπ¦n^΅΅3Šž9•Ψg Ζ’ž΅ύ'cW- ΨJΝΚNύϊPW3‡&Vδ•=νj0M·ƒJ†r}ηgSNsΧ6Ϋsς“œξ΄8ΧΨάΕφ gΐGπρ $;tξ4~μ Rμ­ί†š§[φ?­+ύ~U»'SΘ—˜οg½°‹±ΩZHIAεύΈ ΰ¬ξ™ nyλ *χ.΄Βf!Ώ#ϊΡΆώ +ΩawγoρOw;‘?…XWί„JކBQ\ϊη!ϊ΄­=ψfσΥ"@€CΐαaένξŽρc―d«ώΖ—Œv(θS:9έ_DάΖι–Ώ€{+X!0N·>ηˆw©Φdhπšcη~§‹±$χ»1ίΖΰρ h•=«œyΨΪiω‡ύ#‚U|­nΒ‚Ώj§δ3ρχ~—@ζVώE DŠyχπu,ΖΝΆzΜΉνBς.–v,ώΐνW7sηLϊXŒξnΎ«oσUΥ;ξΓΞEδ#.βE:ΡέrΛЈ2)ͺ ‹Ύ>*υ£ζ§j2σΞ¦(#^Ω=Ÿzƒ.²u„FŒWΞεΝ ύkο£=7°η*ν_,QfIŒQ‘‘Ž;sξ ιιΥΏώ`ή2§ +endstream +endobj +341 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F198 89 0 R /F201 90 0 R /F204 91 0 R /F212 94 0 R /F470 595 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] >> +endobj +342 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1109/EMSOFT60242.2024.00006) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 275.333 697.182 295.041 706.622 ] /Subtype /Link /Type /Annot >> +endobj +343 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1109/EMSOFT60242.2024.00006) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 687.227 213.928 696.572 ] /Subtype /Link /Type /Annot >> +endobj +344 0 obj +<< /A << /S /URI /Type /Action /URI (https://learn.microsoft.com/en-us/azure/search/vector-search-index-size?utm_source=chatgpt.com&tabs=portal-vector-quota) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 275.43 408.265 295.041 417.777 ] /Subtype /Link /Type /Annot >> +endobj +345 0 obj +<< /A << /S /URI /Type /Action /URI (https://learn.microsoft.com/en-us/azure/search/vector-search-index-size?utm_source=chatgpt.com&tabs=portal-vector-quota) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 399.004 295.041 407.655 ] /Subtype /Link /Type /Annot >> +endobj +346 0 obj +<< /A << /S /URI /Type /Action /URI (https://learn.microsoft.com/en-us/azure/search/vector-search-index-size?utm_source=chatgpt.com&tabs=portal-vector-quota) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 388.348 259.19 397.693 ] /Subtype /Link /Type /Annot >> +endobj +347 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.nvidia.com/en-us/data-center/products/a10-gpu/) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 224.187 308.686 295.041 318.151 ] /Subtype /Link /Type /Annot >> +endobj +348 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.nvidia.com/en-us/data-center/products/a10-gpu/) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 298.684 207.612 308.041 ] /Subtype /Link /Type /Annot >> +endobj +349 0 obj +<< /A << /S /URI /Type /Action /URI (https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 278.759 295.041 288.104 ] /Subtype /Link /Type /Annot >> +endobj +350 0 obj +<< /A << /S /URI /Type /Action /URI (https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 268.796 201.877 278.153 ] /Subtype /Link /Type /Annot >> +endobj +351 0 obj +<< /A << /S /URI /Type /Action /URI (https://objectbox.io/on-device-vector-databases-and-edge-ai/) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 254.426 258.841 295.041 268.338 ] /Subtype /Link /Type /Annot >> +endobj +352 0 obj +<< /A << /S /URI /Type /Action /URI (https://objectbox.io/on-device-vector-databases-and-edge-ai/) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 248.871 242.027 258.228 ] /Subtype /Link /Type /Annot >> +endobj +353 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1109/BigData59044.2023.10386517) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 199.058 241.561 208.402 ] /Subtype /Link /Type /Annot >> +endobj +354 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.pinecone.io/learn/series/faiss/hnsw/) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 179.172 252.969 188.489 ] /Subtype /Link /Type /Annot >> +endobj +355 0 obj +<< /A << /S /URI /Type /Action /URI (https://snap-research.github.io/efficient-nn-tutorial/) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 539.045 687.235 558.996 696.584 ] /Subtype /Link /Type /Annot >> +endobj +356 0 obj +<< /A << /S /URI /Type /Action /URI (https://snap-research.github.io/efficient-nn-tutorial/) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 677.264 488.942 686.8 ] /Subtype /Link /Type /Annot >> +endobj +357 0 obj +<< /A << /S /URI /Type /Action /URI (https://github.com/facebookresearch/faiss/wiki/Guidelines-to-choose-an-index/28074dc0ddc733f84b06fa4d99b3f6e2ef65613d#if-below-1m-vectors-ivfx) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 537.954 617.528 558.996 626.993 ] /Subtype /Link /Type /Annot >> +endobj +358 0 obj +<< /A << /S /URI /Type /Action /URI (https://github.com/facebookresearch/faiss/wiki/Guidelines-to-choose-an-index/28074dc0ddc733f84b06fa4d99b3f6e2ef65613d#if-below-1m-vectors-ivfx) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 607.526 558.996 616.871 ] /Subtype /Link /Type /Annot >> +endobj +359 0 obj +<< /A << /S /URI /Type /Action /URI (https://github.com/facebookresearch/faiss/wiki/Guidelines-to-choose-an-index/28074dc0ddc733f84b06fa4d99b3f6e2ef65613d#if-below-1m-vectors-ivfx) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 598.257 558.996 606.908 ] /Subtype /Link /Type /Annot >> +endobj +360 0 obj +<< /A << /S /URI /Type /Action /URI (https://github.com/facebookresearch/faiss/wiki/Guidelines-to-choose-an-index/28074dc0ddc733f84b06fa4d99b3f6e2ef65613d#if-below-1m-vectors-ivfx) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 588.294 374.204 596.957 ] /Subtype /Link /Type /Annot >> +endobj +361 0 obj +<< /A << /S /URI /Type /Action /URI (https://arxiv.org/abs/2412.11854) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 353.9 358.46 391.001 367.964 ] /Subtype /Link /Type /Annot >> +endobj +362 0 obj +<< /A << /S /URI /Type /Action /URI (https://arxiv.org/abs/2412.11854) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 419.016 358.46 524.413 367.964 ] /Subtype /Link /Type /Annot >> +endobj +363 0 obj +<< /A << /S /URI /Type /Action /URI (https://arxiv.org/abs/2404.06004) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 521.377 278.751 559.18 288.263 ] /Subtype /Link /Type /Annot >> +endobj +364 0 obj +<< /A << /S /URI /Type /Action /URI (https://arxiv.org/abs/2404.06004) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 268.796 439.428 278.141 ] /Subtype /Link /Type /Annot >> +endobj +365 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.usenix.org/conference/fast25/presentation/tian-bing) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 466.001 209.02 558.996 218.377 ] /Subtype /Link /Type /Annot >> +endobj +366 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.usenix.org/conference/fast25/presentation/tian-bing) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 199.058 458.815 208.402 ] /Subtype /Link /Type /Annot >> +endobj +367 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.optimum.com/articles/mobile/choosing-phone-storage-amount-needs-guide) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 179.132 558.996 188.477 ] /Subtype /Link /Type /Annot >> +endobj +368 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.optimum.com/articles/mobile/choosing-phone-storage-amount-needs-guide) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 169.17 432.234 178.515 ] /Subtype /Link /Type /Annot >> +endobj +369 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.optimum.com/articles/mobile/choosing-phone-storage-amount-needs-guide) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 149.244 558.996 158.589 ] /Subtype /Link /Type /Annot >> +endobj +370 0 obj +<< /A << /S /URI /Type /Action /URI (https://www.optimum.com/articles/mobile/choosing-phone-storage-amount-needs-guide) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 139.282 432.234 148.639 ] /Subtype /Link /Type /Annot >> +endobj +371 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.1145/3639269.3652200) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 429.97 79.506 558.996 88.938 ] /Subtype /Link /Type /Annot >> +endobj +372 0 obj +<< /Filter /FlateDecode /Length 8136 >> +stream +xΪν<ΫnΫH–ούΑ.0ME²x508vβ8§3V:}™,Κ2-U›"Υ"Ηωϊ=·’HJ²3ιέ‡VT±n<¬:uξ‡ή³Ε3οΩωwžόΏψπέτUΰωΟ7KΰοΓν³Δw½$€?ΟυTςμΓΝ³8?”ΥQβάωΡ$qnπDΎsΎΡλeeΟΉ­6\ω>ίΤU© σ…;sν[½αQΛE«ν—υΚ‹ΪεŠ‹ςθ_ήP~Π*πR7υS€™ΰ Ό ΔnΟ<7K#¨υžωΰžDΟ>¬ γEΩδ›R7AΒ„ΞiUή曣ΤΙΛyΞUUΙ/WΧGίsς›ν „Ξ¬Ίmξ5 γŠz‘χςrφγ«PςζΑBφaƒn菀sy&ŸΌΈ‹S|ηΓ¨_ΣsΩέ³αύ°YaβzF»*Θ†ΟX6ΝΊ>¦΅ςν:y°”Ι³8σ\@’nΣιMeάj³˜ϊXλeS~·:pqΉ]~1鍐hόβ?¦'L27‹οΨ?”ϊΧ`π3Ψ­`Έsq +Ο3†θ£™7ŒS‘σ6_­MSί=E)―Z (ΰPTiζ|Xζ\gΚOΈA›f»s‘³j‹ΖLL Ϋ™8Ÿ]܏e=‚0uƒ ΒτχŸvNOΰǘrQσ¬Υ-/4¬U2z5Xt?Mxš†ΰ Sz,%ΞΕΛ—/ΉnŒžΠHθIM«u ψΜ΅Mml‹.oΈς½nαΉφŠΰ›W‹ΰ#A”ΖΟβ+.Dι#@ӏο―©'AβS-Φπιύήα{”ΉAšΩ#κry1(υ ‡»/£ΙΨΤΜyΞ΅?ΝN€€Ό8Γs‘<ΐκ!βL’ΘMΏ ½‹^αΣθΑ»Ζ‚π—νΠ!!NFϊλ‘JφχΆ΄υ₯4όΆΜο΄αΪί–0π9—?]BΙN΅ζΣ>΄ƒ©}ηM[~†Ξ?Ζ …Z&XvήVΈ 0PΕ@“J$€*V0mή«?]ζeaJnΊΜ $¬‡MŽΒ ή%BΌΰφί–νs.¦`ΏY…―U―uιrσŽf#•η*3Œ3MG ή_CΧ‡:§[θΟ' κΝjΝ|"qˆαχ)v“€n!"yrΰJQ Έί‡£ΐsΰHΥzŽdΎf^Θοοέb»!±soΙ₯—εΒ”ΉΌ'ΰΛ>,ο¬B`C@Tπœ§JωOŸα)?L"$λ;  O„Y‘Ψs +8ƒ₯A˜χ ;μhμ…ΕU τ{*HύCΧKύΕUϋ(nΰ%n%#ΜIž@ψp!ΈoΜj…L,‹­X₯+f1³¨Έβ]΅hs³ΡΞ§ +'εΝζεEΨ3ω₯ o€φd(ΞΑ“D09Ι’\$’ζ°rvoφn!pΘ$΄'’ +ΰ±+αΝξζυ&"Šr[yΜw/@ͺ–Νaα9§ηΙ#ψΞ6…±›Šb₯BΒΝ^XYπnZΦ¬`Ύ}μ2έΩΌτιΝv‰¦wfΑžΒ^πί₯.ξͺ£H4έ2υ†ΒΩΚ4—Α[mϋ'²aυ²-‰ma Ό)©IΞKOBfƒΣαΆWΧ-π­”ΨdΘιλŽsXζ§Boΐόψ~ΐό°ͺΗόπΆ­Iΐβ€0i3sβm8ƒώdϊ˜‚εήC¨BΰCi$ςh½EΑ +Ξ=B·)D³ZώΏk{Sϊ +θφxN«DŒŽ{8Wδ¬;]&Š„ΩšΪή ϋZix­2ί‡τ!°¦4€’…YR΅MP_Y, ‘δCώF–vŒΜsRξ‚(U;ΪI]ŠFβ]φ$›½ήΝ”H±—zq£ ’οBεœU [S9€–²ZΒ~ΖΔY°ζ7Τ&SηΓ·3Π žσΈσφλ^πς¬p-₯ν­ώΌvώe[XΪΏƒ !ΜR›΅.ˆΆ%φ<”|sήJνk½φ…εΨωHq¨₯ν83«₯‚ωœ»π>βlfΫ₯%±J¬ŽLΒ8½vΣH<€aΔL• ,4θuε13΅ ˆjW›v5”ΙίυŸ›u‘‹ΖΧ©ο7H ζ"τ3nΎέκƒ―ΆWT ίυΑUΨΔΥκ€ «”λ«`dͺtF;G΄sΐΊΑ‘½§4hŒ~$’η₯™ΰuu‹ΈΰƒώƒΒ’Λe@υθIB +*kμ0νΗ£ΔηέsLfm]|W“%KΌΜΎB)ξUE¨m‘· , 0Ώϊ-³SUX +€ρνΦ˞ρ2 βα¬}γeύ2΄χ3Αχ)JQr-θšΣUΣuCΧ’$o9+ΊΙ}mήλCrœSχΚ€ƒ ˆΣ½±σ^λͺχܜ09šd> +έDΣ(_θΪvτΠi―τ@ΉμυόΤΛ­ b΅ιQX‰ω²λδα"t‹sΣπYϊμ9g*Š£”΅Έ|Ιv€ΰ₯šΥΥIrσό―σ₯nλƝW«Ώ4ϊΊώλΊΪ4Ί€g‚ςB*",ή*珢-τλLΛ A0<ŠώξQΜFLB₯²o@ϊΙΕjπ9©ή,΄’ΛΆ¬ΎˆΝξ’ sU[GηUωOuA/Q?ηΪί€/—†ΛgFΫꎲ]πE¬°κDίPπX#D)Ÿρh|l-³’­œŒ&pώ_e ½}Θ‘ L'_cΉΝ2Π(Η[»Η­εΟ π΄HΙζ’΄:/Ξ.NΘ²;₯{γ’e'κ7rΫ‰ο±c(€SWβύ9­¬/zΏ)9s~Β₯6Nά‡:QΔo6³βιτžρπžŽ/Aζ¦^6Β—Ÿqž΅?*΅;"IχGη>z‹I|9?^.ΠΎ¨D6‡SόΆΉ" ˜‘ΰω/ΩJΝ'Ηrϊ—rΰŠOψΦϊ†'$μ2σnΠ7£AG’˜Ρθ €ϊϋNΣΰu—/ΗSΌνδΞυοx=$<`˜CbMό ΅F(΄FdHŒp|„λαΞD(–Ζ\7Ε •JΌ Δ(ΚΔHώ"”„©wDͺmtpσφ1H:ΓuΧ€ )ΥxΙ»IΈ… ΈΩVB'8‘T±θ=]w0=AΒϊp£όααJ€6>¦iHOφ)˜(ΙD8οτΚ>>χDθŸφ£ €Κ·U΄22uzΟ²П7F—Ÿ‰ρC΅Έ}k1†ͺΧUΉΈΝYρτe—ϋˆ{#HcηνεδΜΤw'οήsΫΫJμζ0θIΞ~£eΪ‹]u6\t]Ύ"SHΥXΡ½t%γ£&οτ֍έΟHV}(A|šs/ +ušΌθΙΡΠ δΦ QΈ {k¨ζ€\; wr3/κvK γA’&βύBDη…YpAόzPB‰ͺΟȝΉ?r£žύΘ ‡€Ή3ŽDΰ£Μ C—πΔχTG~ς'TΖ§½?>πwλ6|/ή•yU +£Jn!ΪaΤ‰*9hs=ζϋ]5 ζxΧ™βyŒΫ‘εgœ­νυŸ&θφ•R”ƒ“ŽžΓCšήΘzH$ΚΗtφpAUΎ—2$·#­Γυ kNΧ9MVνΤ»½ώU7}„V›¦λ¦7–A¨»>‘΄šή¨ΊΧΈcFΔ<Œ€O½ΣgΩ›Ώ¦žχ_O­Jg‰(Ό"μt™φmώ!ιtς>νύςc˜=Ωύ Pμ_P²LΞΛ0φEή4\ϋΦpΥλͺ}Ξ5π +ΐα5ߜ2υ΄"ΥΝ3Ώ+€²Λlo@N#'6Ύ§©ACx~@@™Ϋ—ceζK6ΪΗ©ψκΠυžsΕ{ Lβ›Ά0dfƒς™Ωθτ΄4\β„y!-Μ|βΜ™ιΥ‘ο΄dF†Ϋ«Ž‹ŒΨˆl±l) kχVq:1 dΡσ5Ή]τρΐ:΄Ψθ›V7ω€ © %s²Η(gΑ«Έ(ς‰„ΈLΞΔύΗ_΄DJ±S΅œƒJΏΉΫ«£ϋ* s΄Έξu΅»q Ί―Μ¦nl ή(φΥslΈT?,ΧΫ†εŠeˆΓž[w¬SyΚβ‘Œρ΄ΜΰΥ¬δ£m ΪήιλZ“a.œ‹|Α oσΊjΘ(7—°bmΝ]fsv‹έ2ζ ½(τz±Ρx0Ρk-N΄ΐΪι υTΓbε‡βτ|ΰKD ~ '`Ya\ΪΦ_ε‹ΏŠcrΜϊ΄€NΗάΜv“$vŠjΑ50V±$‰>!9“:‰=j–Ή.Θψ’$δΝ―ΉKmωK竃λςE–Μ9ΎGΈ δ~ΤιOγ{P‘d8Ογ%%(­¬™(p-Gj7  +ΡΫD7³‹σ‹+4ΔΤ=§ "9‡―ς3­«p–Ϋh¬’Z―rk²Fͺ‹"0;S³14έ‡H½γxμAŠ<’€;(w―pα~r6Πχ8΅„=ΪηS²ͺΉ<σ‚d˜Οa)lΔ#ΒΐΔĜ„d_hψ@bvρ ο8"M:ώTvrΊlνοs lUyd#dΓA|`€ϊΑLΑΊ~πωλv΅^²ŠΐΑθΈΩΈ0'" SΒ' ι]ήR$x—7vpWsΝ+ΒΐߜXiwHƒ@χ³Ό6 Ωδ¦bΟx%‘φ]λ3>ΛΧ.Ϋώ‰‚Ύ†Ά{«LEίμ λ@ǘ+/τ‡ ο εW*t“qδόtZ—zΝvΆΓηh.q°Jν5&ΧTΣ‰‘5gΫJYςFήβ Y(1< 2€(6²™Dϋό4ρx·ϊ‘ςoŒΠ‹έΖ¦ώn„YnC±{ž–³ΚΖ Ώ5[νIˆσλΛΙ‘JQ.‘-ο‘1`S@:%3ŸΏAΘ<Ϋψ:{σΠέοοΊϋ=gD‘*1―/1&₯Œβ£I[1•―ςU§ΜFYWx[x#0Βyο~β½£QwοΪ™?Hβ•£#ρBΚ³NθPςΦͺ;ηεέΕϋΟυ}ΰ=’œweŠψρ`"W i7"δ") u-)e8’²rύˆώ‹rξ>·Μ―³2y 6ήI›­= +Γݜ“ PΡB$Θ4‹€ΪWζIζγ―0FEΑaύ¦ƒ½τJΟs9e ,.ˆδψ―ϊbΛ-’Έ‡i朷†Δ\Γ 31θrΊδΉΘ—*u‚ϊ‹.‘θOκδ‘J+†/‚6Φ΅qŸtr(―ιZSΝρNTGϋΉΣ鍰Lν‘@¦z,ά’׊u»MNFά-Eςι­6TYOοΝΝ3…%Β-υ―™c4dςες|ΙsΦ9ίS”΅sψΠφ³pv…Š8›R‘œΣυ3ΥL©Π5₯«Gυ •CΊήPΝΌk΅5<ΧsEυŠΚ·έœΜsέ›?ξυΡ½>έ~~”νΞύ³.·(rΐ‘Ϊ#$#΄€ˆžΟlϋ€^=h+)œιr•›w"ξρΣεΖH<Η;6ωr΄φV‚πq ±σžjΕ6°ŒsO;{9 /Kb`εߏPL=fIšόΈ”΅έ‡κ}Y³‘sEΪΡΉ$©Z f#C6ζ“δΗ„θͺGZߊ˜z ,κ‰—@₯ϊΐn=λc.1ȟ8δaPSτ‚Xπ°Η^π•φ8ΪEαΣά`BA[άTkŠ…ΞΠΠb|’ή‹½‰ο?%$š΄Ά½ZiΚ«£ +έͺ²ιšΔ<³iΆAΆΔG ΅ιω—Ψ/w”+ +o‘”sT€!G qΦBgIoΪ‘‡“ΞS4dγΐ{ς&xθρN,s6½£–Noςθ]™.t³QNCSη#PΒͺ‰¨2ΐ’4uϋEίΙZc‡Šcη­F9τΌ• ωl*”ρθξ―σ¦K‘₯ΚyaΦwΨAΉ~ΎIš\Β$7qΰql&_±ΐΡ䣞τ’_€ƒj³©…Ω'‡QL(7x²MQΑIx&jΖ{“αξ•άϊ\:M‡) r όύ)‚nͺΖτqΓ™"Φ ΧΟ&ΰ}Ϋΐ–ΙJΜXδ_ι»e»nžK%F-ψ₯ޚVj»„β™^φ9ΗΞKΆƒ†ΙΦ]‘»ϊŠ‚†ΟιƒΨ1o5ŸXAͺΫT€vKπ›–ψΦ!X3Ž™ηOf Μ­’xψd»‘Α8η  +|TλΏbC)η|ξΩΠψ‘¨ΕnαϊY[Ύδγ^vμ΅$ίw„Ý›ΆΞΧkΡξ°ζG ŸŒ.mZ19€¦,m>Ε™Νο +6ΣlΣ±™ψ°Ž»ύTƒIΘί [@Ÿ2~˜ω½ζ»³ͺύΩ!³L|Τ“(L·U] GpfŒ³:-Y½PGτ#ΛvύmYκ\œΙ@Ĝ=Τ/s,p²'~ΡC‹SO‹§ϋ‘U³GγGxg£$‹‡ή½Cυ]ί “μ«π°ϊ/x}5&‡ΝςέΆφ£ώ―ZΦ’Ψ™az€$9& :ώhI™βζΧ]$ܜά鍑"ι§”/Γ8±ΨY‡g΅-̊«Ξς¦Α„5™kƘ‹ΥNώqs?ΰRRω•σΆ½™ι7˜³ΘW(Œ=ˆ’/˜―,Tδ|²6P›0 Υ?σ“Ψ₯Τ;ηPL™'’ /FΈ­=ω R4ˆ}Δμ¦NΡsΟ€½άvΎ·Α™μ=€ +x›±šκŽΟnΚ|K‹{Δlω)ΞQ ?mέΝ']ΞsyΆ)χŠƒπό­g­}hη‘U{ά‚λŒa"Qͺ€Ÿ¦ΕX(Xζ1eu@„˝Θ.τގr΅’τiysίι! NΏΒβe»Τ«•Ύα»ŸPd+ήώ`ξQΎ’~šνξπrΞ9Δ3Œ»υτγΩg-fuE‘HoXψι™>–΁Υk—Λ‚ >Π‹aqBŠ Δl#ui#°ͺ·CH˜ψ βΦ%ύ&Ÿc¬f²Žδ"©αjp +TΜΉFν΅‹μΨοœ2™CπΎσFςt}אEδFͺUύ*“ ΊγQ +πύ4 +‡˜πΒ©ω€ͺ΅{rυ―o²₯‘¨b„o6κEo>O@/ΖΎθλzzŠΓq.˜>,eΔ'wKΥkl’dˆιωƒήθ•$Σ΅ΧPΦεƒQ|jh™ώ΄1VžΌͺΊτunς;ή>c{€0‚9ξΐbw/ώ‡ φουκΑZa3ϋ΅8=]Ζ! h΅νeBΝ8tΉ;gM> /Pattern 97 0 R /ProcSet [ /PDF /Text ] >> +endobj +374 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.14778/3476249.3476258) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 177.094 677.264 295.041 686.769 ] /Subtype /Link /Type /Annot >> +endobj +375 0 obj +<< /A << /S /URI /Type /Action /URI (https://doi.org/10.14778/3476249.3476258) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 70.076 669.135 86.893 676.069 ] /Subtype /Link /Type /Annot >> +endobj +376 0 obj +<< /A << /S /URI /Type /Action /URI (https://openreview.net/forum?id=HJlXC3EtwB) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 334.031 617.528 488.38 626.833 ] /Subtype /Link /Type /Annot >> +endobj +377 0 obj +<< /A << /S /URI /Type /Action /URI (https://eprint.iacr.org/2024/1255) >> /Border [ 0 0 0 ] /C [ 0 1 1 ] /H /I /Rect [ 444.791 547.75 551.272 557.107 ] /Subtype /Link /Type /Annot >> +endobj +378 0 obj +<< /Filter /FlateDecode /Length 3068 >> +stream +xڝY[sΫΆ~Ο―πΣh&’  HϜι8vβKμΤ±άܚ>ΐ-‘‘H•—$Ξ―ο R$E%žΎˆΈc|»ϋνΚ?XψgΟόΑχΕέ³ΓWO€Kψά=~ ©οωLά-ώ$†β―Ι_w—νœκω”AΡ?€yP‘ΑvΖfά$Ωr2 xHήODLTΆ|ŽΥ“U’™’v|Π*PΝ¬+=o†Ι˜$Ωί:ΑžO«ν•-pΠ[έLΨ` UΎY%6œβηZeΨkpν‘1ΟP<Ο«ΦΗΓϋυξΊ6hyυvΔδΨXsaŒ’ύ,TŒ£Β +š`ӊ$¨(-Ԟc£5³›TιLέ§ Ά­λ΄ΣUΎΑκ?5Pg_@³g:†0ΞKΔaφxc€6‘5AΎΖ uαk£4ςcpη€ήQƒζ‹Γ>Hi4RΚΈ'„ψδψΧ0Η2Η4ΟΑFύXi0`»,. N‹‚r/±vβΕ+4{¦ψΆvmˆ(Ό°v<*]k›œ™βqdœ3˜σΈ)ΰαBι|ς­uiσ| ~baπΖΰge•¬Λ#tώΗΨ8NŽ‘—‹θ f Μ-άkOχ ίUώΔ, p|’¬βΚ9έc•ΰŸh•όΈά<χˆξƒΧάΎχ“΄Do0(ρc0/ €φ!‘ςš5–α‘σF³ :ΣΈ—7}υΆΛ…¦ω²ΠŽΡ–¬A Ž#,€3Xžqvš§ιΡ>SΞ@F/?ϋŒΟ5ς˜ω;Τe Ν™ήΫ0ΔΡ,CύΐάZF6Ϋ`";Θ γLuλX3aFί€ΣρΙ“€#€#* xbΦ—2πSQΰp΄Η(@‰{ 3κΈ€%HΖ#‘!·\;D’ έ΅ ~sƁμΗΘηjέ„xΘ'΅V™vQοeήΙή4τάβηnψ€Π›*ŸcΛeO%°|ͺ*o‚ζWΰ7n²Zΐ›•_΄›cm<|$IιqΙϋΆ$ɏ‡Μ†‡ΖHdΞcl™/l¦{”“ΗƒΫδΛ^{ίΩ.‡qΟWy-μ‚%θΌgsM–@-J\[Ίgm!°’Abΰ3ΔΒ£B .‘*gv q'‹Yƒ;*Ÿγ46εXpHdmΰ ŸΫΈ—CΘ>°^Jσpΰ­d°ί|΅GιF½3uέ'H?™I±Κλ…Fl΅I—k=_©$ΕΚYΰ­~ΐπΫΆ6€l£6c389;Βώ—€|n¬d³`(V7Žξ^δtΒ$±‹aόΨ*Ѝmθ‡uηΝL…’oΪhΠόG „δζzήP!%΄”ή“žΏuš’€ƒ‚π>f1ίKI|ϊEi  +λ3gΙφS’Vΰn0~­³Ώ΅RaΪδΫV*kκZM]ΚΛm‘ϊ»Ξ>έdήld‡¨p*bˆŠ €Νt3ͺΚ‡¬„Γ’1τοc0~ΦxΘ&Λ'%Dγ΅5‰ΠvΣΙ†ικmζ¬Δ±Ha₯q?kͺ’ΤIˆΩγyŽ|†‘Δ\τε³Ρύ΄9 +έΉιn\ήIθ‡Α>4ε.#Ί½)S»MHe“)±έ@γβδκΦ@cIyež yΓC“}Ÿ― +.YΌQνκ?eΡv£B7)΄|“dφ<_­σ²€:|°t /κυozρσΛτΓ {Y}{ΡƒςΞ¨Šθδ#κΖW)‚zt#dM€Ÿ³mΪYprcƒ9cΥ”uΈPBmȚ±I;φυ„ +—{ΓUZR TΓ ω wςi­½x=Ε²$ί`μυ.nτšΠApςΕ†βίR”lιΆYθrnCτ~ΤΪΚ؍φŒ€Ρή?&€ΖZgΞΛνΔ%D—ΓΤZ3›ΦφύέθDxαΆΕ―±έΝΦ‘ΤΩ°$ĘSaρFUIꚯ‘¬έX΅;ͺ\’ +·*­ηv¦±)`βΝ2ωΖ"'lΤ=ξ%μ&₯ =iC˜»›έTMbΪg pεΚψmE[“ŽθβMι\/]ΙΊφωΌ.Τό±e(€³zhVΟΣ|ω8ώΠέH>ΉΩΊυcάԅVϋp}6ΟbΩ“!kpφC +xϋoQPώ„-1hΜ)ς O+Έ °Tφί‰v§'ώΫH/bl¦π `κΖμΞΐw‡(αdJuŠΜΐΆΆVΟTmvγ±6‰}˜gƒΪYς`Πf'Υέ9°\‚₯³f‡STΧάΡG“ϊb]Jς^–ΰƒξζ°ΐ^Η€Ά›YΚΨ1ˁω7шg-qνΪξ΄rtςnΒΫ-‘ϊI;a•”τ2e.Ÿ6Œ΅€λqF{βy;‡~ά·?ͺaΤ joT–CτZΣu4Jͺ’¨Ÿ—Έ›„%MίμΏ†ΩδfΎ©τΪώg •Τζ°-UKΫ,΅ιXo³Τ¦³΄&Δ–Ω­¬‘―—«M]ύ*ύ#˜νΛ·ά·ˆλζφσλΘL 9έ3οiω°ΊΌΩ@+Fώ» |(d7ρΙdŒ ­:Ύΐ8κ•IŒΩ εν6B6BŠΘ9ώΏ`kλˆaΐ:Yη[ίbΈbσΧN±J”‹x΅c†ΥγFΟUšΊ‘p?ζ™σqΕΑΗFΒΡΩE=ίFP>Ήlώ1²τ}OJ¬§ϊK‚₯σ7³χXʝ™³g$ο^ΩΘρ7ΰu‘ίXέ€,»«_«ΗΦ2 +o„κ…πj,²6ŠΘ:εAθqσ6DξΫ·—wΟώŒψμ +endstream +endobj +379 0 obj +<< /ColorSpace 87 0 R /ExtGState 88 0 R /Font << /F201 90 0 R /F212 94 0 R /F470 595 0 R /F551 596 0 R >> /Pattern 97 0 R /ProcSet [ /PDF /Text ] >> +endobj +380 0 obj +<< /D [ 47 0 R /XYZ 65.955 704.471 null ] >> +endobj +381 0 obj +<< /D [ 47 0 R /XYZ 65.955 584.919 null ] >> +endobj +382 0 obj +<< /D [ 47 0 R /XYZ 65.955 572.964 null ] >> +endobj +383 0 obj +<< /D [ 47 0 R /XYZ 65.955 559.679 null ] >> +endobj +384 0 obj +<< /D [ 47 0 R /XYZ 65.955 547.724 null ] >> +endobj +385 0 obj +<< /D [ 47 0 R /XYZ 65.955 533.417 null ] >> +endobj +386 0 obj +<< /D [ 49 0 R /XYZ 65.955 510.942 null ] >> +endobj +387 0 obj +<< /D [ 49 0 R /XYZ 65.955 498.986 null ] >> +endobj +388 0 obj +<< /D [ 49 0 R /XYZ 65.955 487.031 null ] >> +endobj +389 0 obj +<< /D [ 49 0 R /XYZ 65.955 475.076 null ] >> +endobj +390 0 obj +<< /D [ 49 0 R /XYZ 65.955 463.121 null ] >> +endobj +391 0 obj +<< /D [ 47 0 R /XYZ 65.955 680.56 null ] >> +endobj +392 0 obj +<< /D [ 49 0 R /XYZ 65.955 448.839 null ] >> +endobj +393 0 obj +<< /D [ 47 0 R /XYZ 65.955 668.605 null ] >> +endobj +394 0 obj +<< /D [ 47 0 R /XYZ 65.955 656.65 null ] >> +endobj +395 0 obj +<< /D [ 47 0 R /XYZ 65.955 644.695 null ] >> +endobj +396 0 obj +<< /D [ 47 0 R /XYZ 65.955 632.74 null ] >> +endobj +397 0 obj +<< /D [ 47 0 R /XYZ 65.955 620.785 null ] >> +endobj +398 0 obj +<< /D [ 47 0 R /XYZ 65.955 608.829 null ] >> +endobj +399 0 obj +<< /D [ 47 0 R /XYZ 65.955 596.874 null ] >> +endobj +400 0 obj +<< /D [ 9 0 R /XYZ 54 638.463 null ] >> +endobj +401 0 obj +<< /D [ 9 0 R /XYZ 54.498 96.339 null ] >> +endobj +402 0 obj +<< /D [ 50 0 R /XYZ 321.326 126.771 null ] >> +endobj +403 0 obj +<< /D [ 50 0 R /XYZ 321.326 117.078 null ] >> +endobj +404 0 obj +<< /D [ 51 0 R /XYZ 54 372.096 null ] >> +endobj +405 0 obj +<< /D [ 51 0 R /XYZ 54 334.048 null ] >> +endobj +406 0 obj +<< /D [ 51 0 R /XYZ 54 286.512 null ] >> +endobj +407 0 obj +<< /D [ 47 0 R /XYZ 54 725.978 null ] >> +endobj +408 0 obj +<< /D [ 49 0 R /XYZ 54 725.978 null ] >> +endobj +409 0 obj +<< /D [ 50 0 R /XYZ 54 725.978 null ] >> +endobj +410 0 obj +<< /D [ 58 0 R /XYZ 54 241.793 null ] >> +endobj +411 0 obj +<< /D [ 58 0 R /XYZ 54 271.681 null ] >> +endobj +412 0 obj +<< /D [ 58 0 R /XYZ 317.955 201.943 null ] >> +endobj +413 0 obj +<< /D [ 59 0 R /XYZ 54 630.336 null ] >> +endobj +414 0 obj +<< /D [ 57 0 R /XYZ 54 245.778 null ] >> +endobj +415 0 obj +<< /D [ 57 0 R /XYZ 57.706 574.545 null ] >> +endobj +416 0 obj +<< /D [ 57 0 R /XYZ 57.706 534.695 null ] >> +endobj +417 0 obj +<< /D [ 58 0 R /XYZ 54 421.121 null ] >> +endobj +418 0 obj +<< /D [ 57 0 R /XYZ 57.706 464.956 null ] >> +endobj +419 0 obj +<< /D [ 57 0 R /XYZ 57.706 435.068 null ] >> +endobj +420 0 obj +<< /D [ 57 0 R /XYZ 57.706 415.143 null ] >> +endobj +421 0 obj +<< /D [ 57 0 R /XYZ 57.706 335.442 null ] >> +endobj +422 0 obj +<< /D [ 57 0 R /XYZ 54 176.04 null ] >> +endobj +423 0 obj +<< /D [ 57 0 R /XYZ 54 146.152 null ] >> +endobj +424 0 obj +<< /D [ 58 0 R /XYZ 317.955 361.345 null ] >> +endobj +425 0 obj +<< /D [ 57 0 R /XYZ 317.955 680.149 null ] >> +endobj +426 0 obj +<< /D [ 57 0 R /XYZ 54 96.339 null ] >> +endobj +427 0 obj +<< /D [ 57 0 R /XYZ 317.955 640.299 null ] >> +endobj +428 0 obj +<< /D [ 57 0 R /XYZ 317.955 720 null ] >> +endobj +429 0 obj +<< /D [ 58 0 R /XYZ 317.955 630.336 null ] >> +endobj +430 0 obj +<< /D [ 58 0 R /XYZ 317.955 271.681 null ] >> +endobj +431 0 obj +<< /D [ 57 0 R /XYZ 57.706 484.882 null ] >> +endobj +432 0 obj +<< /D [ 57 0 R /XYZ 317.955 480.897 null ] >> +endobj +433 0 obj +<< /D [ 57 0 R /XYZ 317.955 520.747 null ] >> +endobj +434 0 obj +<< /D [ 58 0 R /XYZ 54 391.233 null ] >> +endobj +435 0 obj +<< /D [ 57 0 R /XYZ 317.955 441.046 null ] >> +endobj +436 0 obj +<< /D [ 58 0 R /XYZ 317.955 680.149 null ] >> +endobj +437 0 obj +<< /D [ 58 0 R /XYZ 54 520.747 null ] >> +endobj +438 0 obj +<< /D [ 58 0 R /XYZ 54 690.112 null ] >> +endobj +439 0 obj +<< /D [ 57 0 R /XYZ 317.955 411.158 null ] >> +endobj +440 0 obj +<< /D [ 57 0 R /XYZ 317.955 321.494 null ] >> +endobj +441 0 obj +<< /D [ 57 0 R /XYZ 317.955 281.644 null ] >> +endobj +442 0 obj +<< /D [ 57 0 R /XYZ 317.955 211.905 null ] >> +endobj +443 0 obj +<< /D [ 57 0 R /XYZ 54 275.666 null ] >> +endobj +444 0 obj +<< /D [ 57 0 R /XYZ 317.955 172.055 null ] >> +endobj +445 0 obj +<< /D [ 58 0 R /XYZ 54 620.374 null ] >> +endobj +446 0 obj +<< /D [ 58 0 R /XYZ 54 570.56 null ] >> +endobj +447 0 obj +<< /D [ 58 0 R /XYZ 54 660.224 null ] >> +endobj +448 0 obj +<< /D [ 58 0 R /XYZ 54 540.672 null ] >> +endobj +449 0 obj +<< /D [ 57 0 R /XYZ 57.706 504.807 null ] >> +endobj +450 0 obj +<< /D [ 57 0 R /XYZ 54 225.853 null ] >> +endobj +451 0 obj +<< /D [ 58 0 R /XYZ 54 351.382 null ] >> +endobj +452 0 obj +<< /D [ 57 0 R /XYZ 317.955 560.598 null ] >> +endobj +453 0 obj +<< /D [ 57 0 R /XYZ 317.955 600.448 null ] >> +endobj +454 0 obj +<< /D [ 58 0 R /XYZ 54 301.569 null ] >> +endobj +455 0 obj +<< /D [ 58 0 R /XYZ 54 321.494 null ] >> +endobj +456 0 obj +<< /D [ 58 0 R /XYZ 317.955 172.055 null ] >> +endobj +457 0 obj +<< /D [ 58 0 R /XYZ 54 201.943 null ] >> +endobj +458 0 obj +<< /D [ 57 0 R /XYZ 317.955 251.756 null ] >> +endobj +459 0 obj +<< /D [ 57 0 R /XYZ 317.955 371.308 null ] >> +endobj +460 0 obj +<< /D [ 58 0 R /XYZ 54 172.055 null ] >> +endobj +461 0 obj +<< /D [ 58 0 R /XYZ 54 132.204 null ] >> +endobj +462 0 obj +<< /D [ 58 0 R /XYZ 317.955 590.486 null ] >> +endobj +463 0 obj +<< /D [ 58 0 R /XYZ 317.955 560.598 null ] >> +endobj +464 0 obj +<< /D [ 58 0 R /XYZ 317.955 510.785 null ] >> +endobj +465 0 obj +<< /D [ 58 0 R /XYZ 317.955 480.897 null ] >> +endobj +466 0 obj +<< /D [ 58 0 R /XYZ 317.955 441.046 null ] >> +endobj +467 0 obj +<< /D [ 58 0 R /XYZ 317.955 401.196 null ] >> +endobj +468 0 obj +<< /D [ 58 0 R /XYZ 54 480.897 null ] >> +endobj +469 0 obj +<< /D [ 58 0 R /XYZ 317.955 720 null ] >> +endobj +470 0 obj +<< /D [ 57 0 R /XYZ 57.706 375.293 null ] >> +endobj +471 0 obj +<< /D [ 58 0 R /XYZ 317.955 321.494 null ] >> +endobj +472 0 obj +<< /D [ 57 0 R /XYZ 54 305.554 null ] >> +endobj +473 0 obj +<< /D [ 59 0 R /XYZ 54 720 null ] >> +endobj +474 0 obj +<< /D [ 59 0 R /XYZ 54 670.187 null ] >> +endobj +475 0 obj +<< /D [ 58 0 R /XYZ 317.955 142.167 null ] >> +endobj +476 0 obj +<< /D [ 57 0 R /XYZ 317.955 102.316 null ] >> +endobj +477 0 obj +<< /D [ 59 0 R /XYZ 54 600.448 null ] >> +endobj +478 0 obj +<< /D [ 59 0 R /XYZ 54 560.598 null ] >> +endobj +479 0 obj +<< /D [ 59 0 R /XYZ 54 530.71 null ] >> +endobj +480 0 obj +<< /D [ 59 0 R /XYZ 317.955 720 null ] >> +endobj +481 0 obj +<< /D [ 59 0 R /XYZ 317.955 690.112 null ] >> +endobj +482 0 obj +<< /D [ 59 0 R /XYZ 317.955 620.374 null ] >> +endobj +483 0 obj +<< /D [ 59 0 R /XYZ 317.955 660.224 null ] >> +endobj +484 0 obj +<< /D [ 59 0 R /XYZ 317.955 580.523 null ] >> +endobj +485 0 obj +<< /D [ 59 0 R /XYZ 317.955 550.635 null ] >> +endobj +486 0 obj +<< /D [ 59 0 R /XYZ 317.955 510.785 null ] >> +endobj +487 0 obj +<< /D [ 50 0 R /XYZ 367.621 477.429 null ] >> +endobj +488 0 obj +<< /D [ 54 0 R /XYZ 54 725.978 null ] >> +endobj +489 0 obj +<< /D [ 54 0 R /XYZ 54 532.376 null ] >> +endobj +490 0 obj +<< /D [ 54 0 R /XYZ 317.955 725.978 null ] >> +endobj +491 0 obj +<< /D [ 55 0 R /XYZ 54 725.978 null ] >> +endobj +492 0 obj +<< /D [ 55 0 R /XYZ 317.955 725.978 null ] >> +endobj +493 0 obj +<< /D [ 55 0 R /XYZ 317.955 424.975 null ] >> +endobj +494 0 obj +<< /D [ 47 0 R /XYZ 317.955 725.978 null ] >> +endobj +495 0 obj +<< /D [ 48 0 R /XYZ 54 725.978 null ] >> +endobj +496 0 obj +<< /D [ 50 0 R /XYZ 317.955 725.978 null ] >> +endobj +497 0 obj +<< /D [ 52 0 R /XYZ 54 725.978 null ] >> +endobj +498 0 obj +<< /D [ 52 0 R /XYZ 54 400.904 null ] >> +endobj +499 0 obj +<< /D [ 9 0 R /XYZ 53 747.899 null ] >> +endobj +500 0 obj +<< /D [ 54 0 R /XYZ 53 747.899 null ] >> +endobj +501 0 obj +<< /D [ 55 0 R /XYZ 53 747.899 null ] >> +endobj +502 0 obj +<< /D [ 56 0 R /XYZ 53 747.899 null ] >> +endobj +503 0 obj +<< /D [ 57 0 R /XYZ 53 747.899 null ] >> +endobj +504 0 obj +<< /D [ 58 0 R /XYZ 53 747.899 null ] >> +endobj +505 0 obj +<< /D [ 59 0 R /XYZ 53 747.899 null ] >> +endobj +506 0 obj +<< /D [ 46 0 R /XYZ 53 747.899 null ] >> +endobj +507 0 obj +<< /D [ 47 0 R /XYZ 53 747.899 null ] >> +endobj +508 0 obj +<< /D [ 48 0 R /XYZ 53 747.899 null ] >> +endobj +509 0 obj +<< /D [ 49 0 R /XYZ 53 747.899 null ] >> +endobj +510 0 obj +<< /D [ 50 0 R /XYZ 53 747.899 null ] >> +endobj +511 0 obj +<< /D [ 51 0 R /XYZ 53 747.899 null ] >> +endobj +512 0 obj +<< /D [ 52 0 R /XYZ 53 747.899 null ] >> +endobj +513 0 obj +<< /D [ 53 0 R /XYZ 53 747.899 null ] >> +endobj +514 0 obj +<< /D [ 9 0 R /XYZ 54 638.463 null ] >> +endobj +515 0 obj +<< /D [ 57 0 R /XYZ 54 591.482 null ] >> +endobj +516 0 obj +<< /D [ 57 0 R /XYZ 54 574.378 null ] >> +endobj +517 0 obj +<< /D [ 9 0 R /XYZ 54 642.448 null ] >> +endobj +518 0 obj +<< /D [ 50 0 R /XYZ 54 703.345 null ] >> +endobj +519 0 obj +<< /D [ 9 0 R /XYZ 54 351.948 null ] >> +endobj +520 0 obj +<< /D [ 46 0 R /XYZ 54 351.752 null ] >> +endobj +521 0 obj +<< /D [ 47 0 R /XYZ 317.955 154.76 null ] >> +endobj +522 0 obj +<< /D [ 48 0 R /XYZ 317.955 427.975 null ] >> +endobj +523 0 obj +<< /D [ 50 0 R /XYZ 54 401.888 null ] >> +endobj +524 0 obj +<< /D [ 51 0 R /XYZ 54 410.706 null ] >> +endobj +525 0 obj +<< /D [ 55 0 R /XYZ 317.955 222.506 null ] >> +endobj +526 0 obj +<< /D [ 56 0 R /XYZ 54 275.308 null ] >> +endobj +527 0 obj +<< /D [ 56 0 R /XYZ 317.955 162.73 null ] >> +endobj +528 0 obj +<< /D [ 46 0 R /XYZ 54 277.761 null ] >> +endobj +529 0 obj +<< /D [ 46 0 R /XYZ 317.955 268.334 null ] >> +endobj +530 0 obj +<< /D [ 47 0 R /XYZ 54 320.139 null ] >> +endobj +531 0 obj +<< /D [ 48 0 R /XYZ 317.955 352.02 null ] >> +endobj +532 0 obj +<< /D [ 49 0 R /XYZ 317.955 591.123 null ] >> +endobj +533 0 obj +<< /D [ 51 0 R /XYZ 54 252.261 null ] >> +endobj +534 0 obj +<< /D [ 53 0 R /XYZ 54 720 null ] >> +endobj +535 0 obj +<< /D [ 53 0 R /XYZ 317.955 515.407 null ] >> +endobj +536 0 obj +<< /D [ 53 0 R /XYZ 317.955 179.029 null ] >> +endobj +537 0 obj +<< /D [ 56 0 R /XYZ 54 208.558 null ] >> +endobj +538 0 obj +<< /D [ 56 0 R /XYZ 317.955 651.563 null ] >> +endobj +539 0 obj +<< /D [ 56 0 R /XYZ 317.955 475.137 null ] >> +endobj +540 0 obj +<< /D [ 51 0 R /XYZ 54 228.045 null ] >> +endobj +541 0 obj +<< /Differences [ 27 /f_i /f_f_i /f_f /f_l 40 /parenleft /parenright 45 /hyphen /period 48 /zero /one /two /three /four /five /six /seven /eight /nine /colon 65 /A /B /C /D /E /F /G /H /I 76 /L /M /N /O /P /Q /R /S /T /U /V /W 91 /bracketleft 93 /bracketright 97 /a /b /c /d /e /f /g /h /i 107 /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z ] /Type /Encoding >> +endobj +542 0 obj +<< /Ascent 714 /CapHeight 628 /CharSet (/A/B/C/D/E/F/G/H/I/L/M/N/O/P/Q/R/S/T/U/V/W/a/b/bracketleft/bracketright/c/colon/d/e/eight/f/f_f/f_f_i/f_i/f_l/five/four/g/h/hyphen/i/k/l/m/n/nine/o/one/p/parenleft/parenright/period/q/r/s/seven/six/t/three/two/u/v/w/x/y/z/zero) /Descent -231 /Flags 4 /FontBBox [ -1082 -328 6171 1014 ] /FontFile 597 0 R /FontName /SVARXO+LinLibertineTB /ItalicAngle 0 /StemV 130 /Type /FontDescriptor /XHeight 433 >> +endobj +543 0 obj +<< /Filter /FlateDecode /Length 874 >> +stream +xΪ•UMoΫ:ΌλW°‡ιA5iE_…a€”, @ΏΠEoΆD§z°%W–ωχεμ“βαΪƒαjvwv(‘7oΎ<ΔΆw>Nήiυ՟ΗΛΤϊΈϊΈ=E77υΨ^Ž~˜?yίωξϊτό^}™ΖφΑΟκΆΊ―ο‡~~ΘχC{ΈtώΚϊ’σOύπJAuϋθΏΗϊαCΏσΣάώΡΕσaΟ&ŽwΫ³»yψgψ‘ηώŒ5ͺ<φσ!d]’ +lυ‡lEmΎωι܏Γ{eήi­C`3tΥx„#ηh!S©ΕuΞ}?t“Œ¦v42KΥυν,+ϊoΑZ$?<ŸgΌφc΄Z©ΕΧππEΒ=Š;Ζ`ςΣΠΌEΖυ‹œ1q +ž +™ϊμζ>Ž1ήΡ’b OŠš1Υ§YMŠ}/ΖΠY²ώ}K֟‚_²~zOJ֟BOΙϊ3Κeύf/YN|֟ŸuζΠ\²ΞϋX²Ξ„rYgBΉ¬ΣΠΗΓ>[ΜbΕgΜhΕηX|&ŽψŒ^V|Ζ»gΕgτ΅β3ό΄β3qΔgΜnΕgθ·β3tZρ³[ρΎYρ™κ‹ΟΠoΕgθtβ3ϊ:ρ|'>ƒοΔgθqβ3εŠΟ˜έ‰ΟΔŸ‰_πΧOXΞΜβΔΜβΔΌ‡Nό§šό]XͺΙί„£:β?85χJαC#ρ†9u#§28~q½ΌωνešΒm@wε8ΕΓΙrMΖ²θGχΫυnΖκsύ’²6 +endstream +endobj +544 0 obj +[ 641 947 716 680 631 0 266 376 514 514 637 729 253 315 315 433 537 244 358 244 316 514 514 514 514 514 514 514 514 514 514 256 256 512 551 512 430 988 740 654 706 734 609 545 732 817 367 373 736 577 899 740 730 614 730 716 504 652 732 700 1028 718 624 624 397 307 397 518 486 253 506 542 456 561 489 391 521 619 322 312 613 325 905 616 551 581 573 428 427 358 598 529 777 561 558 452 ] +endobj +545 0 obj +<< /Differences [ 16 /quotedblleft /quotedblright 21 /endash /emdash 27 /f_i /f_f_i /f_f /f_l /f_f_l 34 /quotedbl /numbersign 37 /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less 62 /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft 93 /bracketright 95 /underscore 97 /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z 159 /section 225 /aacute 231 /ccedilla 233 /eacute 252 /udieresis ] /Type /Encoding >> +endobj +546 0 obj +<< /Ascent 0 /CapHeight 0 /CharSet (/A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/aacute/ampersand/asterisk/at/b/bracketleft/bracketright/c/ccedilla/colon/comma/d/dagger/e/eacute/eight/emdash/endash/f/f_f/f_f_i/f_f_l/f_i/f_l/five/four/g/greater/h/hyphen/i/j/k/l/less/m/n/nine/numbersign/o/one/p/parenleft/parenright/percent/period/plus/q/question/quotedbl/quotedblleft/quotedblright/quoteright/r/s/section/semicolon/seven/six/slash/t/three/two/u/udieresis/underscore/v/w/x/y/z/zero) /Descent 0 /Flags 4 /FontBBox [ -1082 -257 6171 1125 ] /FontFile 598 0 R /FontName /DHRMAA+LinLibertineT /ItalicAngle 0 /StemV 79 /Type /FontDescriptor /XHeight 429 >> +endobj +547 0 obj +<< /Filter /FlateDecode /Length 877 >> +stream +xΪ•UMΣ0½ηW˜ΓJΛ!ΤNš/TUr’FZ Δ7Τ&ξ©Mͺ4=μΏΗofΊ ˆZ=OήΜΌyqμ›7ŸBۍ;Ζο΄ϊβΞγej]X}ܞ‚››zl/G7ΜχΞu»>=ΏWŸ§±}p³Ί­ξκ»‘ŸίzςέΠ.»²ώN*έS?ΌRΠGέ>Ίoα‡~ψΠοά4χƒ{ ηΓ>œMξΆgvσπ=KŸNΫ$Τ(ςΨΟŸό_yΚ“ΥΏ‘5ωκ¦s?ο•y§΅φΝΠUγvœƒ…Œ€Χ!χύΠM2—ΪaΚΐDͺλΫYVτί½―H~x>Οξx7μΗ`΅R‹/ώαyžži¦·ΑβΣΤΉ©žΤνΏIφ)—Σιΰ Oι`½VΫϋNήίϋνΡ©Εyυ’ϋψ|r*’΅α9Ϊ±sηΣΆuΣvxrΑJλ΅Z5Ν:pCχΗ3sΚnεn‚‘Qe‚Q€Q˜ Œ +]jαq°ͺΑ¨9₯cCCh +lΐhP΄α”E€4Τp 1ώuΞ"ΊΞέώΨNb‘‡ΒΪ.—ΐ\GU +a‚^Η’N8^§Œ-pΖΉpΞqβœΫ[ŽΓi]r_βT/kφ5MΜΉˆΦPf/cψ`ΈfŒΎ†k¦©ΡΫ¬^Χ/±ϊ—Φ›Χ5½πζχΔ"ψ-#zλC­T3Ζ ™μψ’“ΏΖΫι±a σˆ1ϊζ1cΤΟ—Œ7ΐδ―‘ωσ”1κηcβδΓO+>G|ΖμV|†~+>C§Ÿ1»Ÿα›Ÿ©Ύψ ύV|†ΞR|FίR|ΏŸΑ/Εgθ)ΕgΚŸ1{)>_|&~Ξ§a90K)ώc–RόΗ>,ΕͺΙί‰₯šό”TGό§ζ^ |h#ή0§nδ’SΗ1.›— ½L“ΏθF’³§ΊΏ^.­ΣxBύθΆ»^ΣX}j‚ŸΘ­ +endstream +endobj +548 0 obj +[ 375 375 375 543 543 548 742 0 271 271 272 560 829 582 540 815 250 288 336 465 465 637 705 268 298 298 369 550 220 338 220 323 465 465 465 465 465 465 465 465 465 465 236 236 550 550 550 435 895 695 588 646 701 557 485 685 730 297 322 637 528 839 699 702 541 702 587 485 597 661 652 951 660 575 604 356 287 356 518 486 268 457 493 428 506 447 310 500 538 271 272 512 264 790 542 504 519 503 372 390 316 531 497 747 490 515 424 277 205 277 449 338 695 695 646 646 701 557 557 685 528 528 528 699 699 725 702 587 587 485 485 485 597 597 661 661 575 604 604 604 598 297 506 473 457 457 428 428 528 447 447 500 264 290 264 542 542 528 504 372 372 390 390 390 374 316 531 531 515 424 424 424 542 288 435 465 695 695 695 695 695 695 865 646 557 557 557 557 297 297 297 297 701 699 702 702 702 702 702 869 702 661 661 661 667 575 527 0 457 457 457 457 457 457 687 428 447 447 447 447 271 271 271 271 486 542 504 504 504 504 504 778 504 531 531 531 531 ] +endobj +549 0 obj +<< /Ascent 707 /CapHeight 707 /CharSet (/S/arrowleft/asteriskmath/bar/braceleft/braceright/bracketleft/bracketright/bullet/dagger/element/emptyset/existential/greaterequal/intersection/lessequal/multiply/parenleft/parenright/periodcentered/prime/propersubset/radical/slash/union/universal) /Descent -152 /Flags 4 /FontBBox [ -47 -944 2835 838 ] /FontFile 599 0 R /FontName /KLFPWG+txsys /ItalicAngle 0 /StemV 52 /Type /FontDescriptor /XHeight 400 >> +endobj +550 0 obj +<< /Filter /FlateDecode /Length 980 >> +stream +xΪmVMo£H½σ+ΨC€ΜΑγώ€nY–0R;3šD«½:ΠΙ"Ε`–6~ϋUαΰYελρ¨WΥUtξώψωΈqνπμ7ϊ«ˆωiΈŒί”ΟΡέ]54—“οηοή·Ύ½ΎΎΕ?Η‘yτs|_>T}7 Ζ}σviύΥκs£ΒΏvύj‚<ρύ“{3;½O›ηKχ6wύFΐφ©›ί‚Νg―γΐΕΏq1ΉόεΗ©ϊo±ό*„Δ‘oΛᄦh»θˆ·We/]ߎ‹˜ψ"©βΆkζε‰~›Shœߧٟϊ—!Ϊνβν―πršΗwRψ%Ϊώ[?vύk|›²πζρr>Ώy¨ˆE΄ίΗ­ Cνߏ'o?+πΓδιύμcEΟ’U5Cλ§σ±ργ±υΡNˆ}Ό«λ}δϋφο”e—η—«mlE~”ΚΣ}΄“*`©‰Θ ˆDJ„I@„s5’`ρdΕ.ΔDM„‘("+™k”ΐ0aAP<&+XhΔΠΓAG"N‘[ˆ€£] +‹”-b4ΒHv0ƒβ εΦ: $˜”‰ ϊarφΒB“΅ά d±xi‘V %‘!KΖ +ͺ—Œ]4fšQP'ΘuεruΙ!,Χ«KayΊΊp/ΈZa%‚–‹RC‡}=Υά\OΉωη8.‘”D`!)DώEΈ"ΜΙ%α„ψš0V’γΒ°/Ϊ Έ>‰£tΤ’ͺ€q*…’¨ΉK4¬!δΚ βȌ‹.sΆAyJπ, ΓJ2F₯ΓW±†„(γQ!ϋœ1Ω;Ζd_±/΄©γ˜gFB›¦Ό‚A³Ξ0½S^EC’ΉWt.š†8-hDSΖ‡z₯5Ω“NιWs½:5Χ+Θ—Φ‘šwI`5―€$žυKΤ’p4lRΆW˜θ”wY‘”W0ƒ~Γgg‘ΩXΖΠi8―E|³ΔALΓyι›`8oEψ@φ)ΩsίΤb—ƒK}s»τ ³dυ:ρ6α•Β\Yž1ϊ،ϋ@φΟϊfI1dS1&ώΐΪl͘Άρξ@CFu9 ϋά¬|noψε\ˆηs‘>η7g‘—7ΈβU${ξ εΝk֌Ίz"T‰ψŽφNUθ›γ½£§ΣΜcNœaŒ:ΛΎθƒγ=’=-Δ ΎΩεBέ`½ξ`‘¬;X€7Ψά`»Ξ|Αύ§3*Έ)α’1f¦΄τ}§s/©ΟͺΐΌ•άημΛrΑ‘ιςu’―.%\ Χ]sΗp-K7ξΆχρy8Γ‹ώθΏώΏ€§uτxΏu +endstream +endobj +551 0 obj +[ 250 636 471 636 512 636 636 636 636 636 636 636 862 497 497 636 636 636 636 636 636 636 636 636 636 636 636 918 918 636 636 1024 1024 499 499 1024 1024 1024 636 1024 1024 550 550 1034 1014 1024 636 347 853 536 536 634 634 0 0 587 587 640 500 908 703 712 712 639 870 718 648 860 622 669 704 876 650 775 796 749 1080 822 703 709 703 680 679 678 897 892 1202 738 797 865 654 654 654 654 654 466 466 371 371 371 371 411 411 363 363 200 400 499 550 460 264 703 727 664 409 654 654 636 636 500 500 500 453 578 531 605 542 387 642 908 443 677 957 443 443 513 1173 456 456 376 633 889 418 663 928 418 418 472 1134 424 440 727 670 587 500 697 460 333 333 333 333 333 333 333 370 333 333 548 454 454 454 454 454 454 280 175 334 389 421 275 350 425 334 564 333 333 333 333 ] +endobj +552 0 obj +<< /Ascent 437 /CapHeight 437 /CharSet (/comma/period/u1D434/u1D436/u1D437/u1D438/u1D43A/u1D440/u1D441/u1D444/u1D445/u1D449/u1D44A/u1D44B/u1D44E/u1D450/u1D451/u1D452/u1D453/u1D456/u1D458/u1D459/u1D45A/u1D45B/u1D45C/u1D45D/u1D45E/u1D45F/u1D460/u1D461/u1D462/u1D463/u1D465) /Descent -11 /Flags 4 /FontBBox [ -342 -238 1011 786 ] /FontFile 600 0 R /FontName /PEYUND+LibertineMathMI /ItalicAngle -12 /StemV 82 /Type /FontDescriptor /XHeight 400 >> +endobj +553 0 obj +<< /Filter /FlateDecode /Length 592 >> +stream +xΪ}TΛnβ@Όϋ+fHδ@˜ρΫBΒ/ iI’€V{5φ@,mωqΰο·kŒmrΐͺ©žξ.ӞόzίΝΦyu3λ™³ΩV}“ΙYΈMkc2‰ͺ¬ΏΘ²{•2—ωxΪΎ°χ¦Κv²cΣpmΚ’{"ρ¦ΜΞ}.GΥE<₯– ›ξείΩοβ ›(ε6ν>·›Ω‘/ΞΞ8²φEw&υΟBF§μ›S¦ω#›Ά¨Κ&ž9ηDΔeVLΨσ[—l>φ},ΚΌΉ΅Κhά&Λ‹¬»Eκ™]Θ*$οm'/›ςXΛ%›ΠaΫ5WΥυ“1krΩε‰MΏι‘4»ΎΟύ0n¬V,—GΊššYΎu'Θ―ΡӍΚ>Σζζ)灲Sqα£:7ΰ`pm 8ŽV|#Η ‹Ηΐ¦Β} +gπχΫz°5μ@Η0ΟuŒZv€γq¬}ˆ«ΧδιzΧΦ³ΉC\ρ‹μ>hΌι7γ!Ž΅˜έσuo¨ν«ZάΔόΎ3ΰπφ”λψbξ;’υMCλ£–T-Φ€ΆεΎΗuU#KύΤ`ό!zKŒΝaL +endstream +endobj +554 0 obj +[ 667 557 616 667 526 457 664 673 280 315 637 519 804 666 668 499 668 555 454 544 634 597 858 628 552 578 486 478 389 489 401 314 499 519 276 259 486 266 783 518 447 489 491 357 353 307 521 391 688 475 503 436 636 668 621 648 691 594 530 670 687 693 478 522 472 458 450 369 402 490 447 258 506 494 451 440 391 525 482 410 521 410 455 546 469 584 630 424 421 507 507 537 454 642 494 539 519 494 494 276 259 381 631 470 511 540 338 462 448 481 425 473 438 469 469 516 465 465 465 465 465 465 465 465 465 465 220 220 ] +endobj +555 0 obj +<< /Differences [ 37 /percent 48 /zero /one /two /three /four /five /six /seven /eight /nine /colon 97 /a 103 /g 105 /i 109 /m /n 114 /r 120 /x ] /Type /Encoding >> +endobj +556 0 obj +<< /Filter /FlateDecode /Length 732 >> +stream +xΪ•UΛnΪ@έϋ+¦‹HΙΒaψ!$°ABJ“( ͺ» +μ!΅62FJώΎsξ΅‘T]$ ¬sϜϋϋrσνeιOŠzc}s/Ε«=Φ§&·~ϊ}}πnn²:?νmΥ>Y[Ψ’?=>ˆ—¦Ξ—Ά·ι"[Te{ηΔ‹*ߝ +Ϋ«ώ/šΪ·²ΊHGάμO±¬ˍmΪ²²+ΏέmύΊUΎΏY­ΏΫ΄Υ―ΪΎΏ‡[_"ΚͺlwΞϋkŽΒ©Ε'Υ‚ό°Ν±¬«‘ξ₯”Ž˜UEZο1‘£7θΊƒΎΟmYMךؠQOiQ”yΫYτΜχn΄p^~[»_TΫڍΔΰΥΫζƒΊΊσΟMa›²z·Ÿ¬Ωω,O‡ΓΞ’>!½ρXvλRΉ?­χV Ύ6³σκγ`…&[q'y]ΨγaΫf]½Yo$εXŒζσ±g«βŸ³ˆ=6Ϋ^9©Œρ0“`μ4|΅!v„2a@@m¦LĎ0s‡&φF!ά£ {£‡QŠ@Rͺ«― Τ}EωοuΣΥ.M‚Lq΄–!°f ₯a<2žάΚ–!cŠ“0&ί υ§SQ.eP’’–΅‰€5—Ž8ŠrΙι˜rι”4”Kgˆ©x:τ"ζ'ΐ1ϋ’†jix43ͺζ¦Τ©Rβη€Ο§€™±FΟ94ZσD3ΰˆq‚ α8τe8ށΖΜΈΤt·‘³&€&˜qΘΜΉΔ %λ‘7ʘ‡>b}†8ΧFsŽ%Ο0t?ψΗκbŸ9ύΫ\lΔ‹‡Χ>ΔσˆΓ‹My£‹;ˆγ‹χ%NΞv„;ι.Τ³ŠωކύYzŸβgΧε˜]s”g~ΕQΌ„g(gγ[Kx&Α°‹π}τ™˜ξ{κΎ©dxΆ5jMxZυη!ω{=ιzέ—F_vvέy䧦q«‰"-¬·†Ξ;σPΰE?ZΆύ¬ηΉχηλΉr +endstream +endobj +557 0 obj +[ 637 705 268 298 298 369 550 220 338 220 323 465 465 465 465 465 465 465 465 465 465 236 236 288 550 435 435 895 695 588 646 701 557 485 685 730 297 322 637 528 839 699 702 541 702 587 485 597 661 652 951 660 575 604 356 375 356 349 291 268 457 493 428 506 447 310 500 538 271 272 512 264 790 542 504 519 503 372 390 316 531 497 747 490 ] +endobj +558 0 obj +<< /Differences [ 38 /ampersand /quoteright /parenleft /parenright 45 /hyphen /period 48 /zero /one /two /three /four /five /six /seven /eight /nine /colon 63 /question 65 /A /B /C /D /E /F /G /H /I 75 /K /L /M /N /O /P 82 /R /S /T /U /V /W /X /Y 97 /a /b /c /d /e /f /g /h /i 107 /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z ] /Type /Encoding >> +endobj +559 0 obj +<< /Ascent 0 /CapHeight 0 /CharSet (/A/B/C/D/E/F/G/H/I/K/L/M/N/O/P/R/S/T/U/V/W/X/Y/a/ampersand/b/c/colon/d/e/eight/f/five/four/g/h/hyphen/i/k/l/m/n/nine/o/one/p/parenleft/parenright/period/q/question/quoteright/r/registered/s/seven/six/t/three/two/u/v/w/x/y/z/zero) /Descent 0 /Flags 4 /FontBBox [ -634 -312 6171 893 ] /FontFile 601 0 R /FontName /TCFTCN+LinLibertineTI /ItalicAngle -12 /StemV 76 /Type /FontDescriptor /XHeight 429 >> +endobj +560 0 obj +<< /Filter /FlateDecode /Length 876 >> +stream +xΪ•UM‹ΫH½λWτ&ΕέΦθ+CKrƒ!›„ΜφΆΨR{V¬-Yf™ΏύͺΚ3Ω%‡δ`σT~Uυή“άΊϋνΛcl»qογδ½V_ύeΌN­λίwηθξΫλΙσ'ο;ίέ~½|P_¦±}τ³Ί―·ΝvθηwΌΪγ΅σ7֏I•ξ‡7 +φ¨ϋ'Gό±>φ{?ΝύΰŸΆρ|<Δ³‰γύξβγγ~ώ̟§λίΔSžϊωΊ­QΆϊIΆ’5ίόtιΗαƒ2ο΅Φ‘°Ίz₯£υZuώV…Œ?νN^-~-Χζ§—³WKΊ6μ€;9οZ?ν†g­΄^«•sλΘέ~3 ·μ7ξ&p΅ _K”λheΠl–T05 +) +’PX`΄8Š€SΗ…"20rš¬30J0Κ“ƒQcKΓ3ŽV  ·4`lΘ„¦Β ‡‘Ž[†:΄Έš hq +<#`ΨΏω,ΝΝwϋΧn’ˆ‚8 Φ†pυ γzYgΐ α!θβX¬Ρ)ΧΰŒ±ΞΉ7.ΈNό’{°ε:’Φο%NΝυ +Έα¬1Σ$ά‹Ίa aΞ2A†g&Ψkxf–…έΝϊνϊ΅Φ|WΓυζνšnΈϋoj˜5‘?ӌ‘;—»Ž, +ΚΤ„6Œ‘i±dŒ]EΒ3‹Ζ`ΚԐη"cŒωEΘ8{††BόΣ^ςoEQ1ΖsZԌ‘KΡ0¦ωδΧ€Έχ…c %λO±·dύ)ψ%λ§g₯dύ)τ”¬?£^֟Α{Ιϊsβ³ώŒψ¬3‡ζ’uζΈ—%λL¨—u&ΤΛ: ύ8g /Vr†G+9§ΐ’3q$gμ²’3ž?+9c―•œ‘§•œ‰#9Γ»•œ‘ίJΞΠi%gx·’3r³’3Ν—œ‘ίJΞΠYIΞΨ[IΞΰW’3ψ•δ =•δL½’3ΌW’3ρ%gβ|–3^*Ι^*ΙΟa%ωΣLώoXšΙ‹ŠζHώΰ4Ό+EN0κŽ9““‡NΑxΕΌϋνušΒήCtžγ$§λ«κ<žΡEzΗέήΟΈϊμ’㏠+endstream +endobj +561 0 obj +[ 705 268 306 313 433 527 219 333 219 291 444 444 444 444 444 444 444 444 444 444 219 219 487 527 487 405 829 667 557 616 667 526 457 664 673 280 414 637 519 804 666 668 499 668 555 454 544 634 597 909 628 552 578 334 321 334 518 486 268 486 478 389 489 401 314 477 519 276 259 486 266 783 518 447 489 491 357 353 307 521 472 688 475 503 436 ] +endobj +562 0 obj +<< /Differences [ 132 /dagger ] /Type /Encoding >> +endobj +563 0 obj +<< /Filter /FlateDecode /Length 689 >> +stream +xΪ•TΑn£0½σήC₯φ@cLB‘Š"αR€n[5Ρjo+N)1Cώ~ύfHΫ]ν‘=„<ί›y3`_}{^ϋiέnήJρbϊφΤUΖ_~/ήΥUΦV§ƒ±Γ£1΅©/»ύ½xξΪjmq½\e+Ϋ 7ŽΌ²ΥώT› λ$m^ϋNAq½1?ύ‡Ζ>4[Σ 5Ψοό‘|[φΖίoϋΛ–m{|‰,›fΨ;υΧ„Β±Ε'Ω‚Κό0]ί΄φ^·RJΘm½l˜HοMΖΔδη±u7Ά&ΆhΤ ”¨›jWτ¬n΄―Ού`+»k½ω\L^άf?tgκκΖ›> +endobj +566 0 obj +<< /A 602 0 R /Next 603 0 R /Parent 99 0 R /Title 604 0 R >> +endobj +567 0 obj +<< /A 605 0 R /Parent 99 0 R /Prev 603 0 R /Title 606 0 R >> +endobj +568 0 obj +<< /A 607 0 R /Next 608 0 R /Parent 6 0 R /Prev 99 0 R /Title 609 0 R >> +endobj +569 0 obj + +endobj +570 0 obj +<< /D (section.8) /S /GoTo >> +endobj +571 0 obj +<< /A 610 0 R /Next 611 0 R /Parent 102 0 R /Title 612 0 R >> +endobj +572 0 obj +<< /A 613 0 R /Parent 102 0 R /Prev 611 0 R /Title 614 0 R >> +endobj +573 0 obj +<< /A 615 0 R /Next 102 0 R /Parent 6 0 R /Prev 616 0 R /Title 617 0 R >> +endobj +574 0 obj + +endobj +575 0 obj +<< /BaseFont /UZJQXT+txmiaX /FirstChar 56 /FontDescriptor 618 0 R /LastChar 61 /Subtype /Type1 /ToUnicode 619 0 R /Type /Font /Widths 620 0 R >> +endobj +576 0 obj +<< /BaseFont /FBLZCM+LibertineMathMI7 /FirstChar 25 /FontDescriptor 621 0 R /LastChar 71 /Subtype /Type1 /ToUnicode 622 0 R /Type /Font /Widths 623 0 R >> +endobj +577 0 obj +<< /BaseFont /JESRHI+txsyb /FirstChar 82 /FontDescriptor 624 0 R /LastChar 82 /Subtype /Type1 /ToUnicode 625 0 R /Type /Font /Widths 626 0 R >> +endobj +578 0 obj +<< /BaseFont /THUYEG+txexs /FirstChar 205 /FontDescriptor 627 0 R /LastChar 213 /Subtype /Type1 /ToUnicode 628 0 R /Type /Font /Widths 629 0 R >> +endobj +579 0 obj +<< /BaseFont /IIOJER+NewTXMI /FirstChar 157 /FontDescriptor 630 0 R /LastChar 161 /Subtype /Type1 /ToUnicode 631 0 R /Type /Font /Widths 632 0 R >> +endobj +580 0 obj +<< /BBox [ 0 0 515 294 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/bg/anns.pdf) /PTEX.InfoDict 633 0 R /PTEX.PageNumber 1 /Resources << /ColorSpace << /Cs1 634 0 R >> /Font << /TT2 635 0 R /TT4 636 0 R >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << /Im1 637 0 R >> >> /Subtype /Form /Type /XObject /Length 3205 >> +stream +xݚ]oI†οηW4 ,Ξ‚Ηύέ=·.i%B,νΛΕκΘ+‚lBœŸ§ϊsŽ?’corC¬ΥΞΤι©κy»κ­κy§^©wJ+½½ρgU0~εΚ(»Ή5s₯n―ΤχκŸκβε{£ο•)οπ‡Ή!K~M>Έ€¬g”Ο›ΚHXDT6δ5ŽΫE ΝΑ ‘²AtQM-]2lρX—]ZΊ―W«΅ΖKΕΈε‘>™!nJΖ +–%\ςrΩh­ώ«²ϊΊϋεkρ’5!ζΘ•ΣGy<+Χ[V―_Κλ[™ŠN9ymΖίω±hΜ ;h9άΘλΛ6:»œͺ•±1ΈΌn\$ΆΥ–hΜ\”χ:§ΈΚ “Ρ8?°΄ϋ‘νΠGx—“«™‡0ƒα’·έ†IT•ί΅ξfy7―Α,P :κ.°«σΈΝhόfΥε­z·π~L–Ώ:jάΩΰVηRRη–ՁΎψύ₯ΪΜΊΙΏ2~Γλ[Τ‰㼌KΛ卺ΈΌ΄ ς'uφ‹κςꏗ2Ÿϋθe —iVͺL +k”­z›l19­[6‰Εσ^7Γ¨ ‰LΘϊ50~κ’ƒb#°³›2p–eKLM]2νρήΪ(|W/ˆšp…wqΪ[L—Θ+ι³κ²>σ©iJϊϊδu/ǟΏΓ‘Ν!Η/@ή@]ƒŽγ?T,Κλ- ;Β/ #fτέ ψ-’ΒρpΓgκκ°€ς²0ω9π+ΫωaθšmuΔ―Ξ ‘-n8ΊκΊ_=]φK ΌΊŽΈP―qf•¨;ήU,;Ο‰Ί‚š|T.ψ5•[m,QwŽ „”¨k†–!)†xfX/IΙ,C‹F ϋ£Ψa··[žSι²9έ¦e,`©+Ϊ#΅EάρΤi·9e‡T7γ,3A―ΰτSq–Žβ¬u-ξpŽέ໏²± ΄φϊϋ2’¬lŠιΣbμγušhΐh$‡< Ώ< dΈB\X3 Ab«λ2‰Ψ‘ΘΑ$0ΛVΝ«π›¨B‰k:.CrP±ΖΫ9N’ ›Іd˜;s(£λV¦0»"ƒ€ΤƒΩf,$D’>§!σξŠ–!Ω™›‘υsβ΅»¬»γcxma—’w‘Uβm wΓαΐVϋ}"Ά„ΨFHΪ}‹¬°’Y…άŒ‡?f}Z΅7°ΖsΛ LΒηΑφW'ΐΦHf’f\ £7\vΙZ{›Θ„L`ε^+j…ξbk“ΰ™=φ!p+„ ΪΠυ A7ΖcMΆΐ*!BPΘ©Θ aΫ sΚt Ου9uќuSD`νλ˜ζΎh§‘ζŽZ‰’ƒΞ–`‡£+…47ΪΙ€AhUB#΄BΔκ#5ΠRV–ΚπjSε±ςL ΅rQbπnh»ΰ+ΈΒ «ΑζS(νγαVή=„θRm~n}n}$€JΉdHΟR ·S–σκβζ”OŒJD³!ςνaL-C‚3΅§™WΣGΨf)5MΧ4%έΟ΅Q‹PΩ“Ωiκ€Ϋ›€„€;f5F΅ΥLMs-ΣήΣ°»œVŠMC}ρ­+  b5M³C„OE.Ε»”fŽ·Λ`ƒ; Άϋ‰ΫΞ.nΧΨ@”Σ?΅>‡Υb±Y)Œžά―O.ψΫΐ_/ΤoΤΠ¨ ΣνBͺ}J*@Nj–ΪqqL κΆR'τQ$gκ'Wš UӐ” !Yͺ<^jWC&εα0VΪEΒscJ}ԜvΧ4%ΝΨΣπzbλ`ZiΛξ±–L}Π>‘π"Uτœ~L½T{zΠΚJh›Αϊϋ!΄FS”‡@ΏΐΧΑ3ƒμo&VaJk6ΔIš +ZVμΩ­v»ΆUK·.I&Z’j•‘€£DMΛ…ΞΨΡ₯)] = .CBοz ‘έ(Ιόειͺ¦ήvK'½\—Ξ ₯AΆpβMšk™q,Bο`₯‰š¬LC(ΥήΠMΣ{Κ*šΔ6A˜Ό₯ζq £΅ΏΚRH‰ΒΊΛ‹χ$6 ++γm>yέχ”Ν₯§LO,ͺΝ{lsψl“ΒλJ#“³ +nήx–ZrLŸ·­ό“―ϋΎΆ±phS(Yr0mε³OEγΚyW1p͚vƒ‰4ρ_χρdξišΛ¦Ρ'P“FšgwU ΙnΌsZšv€θXςGΟ 7‹²Dώ’~³#₯Η’?2a ^»œ’QΊΜ¦%Ά&[˜ @8iUΣn»%ž¨ΏσK›y* κ—·ΣΝΠ/i:wc2]6'ά5MΙ4VςΗhΨ–ž{42•χu6>ώx4ςxιΨφΔj Λ!ΒB“`πι­ε^<%iL"q…ΐ!sΗkΚΦݟ‰Ι±Ξέ¨},i$Fˆ†θ[ΨXgA²O_΄Ό’ά±’ά…( vrF+§*W3H ΪDΞcΛw•ΝM ΓσœOΕ¨·r@*gX†ΈY{-(²ΨΆž<ΙδF(m¦ν”C―#/άΣ5Mg¨žρœmι+Υπα¨{JyΉœJyŽβΒ§GέGBΧ?άS6m§#M,ΘmHϊS‘ΛΣϊδΠ ΘιTδ“G8MF–³[Ž₯―$,ƒΰ.Œ”Θ…Λδ dJˆCϊε oŽκεώPԻÏu‰@Ž5θftEP[9 ‘άΝu ₯π˜T—ΫΌ§¦.ΩΩ;Ž`Π‘Α€ψΣw‡Ή±ήΑ„§.ϋŽμτnmBτ–ne½,·”ZΠ~άj4ή@=.Ŝœ)·{a½Νh“ΜŽ[ΥK§βgv[ΰΦn95zVόϊζ(|Eω—ψ°ΔρΊwν›ͺ-RϋΤΡΖΔΦ•-¨‘/$ώHϊ³²ΰ§ΐFNlαα–£‡kΪMτ΄—adάLΗ0dn“n/‡¨Ιj‘K†-5ψ6 ΄s‘Y«ΦώLj’›…‡ Ρ& ΘΡ‘œ[Σ¬“t’^Ύ¨π[­ψΫ€lƒQυ[†ΓάΉ­‘§΅Žσ=¨ΰFWO‡ΊΩ‘Yό όg#ˆqC]O‘„C5­!°τW<ΗC²¬ώΘŸήœ,ΎKΔ f"2ŽαηM…ŠΠωΝ₯۝€ϋx#_‰ηi²π /’"Άy›¨B(’c‚–ς…Šη0(ΘΡ;5nL<Κ8ښυ̝ΧΖι―δNψ #n‹ΌšlΚμ λγgž•2k£υΪ^Γ»ξ»6/t±ϊΞκD‘/’Δu|ΧG[ •@ +―ΕwΩΑ³YΌWΚΊΔ +› ?uYρ¬Žœ‚ Gε­ΓΔ™U}!šY8ι”Ν(°kž7Υ³9NΚψXF<λ ΣβYbͺfέ νQ€ „―Ψσxπϋ71ŠS +£^{<¬,‚Ό+πvJ5"όXΎΘΠ©pz’Ά…ic–B-ΠιžmΞγcŸTS/?“³Ύ%™–ΑŸ «,‡*μ=“J5LΛb f%νnπoΦ8$ψ©Jͺ_' mν3Ω£ψ΅ώjρQ)ƒΪh`ή–WTΠ:oͺO/ώ|u{ΈϊΧ‡x­nίPωβZιέ ώƍ‘5.G‰ΐ…±„2έ‹ooŒϊΓΫύ§/ΉΊώρΓ›\½|{ύφφΝΝΥ‡Ϋ7Ρ¬Λη/<Θ7l(³ΘΞ-!swΦUΌHI{yPΩξΎ„Ωέ°iSϋ”€-,_Αό•ο`ψšΐ©3ŽlέrΖ ΨΉWg(όM]ώ©~σ8ιž“$Έρ)ŽΌ$6d Ϊe.>/OΛΧ4ΩψθLZηLJ¦™¬t9OκŒθΕ=ϋν >:ϋέnπœ­ΤV₯Ӂμtt²΅\ε΄MZ `‰ˆŒs䠁ΣΩW»VGal'Ά[»zBdU?ω”τR HΒδrηS―ώtτν +endstream +endobj +581 0 obj +<< /BBox [ 0 0 654 173 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/arch/Canvas3.pdf) /PTEX.InfoDict 638 0 R /PTEX.PageNumber 1 /Resources << /ColorSpace << /Cs1 639 0 R /Cs2 640 0 R /Cs3 641 0 R >> /Font << /TT1 642 0 R /TT2 643 0 R /TT3 644 0 R >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << /Im1 645 0 R /Im2 646 0 R /Im3 647 0 R /Im4 648 0 R /Im5 649 0 R >> >> /Subtype /Form /Type /XObject /Length 7213 >> +stream +x₯œΛ’Ήq†χυ΅δ,t¦ξ—₯4RΨR8dΙ€Γ‹‘4§gHE“3␺ΌίΛ―βοO ¨:η4I[’BέΩ@"‘Θ;υ—φν_ڎ.σΤφλΨΎh£}Χ~ύΝΟ}ϋκηΆo~uώϋχϊλώΪ_hΐπ}Ϋ]φyμ»ωΖ μ/ΓΠυ]ί.]ۏsΫχώΎΪΏπwiož7’¬kŸ‘½ύψ‹ΎύΕ°ον4Mν«·νΨu†ͺΦ©}ΫNγ\~},ΏŽΫΤ<–ΑόΪ>Ά―Ϋημή7Ϊi£wVωΥ‹ΆςςC;φσ₯§ΎΗρ2Kσβmϋυ‹βΔ‹οΫgϊώΝoή½|lωϋί·Ώ}χέΓίΏj_όΉύΝ‹΄˜ρ­»lσ6uά9ύΠΤά.ΓΎnύn<š2‹ξΠœ˜Ίώ²vΓΠTμp»MS¬Μ-°w8‡§Ήγ‡SΊυ²μcίNύ~7~8°ηΫζΩ?Ώωαuϋλ‡ήΥφ[ϋμα‘ύƒψσΓϋΏΎyχΓWνΆ/~—Xvg»:’jΡ‘»τγΎ°»!mώ°θ³zς§Χ,σρΨΫ?=ϋοωO_Ne”4w—yXηAG}Ωύgε§iΨη~^u0ΓzΩFΗΊ_Ά[wDnΨΩ.Ί³—ήξ0]φq˜–jΦxιΐΏ4`š/ϋΦν[»’ +ύ†$ Γe_φm© ύ₯ο‡enΧ<―EqφΛ0ΟΣΨ{S A@Μs*YΟ±oύe™74/(pHs½»WhdίώωžZ ϋβ±L—~ίΨ1›ρ£—εΝΛFν—nYg >oΕL ΣΚΖΰA|VP(‡^όΨЌ:β~X”Iύfμ¨PH,οσ2‘pΏ _†Λ8i΅ΌzόξϊΞ^™±UφΙ$dνΧeZϋ ιΦmBΠ‘y»lΫ΄νΎ\Φ‘αΘ]ΦiΐξR…Ω(€νGΓcV³ν—}ŸW1d^/έ°.c»mˆφ6Ον<]Ίq(€αΟ;#ͺYέeμ†nkφΐ!±όœg9…,•οΫe^‘¨¬νζj_;ͺ9BΊ˜ΠŽΣzΦm•XΜ»Έhg7σή@δ e’¨ιΚ1͜!<ΰ‡ΦυȌ*ˆi‡ ,σX. LŽ<yyώ’¦΅\BތӌΦχZΕ (σή\0fΌΑ8Κ’―’,Β" 陨C—‘δ»GίΆμiίP…yΆcE|›°>1k’Β˞†ώΆΛx™—n˜[65 4CYΚnFΦς<ΈΙ0cλΘΔϊWš•idŽcΖάtX«HήY³žŒQ\@…%I.€xP5bδΦ^žiΖU*4σ'Ξ%ΝB>7φ«£ΒlΒ„vάPΏ;:N2hNYl; Nσ€ψΣ”˜Pˆ0ΕΌD₯¦9rLΨΎοζdHE₯ΟRt#αΨN«3³ˆ;—\L³l?b1—ešηΎΩ–Kߍ„Bxm +±G―§ ώ9,VωcΜ“'WLpαΟ7nƒ+š‡6o—u]ε°\vμ4^§Μ"bκ)~ α.Ό>Ά<Ο *α‰#ίπ;φλ·’wΧϊ<ŽΰΎpLƒρN’cϋ²Θlθtΰ0μΗ<β2šp²1 S½ ƒŽ -„ψΞrέ1γ*^ ^‚ώιχ4KΒ1"δΛ€,9κΔκπF“œBMrΔ²£–Κ‹##9ο,{φ“°rGF|ά{ψ€σΓ― £Θ•iβΝέΓ€ή»ΤΥQ’YΩa‹DϋΎlσ§0,˜PdPήvιqC: 8> ++DlΛ°RKΖοψ°Sν₯ΓΝ): θ G±@Οq$ ŠΑ`Ÿe’=DX‰aIί`Ax“Ύ[ήΣκ(Ϋ³o€ύά)zΙɜRš{ϋ™Dœ·)ά΄Ο…0‚μ k† ,ζX +ΌoDόŠ\ŠΨ€ˆε‹InŠ"_‘šaRcξ5©Oψ>‘*› Sϋ/βjw&Ks&f؁Ԙ{ƒΤϋΎH3­_Dκ!5Φ…’φΤ˜{&υσς¦N‚p‘ι$wE‰Z'ΪgΏyϋ_ί}gι•η£RήAy1‹RŠ•TGΒO%€€~VIΎ•ώππώΥΓO>’ΧΎ#•'³ηΏ>ξ²σ―ϋΆoύ#6‚…,—?U<έW’a5…ožΪ'S~˜•κACPlͺ–~εθHm&μΨ””jBŽυΣ΄χ>㆕¬`Ϋ6r‘† ³θ±ΕœTYœγ Χ·¦Rα LΡέNŽU&2™Π₯° +Ί##SςUΡ*cj+V°Χ1»©fΛΦͺ6ςDF¦δžΨ[„#"’ n{Ž–_³±ξ 2;6Ž=žΒFτ +•P „ƒΉNrœΤ<ΉwUe&μ¨-rE±‘J/Nͺ@ά£e_Ÿ'}Im¦&B―ƒ²mλp.Ξ|?oή=΄Ε3ά1κΗςA<ω>©ΌΧ…α„υω‡ίΏόα‘`5YΆŠΒ“U0Β +ΣΌ~Le°©KžF§|‡°‰}ٜTπ"σ)Ώ>ΆρλΨλΐc0Ώ3Ss΅γi_Ψ“gN±WO`Ή hGε Ÿ’AΒ?›ϋρρ$Bδ:έp‚Ak†5€jiϋά +v(D‘q¨ςήόκEmgμZ7SRvΤ{Αί~ήΐ+U> +Cΰ;BGνοˆχ›—―^?! +Τ Ξ2ΑަuΊQD!DοXiيΑΦ +7Rφ½£%ϊυ±όšΞήS½,’ σ/–χ„#¬L–@Yˆw­>•ΥWΞyœ9ϋ!Œ½ ΛΖ>°ε’ςΣGX—υ6ΚVT[1g$)SΦ»εUνKΊ V…£_ψναՏoϊψαε‡7?Ύ+j}C>tGωά”ΏΡ›ωŒϋ95Ξ +žξƒΏΤJ"CΟ)SX½“σΗk]f +ΊγǎЁ·ͺ.«N,mΣTb©•lY~0@μ!»FΌi*«>ι D*©‘-dΥ”§²±\!ž\?9qάξi*”~†$έΖΙ Π*z/V”\ρΕ³Šώzl„h’bΑvΕ2’Ω©‰ ! „T°°H‹ xͺγ<Θz’ IWϋΌΤ-’`U·ΗΦιl¦6ξi52ψσΤΟΰΒ,ΑήΙΡ’ υΧ―I08|μED\ͺΨ H‚±›!Β“Š˜Σ€Oξ{ΖΏΫ"‰Œš*P²ˆCΐ<β°mΗ§IŸΨ­„lγΡbε•~ζ„ ›Ν*`i`Oύ[ι¬ͺ‰D£G“Ά…MΞT–> ‰’σ΄Onά—oF*ʞΉξI N5γ΄P@^WΙΣͺΝ7wΜΟΜVˆtΈΛ7?Ϊ–ΓTό–igB5¨τΞΨ‘š,ΨηƒTrU”H$ΊRΨ’€ JΤΙΦk§ŸgλŽΒgO\7–θW€`e:HLΕ6ΠWδΝΤ8΄$…Ώ ωυΝΩڈΉ.]Δ˷ĐT°6΅ϊw³4‡œοu5Bq`γMΏΈL±τΩΩUαDω7Ε +}.Sό;^ν?>ΌΗέ˜yι ΈαD£ϋ/₯έ;¨DB F­=¦N’E bSσΡΚy©ΞgKω{§θc™ΚF<‚λRδDΩMsQ’Λ2vΙWz`Έ„“ŸTσ œ°­8‘{·H$‘Ÿ7R4x$ΦΖ)l(ˆVœM(˜€<η89»9ͺΫwͺ–ΨS0― SΏRΓ΅;Υ-ί©ή Έ“R„;)¦%<Œƒ°άαtD>NG.-O΄νΙ~Jt?/³―γ jΐX +zJ½Β~L?ΎmŸ½ΰ*•Jΐ³ΏύΨώΛΓ_Πυ‡—ωNυΥkέw" +Ίρ,Χͺ½]΄Ήω–+χxΧ~w/¬ζ° Yu7Ηyt©9­•’π:"Ο{l6‘έάΟˎΔά +}ΚΟ/ͺˆ§)†kϋB[y·f–Ζ’E0έ“%~YD`:°φυ(i9..Εΐ₯"9яͳ²0ξU(%jUξU£ΏAθSΘσoΈg ¦Qi=jΤ1/Aΰ“A΄^E*žF‰φŒ‹ ;sˆŠ»Ρ@ν_·‡‰φL©Tn:Ε+ ‹Σ@²Τ‰οΐ2₯Δ8£Έ\v€3H΅gBSγ{αL††9χβ<νi^K!.ρX΄g\”'ς‰Ε¨8U§iˆ³wJ‹„δέ4Σ)*If€hϒѐ5ηLΘmp―‚œε½β{ŒR„©<αΫ'•«Ζ€;TΖ,:ŠΪγͺΕχ€a¬Π'ӝ€Ααt†DQg(Θ.\―˜—.˜#؊6™ξŒ€ƒ1Ήdυy‹„‹ζŸgš2rw­ ›— /‰[Σziε2»υН…VLΈˆ,mœC¬ΠΨ vΈ₯D΅DZ1ν†`›Z’*ν™ΓδάiϜ,cΟά« ‰Η†ΙΖΐ«|†Λ`Ў<$έ \HνΊΰBΦ’ξT4H&Mw€%J‘[ӝ1vΔt‡ΛP„;νYρ ρέB}ν9Cΰ•s―@2E{šΗ¨|’έqΕ‰Ε( +E‰3ACuφAiHHή ’εR”χl2κ²ζœ)ιά« gy―ψ£R8R:ϊ ^UfΕΧΠ³Gκ­ά;`b±QŒ²ΓΊu²4›—-Β@ήa£ 2Yk W~XΒ^,C²•2LiŒΫ²ΚΎΥ~Εqέ°ŒΩ―+Ώβ”V–8零ֺ”΅Ξ6]¦Ξ`Ι?χ*Hζq@4Ο5(Ο#λRυϋθW$|Άλ˜)ca6ˆϋΑ~±"J0JέK +rφ+fžl”{άΙ~LΙOSμΚώΪVω•sΏR ωT*£‘ς+Aiψ•ΨMψ•Ψ3g~Εψž9ά« gy―ψrq'«\8£“ FΗjœΙ‚i;ΜN‡.Άtp„€˜-H(86V +S™5&w&˜˜+grΓμΈΉ:›&΄Ή8“˜W9“»6‡ͺŽd“)Μ2T†υδLd­΅Ω3Ρτe…)†ά9S…s―Έ‰ΰqΈό,FdΝΦt.ΆŸ†v:d²ΒT4HŽ”"Rv˜]γ"[mηUœΙ‚΅6첚{@2χ| ˜Β™8 gR`~b$;§!iarh‚%·'εΔ΅CΫMHQμΉ’5JF{-£‰{&‘i7ΞγΪŠοKΞ„e’yW$-nJΞΞΨΩ™λ$.ΖΜiL†U OŽγΙ™`u―œ +g’ξW$—|9ΠΟu’ε7 •3qX:‚eΗUœIŒr“©1WΞ˜ιFq&y7U’’χΞD‰™s&Μ}p―‚$’”|ΒI +΅Ό£3!…J|QjOΎJRtedΊIŠΚp„² ggβι[S’”Hϋ²3‰€―v&>xrXNΜq•Su*g29₯αL΄Cν¦JRςžIŠs¦H€s―‚œε]ηuv0JRξ8ZaΞΟ³RL<;:Žγuv&T/ΞH)βμLΊd’έT¬@œ °dΤ°#21β™I@ά€i=ε†ŽΉΐ’9¬pΉΙ,4Γ*ΨΙ™ψnά™ΠZyνL‚3α‚{Δ³υμΔ+?‹νARw΄ f…)“3. βΑ™ΠM΄;AΞΞδ*g£}v&$œI gR`Ω™TΈς©B§Σ€4DVꔆ„ψnά™€)οΉr&Α™p +Α½ +r–χŠο1*;“\ρΒΡ/ΧλR‚sWRΝ(Wlς¨&Κ]>Ν«] ιω:κΕ.@œ’Z4s‰ςέΖυΏ€jρδ€υKbτ„•’rςIIk0h₯< uΥ5u8LCρ±i$—cYά”; JŽ"0WΈ¨)Ϋ*@.pΰ…QZ^EρΚaΠξεŸ Δ‹WyT³"Y)AΒΝΉρ]h|–(EFSL•w¦\ΌR£=‹οž›™1ΪkˆqΉ“x¬s.£Yˆο‹+?Υ !Ÿ½αrJ³„Ψ³Γs)Š=Γ4—΅ΰŒ΄&Ρ₯?—Ρ౏©ωξ°SρͺθQΤ2ΘJ(9ΫπQQŸq€y|Y’Z―sͺQt?ΕοςΙEΐΘΪ‚ΈΩqΑ˜δάE Ω*3ΈyLvθr˜―k«η.ΒΫ‰EΛ F±―y ΕηZUXιRͺdΦή+UMδόΝU½ΔAU™ΚAQ₯Κμc.RΘώ)ͺ:€ΜEDαΗ·ͺΨ&Ρ@ς¨ς‰β hY>‚ξ¨δ—«κTq1*»ˆ”O›δΓ‹Εˁ.±ί&·υm+\ά’*UΩϊ8 βq}VI*LU’ +˜J#Ι!*YύΌπ‚Δ$"κJm£ˆʝKRrΙE`8.β`rΒEσβ."CˆΫŠ‹ˆQΩEΘ½˜BVΉˆ’I’\ΕE0*9³bT}7nxKIͺ˜gηLfέΉG†—F)I9€*Δ¨Ί$εγΌπRα"$?»ˆ(β—Ή/τ°Γδπ(τδΎΪsΒE Ζ=‡€)\„Γˆ%σY@»—·Β©Wμ"œδ*…œ½SŠde )»ΑΒ[ %)˜ΩE8gj,d4g!΅5ί]’“‹PSΑ—=‘‘Η)_λ£.<#τή”ό„†‚_ώτSn"ψροoήΎό@Wki Έ“ι{!ιEˆy‡¨κyχxnΰόϋΛWJλΛηα” šxš4aœ‚#Nθ>4?Π]σραηšςh€ΉjΔ%§΄Ψ#|oΞΎω‰ΆάhΊφv,LZιΧ”2λ1@Υ«]=Δ/sΥ––ΖE»„ΎΗπρy΄Nš ŽœΚ<ΡΥyμSώΓϋοΎkνiόWw£ΒœOΏ…WC ‘x±H·εJ±ƒβυžφ:EV=Jηuο*ΦζX™FˆNk­?B₯‡†jA&˜ϊ.iΗιΡ鐦ηι6ιN,ΣΤΥI—zΑ€X>Ο +΅XBέPΏΪxMηŸ/_AΞ{{ϊ%|£φmc†UΕΦΤK +ƒΔ ψO6EkγT˜œ#˜†7β₯}*"k_₯.4ΟΌΤλcoΜ3„¦T1C5Σ4ΖΪ²Δ Υ‘ς Α²ηυc^X4’ j8„ϋe,_ AdšG,§6&) ]υ[x½P+oαYDɝB„i·&uzΣh!’_ένv^χ6Bž°ρΦ -σ8©Α^‚ΣbA³²Zω±¨ϊ*²₯„­†°8 Š4\ϊ,:¦hUΣbŽΊ@ςς JΣ +‘<­ΛΘΉΖΰΩ_w’Φ―η½=ρ²uΖμ‹Ψz–ihΓ"*#δ7v> QAθί{Ίέ|Φ“ΠyE•yύMj'pg€{΄bΝc„&κnβC= ζ'FLŽΊ@ςςM™ηDj1GΞͺ=]…yuœžv ο Ωh$Ÿx(Bκ½’œΔΪrL+’LΓlO†sμμk21ΆbŒ%’ γrΨTQ4Z»Ϊο@C\‘j΄Œ{SΝcC:^^>;φ€=υ]Ν+tκ {ƒx™ ρΘTσώž”˜Α΅‰ M>d ΞξXdΖα€CΘ%ω6†1Qύ‘Ό›ΗAπb_͞|ύE]Ÿ°7Ι₯mbF ;ΪΤaζPΜ°'dΒ>qK/˜0ΛζHψ‚Ež—ιΤzŽ]…zN2(ΐeˆΔθΈ?· TψžxίλJ£­“¦ϊ ΪDuD<χνœΪ΄"‘;YES:GΛ<Θδ΅–z‰zŠχ|ΧL5ν|p₯WDIdΥT,>F@σθyδ# ˜žΐ €[mΝΣT¬—±Λ@υκLφυγχσήξˆjcΠ›‘j(υuθκxΔτωΤ‚L7 ΏxŽ˜Gζ9ξγͺ4œόΤ‘°`PhFδ+>p^@ΉΕυlj–ΔjCžΕ=γξ¦@b}5Ϋ<§Rσ9φj“FΖςΘ4bΡςޞ|―e±β"ŽBXAΨΡIειxβ©R›<ԝprΒ- Ψ ™Uήμgn|Άe;"74CΡψΫΒΓg§&4λT΄Eiγ£:!}"CT ‡OΌ°·3Uyέή£ΒQ υ@α―1γ±L’""VoΩξΉδK,Λ#=YO=©Αδ';FbE_}zQr5κ£gϋή+ΎR‘– Ϊjz ψfND5²Μ+lζ0τΈσzΆψϋ‰χ¬ενύΥ;”biΔԞ;€—lΒο°Φ„’κŽ „eΘ;0=jPΑ’W3nLfά‘B7ζk—ιB¬τdBίI­ŠρΡ τΞ‘ΐκ7 y6τjήΑΞΑs+{ύDMό’φxϋγνκΥ[AUοs»:ΤΗΔ;osΰvπfϋ³_½ό@ŸϊsN~χρ‘'Φ±>ΤίθΚύκν'ϊΥν&oΒ¬;ΒΘ§Ίh΅οΜμ8O>F₯o’d˜vœa2%„•z|΄ϊ\‡™ˆθp>;1βڊgψ„6HΫΘƒ +―“IΥε+]\ΖUηVΐjRi0Ÿ«ςΉ`΅LΨGΏμE+'GJb Έ|Vξ˜”'EθΏ°$’'§†Uή‹€ϊ6Φα ±²SNtφv‘εΩ―ρξεΫ7―Z£ωκ³ŠFˆξΠ8UηΉ&ͺΏτ8«ωϊόA ώdO‹;‹cŽ3Σγ¬ιζγ¬κuρρ{wΌ?ͺέVϊΫ”μξd½‰σ‹· _q€a_q|ώαα§v*S41’ο=yIOφ’wϊwύ،dΰDωS~*}š–63ί܌c«ή7ζό?γ»ͺDΡπP‰Ϊ‘Z €o;πέ½ ΉΝ–μΘ)Uϋm¦0l”\™LΌŽQ ¦`ΨΎ0S„[³?ι@+ΪT·€6ΡKiXKκ³m’—%ωPQZ²‚ι{ΎUβ ΝΞτΖ‘Λ!Ž‘ζf&Ρί’βBΏΓ±?­~gŒ›^θΘΜΫ‡ŸΚX7PŽE’>ƒHς…δP)Τ³γ(+uώγnΛ[τ +endstream +endobj +582 0 obj +<< /BaseFont /XKEAZI+LibertineMathMI5 /FirstChar 56 /FontDescriptor 650 0 R /LastChar 56 /Subtype /Type1 /ToUnicode 651 0 R /Type /Font /Widths 652 0 R >> +endobj +583 0 obj +<< /BaseFont /ZGUGQH+Inconsolatazi4-Regular /Encoding 653 0 R /FirstChar 46 /FontDescriptor 654 0 R /LastChar 121 /Subtype /Type1 /ToUnicode 655 0 R /Type /Font /Widths 656 0 R >> +endobj +584 0 obj +<< /BBox [ 0 0 424.8 172.8 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/main_eval/hnsw_visit_count_per_degree_corrected.pdf) /PTEX.InfoDict 657 0 R /PTEX.PageNumber 1 /Resources << /ExtGState << /A1 << /CA 0 /Type /ExtGState /ca 1 >> /A2 << /CA 1 /Type /ExtGState /ca 1 >> >> /Font << /F1 658 0 R /F2 659 0 R /F3 660 0 R /F4 661 0 R /F5 662 0 R >> /Pattern << >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << >> >> /Subtype /Form /Type /XObject /Length 1358 >> +stream +xœ½XMoά6-zΤ―ΰq}=3$‡Γc4r(šΖhMMβΈμM‚ύχ}ΤW€"£2P,‚uvŸ€y|Τ|<ιόΙείΧo.zφΨ}χr8Ÿ½ω4°{Ο•#χŸ/Žέ3|r#ΉΫ!JτVΏήΎrό½Αρ»o Γ»αό.ϊ„Ο† Ύ˜vΑ<η’¬Τ8œ½6ΘΝaMψ{3_tLQrœŠ©…γυf™ ΏΘ+g-ā³cΙ> +Ef–"ξγ₯ϋΕύιΞ 6°OGG³ΖL©`₯CŠ^,0Ηn­9ϋΩ:7-Z| %JίDhP,ώρπΒύ?Λ°―λΛ·ΰ•4qΏΥLΩ±nύ ³Oˆcu©sŒ=‘œι‹”BΦ+k0SI½‚Nζc±HΉΒs>•„(^RΤηPMγΜB"½„VσΔ©ζΣM€…O%!Ϛi‘G%y + 9ίK˜a”D°”ΈΦk€…O$Aˆ|!BτN‚ e°pŸH l¨ί¨₯Τνn‚΄π©$ς"!–ΤKH(Pα>‘Ψ +κv_ΆMŒ=•€Κ¨Vϊ4B{τDI©τfΈ ε‹q˜dc4θ©˜y„€€ώ‰ΡΟ―fb\¨l: ;Fιπ‰Œ€%¨υi"{4H}50SΐΛF6‰;FιπS‰ΨΟ"2ιEdhNΉτ©ΤΐΜδ-αŸLβŽQ:όT"²zΜ’˜ϋtŠθ2˜V\vh†ΉA <‰;Fι𣈽ΑσΊ‚oƒ‡ƒ'ϋR}XEUL‰9tKhΠθυnˆΗξΛπψb8ΚΞ|Q9YΙξβέ0qGl)v";4Π\°©€!DwρvΨρΟάΕϋαϋ‹aZΒ€1Πd΅tΤ Ί™ΊTΚ9†BΒ+Τi,=5΄!«‹¨φ­7“3TΞ΄€jkΒiδ…tFsFb!NΟίΐΫωa Γί`χΡμWψΣΘKύ譁”ςgt;;¦ΊEΡ"ΜIWΨ…FY¨ΗLς*%¦άρήΜ/θ ¬ψi8y…=²Π.θh%‰₯~ο[x;;Ζi΄뭏q…?Π–κ±c’{λzΫ’ΫΩQrƘ#αzΧΨΣκΊH*M¨oœ Ό–[2n~XΛΌHc\¨Η@„‡νέp oζ―3(¦’9 Ρ5ώ4Ζ₯~μŒΫ’~F·³cxd ”λm[k:4¦₯x4ͺ€ΕΒbσx;=L4“Βƒ¦ k©—˜ŽβA0δ’RΞΒu*ί’^¦ΤŒ0£\@Tj_έξ‡o/Ο܈žŽΦnΛ—ξž\^ατRJ*˜Εν>^βΌίάΕσFοκƒ~-Y<;χZWE1π‘S’;Ž -b_žΘ±lZεŽΫ|G«β#Ά¬₯Α‡Ρ¦:d°IΠeίXο‘5υυNInigπa΄Υ›EΝ\MϋΤΣ,Œ’iΛΫ #―`l’)žεςΎŸάΗπΔWΠ{ζ} sΰκρ%L”©ŽογEΩΤ-LhΛ;£δMxφΞ n7+ΖΐTDU@O52¬ΧηφDšRF!EΕ$εlŠΠΨ»[œG‡‚ϊωϊΣυη•Šϊργ‡ΧΏΏΎΎΉώόΟΚΡW»Cq?•Ν«¨~† dEt)n–±ϋvuώ4lκΣLΞ΅ς0₯šXL‡`Ρ!cpΫ‹¬ιcaΈ²α)Ap :–l˜΄‚ΦΡξΞξ›C€΄9R½ΥΘ‚om$=DΪ~Λ&σ…Ϊ&˜)ψΈ&Φ«³»,χ|tςΠύ{ΥfχΥ[ΛαεWo:oo:qΚΖ7€ν™ϋ«Wb½ώϊl§ +endstream +endobj +585 0 obj +<< /BBox [ 0 0 1996.0275 438.448 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/main_eval/main_exp_fig_1.pdf) /PTEX.InfoDict 663 0 R /PTEX.PageNumber 1 /Resources << /ExtGState << /A1 << /CA 0 /Type /ExtGState /ca 1 >> /A2 << /CA .7 /Type /ExtGState /ca 1 >> /A3 << /CA 1 /Type /ExtGState /ca 1 >> /A4 << /CA .85 /Type /ExtGState /ca .85 >> /A5 << /CA .7 /Type /ExtGState /ca .7 >> /A6 << /CA .8 /Type /ExtGState /ca .8 >> >> /Font << /F1 664 0 R /F2 665 0 R /F3 666 0 R >> /Pattern << >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << >> >> /Subtype /Form /Type /XObject /Length 9284 >> +stream +xœν]M“$·qυΉE_±<°ˆοƒ€%­Μ°i‘ά°”ŒΥЦ<+Κ$%…ύλύΊ»‰.Μ,—έ³½³CΞd‘€όB"QυυΡ/_ύύΫ—―ΎxώΙώ_Ύά}ΤzωΓΞoφfgόόcoχΟρσΝΞΰ―Χ;[kZŒΛ‘:ό}§ώΎ,!PψύΏv»?ν>ϊ]ό€;žοvWLu%μSZJ0ΑtC]j.6zAΎ“d—νn΄ χNΉ φ?ϋΣUC²k9…œχ!²})©¦jq» KvψΣ§hχίΏΪn—ύG;²kwPΑ’ „―φnqx©iˆρGό­Ιδ<44θΠ–₯ϊhƒU2 +²βzχεξσύ`Tί”΄ΨύΓψωΗαΟηGκξ“»~ΫύR­ρ>9_νώşv5,.Uο³ ήξ}]Š !TcCΨΏψγξ™5μ_όχΊ½MKφΩςΦRx―‹΅ZWΠΓΎ,)§’½©έψOΗϋμω}Φ/Ζζβ)Q…RΪ8­ω―^4.czδΫ™Α»Ψ š2ƒ _Ϊ Ά@'J2ώ>;lθσ^;˜›R,Δ ΰ¬x­ΨNΎ΄b]Š‹Λ9טr.©Ψ›ςXτ΅€BΤΑQ/­Xς]Ž)Ε /¨XwKŠ ³ϊΰ\TŠδK+6ΈΊΐa’ƒ― ώ–=τP<½œwς₯­cΖaMq—α€Χ62bXXͺIΙ šŸ‚[𧏎βμΑG4‘¦)3{ψjμίΎώρΥ_^ώοϋ­– +¬σ&δύ³ί?ϋαχ|•ΎψτVlΧ“(hd‰`ZQ©˜ tΫωhγ1'¬ƒR\ε¨f1Ωa-.«νΜ/nΙ»…†ͺY<όΩΩAC~ »g«OSθVoVE.bbΜ§U$θWPήI&2Δ<¨Θ Ν:1πC ρ0Λ А«{LΪ‚xβ1ο‘Ώ―ΫΆ}rnφϋοώϊέχ˜Ϊˆ9œΩΧέ?ϋρΫοώςυ]Ÿς> ΡOqμΛoE)΅DW tρμΥ»?μ.­§,tΠ§Y25ΪΤ)/ξ 9δν)βkβœ…Nm²}6©$LΜ°X!Έ³Φ.‰z«šŽ½Φ'—•œ‘mB6žC:ώΛΕΔRΐ(,τύΉxύΎέ}χνš(Ω§b*œ‰λρ¬CŠ­Eο=a9Ρ»|TΙ©IΏ΄J,υP‹ρ/‘ς˜Θ…Ά,ηzΈΏ1…‹―Ρϊ<ονβLŠΜ”π‚…ού\SxΨ5»£ M#ΘφκΚ–?¬Wwχέ• @^ ‡TTcƒJ]©Dτse•\}ŠpΕ/!œδ zkO^©θςn[,nb8ƒ½©ΰΜzlW’ΝΘ«œΕp&9¬ο/ΡGΫkDw‡δ !Ό­ϋˆ–ΛEΡΔu8vΡιŽb1{ίˎr΄#o Δ-ΜcŸkλ’σ »!ώ₯ΐ»|Λ'΄α$..0':Ρ­ύβ‘ΜΥ1Β-ό/αΊfΓΦ%ω@φΟvS<;Q†έœθW(Yp‘LYφ™M]ΑΕm=μΒ*¬4Ί3’χl²Υ"Βe,φ%jwN\Β3wίڝ˂uή8―έΉ(άY•;wΊtηή±pηΞ…vηΞ³tη.ŸtP1œrηΌ8Δƒuk V•“-ΰΏfηCUώΌ)ŸhΓn©Nτ+ύΉs‘ύΉσ,ΊΛg6΅qM‡~Œ… Ή[ξ4ΊΉΗŽ5^žjυCΠ.ΨVG΄½Z2ΨΒ&₯Πμ¦’DiWA—^ή;^ήΉΠ^ήy–^ΎŠ'άVθB;yYΌlE;y\r,>hχΨxLŸΡΗ±“AmTΜv›Ή‰.ά–βDΏΗ;ΪΗW–₯‹wιΜ¦.μβΧNLЎ›«±=©±=Z‹ΜΥ¨σ#[γύτ­φ ϋΫ;3io'ν­lο&νν€½}Έ½™΄—όίτ2L%ξΨΓ`<,^GΊ6FΨjOϊƒν΅²D{;i―Œα'νύ€½{Έ½™΄Ώ¦ρaΙq1‚UΜ:˜4™]›(m΅=?Ψ^«P΄·“φΚDi>NΪΗ‡Ϋ›Iϋ+›τκ!–‹oHδ_›4€-,%n ₯ξ vΖ!`ιn*± ΜTέ=τЬΥπΧ‘Ÿ ]Ο•Ο)]υγ'γϊ Ÿ²AΏν»P°=ŒWqŸα,”τΑHΘ΄"ƒΤ=te$ΡΟ„ŒΤωœ΅‘&γϊ Ÿ²Ÿ+οQB,2δ€₯ގ!6!νεΣηΑ€U’ύ‚ŸΆ΅3ΊT­μgBΧ!sεsJWύΔΙΈqΒ§μηΊ&½rˆ Ξς=DHa_Ϊ[Q"g^ο‚]Ιi%ίν‚ί$ίr"ξ‹ΟGkΡΊ•¬ ›δχoͺ$%•μœ;d%vή$ΏξάVθZ0Ύέ—Ό8 –b[s°Ÿ΅|'˜±’ζφ’žsΧ f'MΤ&ΥΉΟhΰJ ©΅F‡ 6~ŸJƒˆρ±t™Η1 ’ΪΦ%#–ΐ1UΏ1CTηΐEΖNΩUΨD’oy΅%9dθ/iν#ŒςαaŠƒ„~)ΥβŸ&GΎήK9W­&LMΘƒh,UΪTκOaA{“ƒl ΧvΜ{έoͺΓg3rl!ΐΌωͺκ„EΫσ9‹GŒΚ(\!ϋ0Μ‰–ΒG8ŠŠ”cΈ΄Φ]­KŠ…u»žΕx& 1φ/|η,ΪςαR0˜šͺ_OgΌf’=s‚QΆIŽ―πέώΣ=,ϋλϋ>@βƒ&ψέέ—Ϋ@ΰΧS 0ξψI€bΥ^τtο}πεO …nPBΜώ‹6Ί•“ΡΛ’ah›šΡα+Φ‚Vς]σ…3²7ιD¬f‹NΔ—;AΆΦ/Άdγ~#θωDΓρ ϋJ5Τ—RA?$gηtF•š7©^φ½»8wŠΌŠ.ΖλJΪΤσρqζΡ†ωώ*Y­t žπ |ΣΐG―˜ΆΥ7lΧΪ³K…π"9JΑ$Œ Tςϋώ‡¨δ[Ω†«Œ¨1oβ-b8f_η|τ-WΛxφΩη—τψΓJσiS?„°rΟ…½’G&@ς-IΑΙ‡·.+<φNΘ±?Š‹‡—#ΆxGˆ?ςΈœΖv&ΓΑσ-yM|@`‘$P±Lt±'©fΧθΌB›ύa­€„j‚«†@>Ώΐœ±°Vκ{θΪ0Δ¦tάDΨCB›Ϋ» +°Œ‘‘ΩWs!εΔΧf‘ΈN,(μΑB£°3ί ΑG}†²Jγ.¦εψα Ή6£X<¨|'μl*ŠG†‘#Χ'>°θIΥΛ‰Rϣ䛝ψΐ;wP1JΤ5ΑΔτcΆbΜ’“©y‘ϊK΅ |ΫPψΜί{Wvά biFζ`3¬ΦMΟ>yυ㏯Ύ?yή±BCV~˜cεΗ.ζφR&λˆl1§—ΘDΖΧNUA΄wq^―A5sNΤ·„±νo‹c­άŠ 9-zιδ‹γX3_ο@pSr·R³ρΞ ‘ +6GΖϊ¨ !Θ—6D"¬šGDbV΄ρΞ5‹ΐˆΤΫζ(5+Θ—ΦlΖv 9&Άƒ&\T±·ε²%Δι”VλJΌ΄R‹Λ\ΔWƒEκZο\«•ΰKdYΡ ςΕ‹θL]˜I€ΘU²ρΞ5‹m!EγήnXΡ;ύβΊE¦‡Ό1"©uχ.v―Ϊ@¦‰LI=γ›­Ÿ[΅ρΞΝΧs)UΆΡ3―±jCά «Δ CƒΘΦ$¦_δvώ θ%μš{½9–=!Ω­ΨU³#±μ‰€―œKu³ž˜&N°μ—βκΟ^S›¬5pξ„­OrΦΖΛ.ι£,ΊΩ„._J#—zςΉk²cΧ— +·ηƒπžχE§•r§θJxΡΟU₯„ ›mdZ.ΧQ'‰O\SBΠ•ND?ΧΦΙΥηˆmΔj5.kVϊΜ₯HΠ׎#XnΦ%>6₯X5 +/9> ·,˜½δϊ€+ΌO +O;rPvΌ‚ %‘(XξπE)]G:J]H\$c¨©>&ݘƒΤ\½κΨ’Ÿέ€ANΚ`ωhΨδ•e³)™θΒl)Nτ+t,ΈP< ϋuρΜ¦.6q‘7υψΌaΗ_Ζ’Μg?Κ—#–·Cι’tf(Γ"uΚηΞΜg|>©ω0žrε#ipδU»ρ©KαΖ}|νƝ[νΗ'Ι΄Ό"ŸκθΖΒΨэKΆή»s7FVh’άψL6³©s.Ρ£vΰΣψ£ŸΈΥ|’Μlhαͺώϋ(λ–m―ΐBύ¦1Qš›…RBΦn]kG·φ-~ψ’°λΙχ»₯$QUΠ…ˆŽ…sw.΄swž₯s―β ΊΠή]g¬±Ί5vJ 6ŽΪΉaMΞ$;:·γΖ([‰]OvS:;Ρ…έRœθWΊxηB»ψΚ²τπ.ΩΤΕ΅}όκyί'ωΐ> +Ι•?Ρx4›­φ ΫΫ+pͺlo'ν%bKς#Ϋ ώUϋTno&ν7Α²7ΉξfζSȈ­¬‡©x€kkψ­φ€?Ψ^[O΄·“φΚznήMΪΫ‡Ϋ›Iϋ«ZοV¬³`Ξg3Ϊ\ιΪFq«=θιΑφZ‡’½΄W6Š“φa><άήLΪ_ۦ׎²™'`©ΠΩ²4*{¬ΏC0]ι +VšΉ§ΚΑΨp]ΐVU?Ίžv'6gdΥ‹™Œj&\ŠnΆ°²7aγb?Ÿ™Ω’ZςaŒ€+] ο‘ςΌ±χΠ₯e?ΊŽ°+ŸSϊ™ lk'|Κ~i½Ηˆ°Πy%Xm΄i`]Ύ/cA‚ kD¬FŽo(ο‘KέΚ~&te»Ξη”}c2Ÿπ)ϋΉ²M―a‘ηcγJΔ”D9—z’J4s΅[Τ›CυΈ΅ςZΎjW²Πo’ίΓ©Z#ΩΉ U 5¬d%wΪ$Ώ‡ξŒp[Ψ/cΎK¬nΖnΪ'Ÿ­FεfΎ˜ŒŽεŠΜ]6Ά½^ƒj3O܈ΕP+W (ΑΊLτ­Ύ%°‰Γb}{Ζ ϊ , +‰&jtw‹>Ζ—m¬ξ-ΞΆ†ρΖ΄Z©}π°νyK²­ΞψΜ2³€ψͺ΅δŒŠD?‹ρ”ςω.iyJͺ-"»I‘ZuΛ,½X>bVLΎn4ΡΧλͺ1VνR—TC¨Ί¦Σ\…0z]†γΒσ4.Ο~!’Φ^‘λbʚ1 ²Jet56ɚ‚\ωΜΜX§k + +–ULφ\.ͺ[R +ΡOj +ήΐΎ~œw_nβ"_Οp‘κeϋΓθJύn^’Χηύ?^η:™\€Χ‘/,ΰΞΐλτ„3rΖ―GbGpK’― ²―+ϊ +%Γ ΨΉΰMΧ…( +ΌžyΤ=σψ5oR½μ{₯KπΊ$wΡϋxBI[zΎx}ν™Ψ{Iπ:Ρ6Ε;ΎΓ8Γ΅φβƒΓ>™/φ5X¬ŸρΔΓΉ‡Ι!ΫΕ.κΩ·φλΟ?Ύ8’ΖV₯κΞΜ…<·³΅Τ½Π]Rδ‹Ρu―ΰm@V¨{5Ψ’±ΤΜi¨{…OʬΙλ^‰S‡¨­(wΕΊƒaXΖ@D‰u/,ΌŒΏ +κŽtyΑBHwΗΊ³οe)λ^Ϋ  ׎+XχΫΟ:#u'΄-@–γιQG¬{εY%Α“Žu―L%§ωUbέ·΅ϊ.±ξ₯2o²%Φc½Φ>‘9λU<Ζ‚‰εΥΈο.θ*ςʎΆ1ο’…eέα»bΒζΖ‡aριτ‹‘o剳Ύά0φcβήoΐ–'•€I£Ο(—τ‹ƒ'Z"΄a‹MΊ(δΥܘvY“ŒM`η΅ _\»ν,Ά₯ψ”ξύ,ΗΟΐί€v=3Dέ1λτ‹k—ΥŽXB„Μρz@ψΠ.+ζlbΕ›Φ _^»Ψ—μ*°c.ρz`ψ[Π.$E*^†/IΏΈv:DάΕΏ +o™Όa%tΘ/ύππ7`A‘jiHΌHΞFPΌΊIΑβεM0^%u +/Σΐ pό%”τ“Ρρ\ήyώc-< +Rυ~o}q<μϋ€d"QΑΫω Jςη½co1γpΌ· „>~§/θ3Ο׎6!ΡSΚE°ZžKŽm«‡ϋž+€Ο1υAψwϊΒ €ήΥuUπΗες@`ˆβψ `PŒ‡Ο»vNφ ˜~aPLοκꊹώ„ρ.•‚F]R1|EWτωοΆ=yrνΌ^q-έ(¦|84X]K5ςyBΠGΐcΥ[‡'™Kͺ:'Z\ΗJ‹ΞΕΤ‚}»ΰ]œύ+%uRR1¦b†Ρ|Φ§σtiδNΙTέ;›ΐ΅ΖSΌ`b4‡#‡;οn[P7SŒΫΤ£θ[*½³’m$8—6ršm½lΒ4o56‚ο˜M‹ιgžξψ(,yzF–δύ™§§`ωZκάΣ£…ŒξΜΣOΓjO_©£§Ÿ. ž~κ\{ϊ‰—3O?ρ>xϊIΑΣΧ1GO―ΆπΙα™§ηΰy(Θθι•Πΰ WG2›k]}KR7ӌΫT€θ{tυ#/ηΎ~δ}πυ“€f[3ΧυυGY!M&˜Ό1ψ<Ω—³P±Š˜x6JŽζ,ŽΐI³Gδυg3ΰ4¬ž+uœ§ Γ 8ugΐ‰—³pβ}˜'I‡°Ž©g€cŠΟιlπέ›)g3 [ζ3‘Τδql κfŠq›zμ]ώβδΜOœ+οršm½\έ!Χ!Ϊ‹8…Ok›¦]!bAίhwΨKέί^!ne{;ioUϋ0iο'νύƒνΝ€ύ&ψfπφŽ’˜rf?Ί0dλŽf‘‡οmΨο°³;+Ξξπ³;όάafw\Χ”²>1ΛηΖ뙁λιΒ`ΈuG»ππ£ϋvvΗ`ΰΩav‡ƒ;Μ쎫ψ0Ož‹΅ΖΡΐ„kΗc±;φ +KΫb§1Υ»r߁ΦΥ]Ν. ζZٝ_Π]ΉΩΰnΖμj ,|³ΑΨ!y(ќ-¦ž΅}ΡΆΓGFW>^8·ε χΠ΅½DG³ ƒ½VfηΞμ΅9Ά›π*;Ίͺg£ΐοΑω”έY fΆργ7=δ…Ρˆ° ΆP&έwA©Xv5»0Xqew~a0γlp7cWvum_9CH·π ν O/η ¦]"½ωEŽ-ϊmΗ(pν—„½a*ƒ”u₯k)ν&ύ}œΒν5<Ό΅ΔAvoWΊ–έoίGοζWA+1»  ΝΦβ―ΒN4žίZόεxZθsΎ’Π$ϊ˜§FςMΉ‰@.G•ΈfΠyΟΦ­#‚’І±σYή‚(cάΐ +|6ǘ‡#ήo{RSΨΈ€’νh‰Έ8g¬ ά[†£ϊ›Β} ό΄Μ`₯0 ΌRmt0Μ™bρUΒΚΚˆƒ!˜n#ŒΌT‹-—aMτu-ρ ½γqΡσΛΚ>ν›s 2 …‡Ιtεβ‹(Ÿ)1ρS©Κ:9ͺ2q +™'ŸxέΊπ[΅Ž3Ε%€\gφq6q‘f2Sή°` + ‘< Ψ„ŒΎžCF€Α›ΐOGL‚* Έgœ‹0mμκ ’*ΐΊvT:-ͺ,τzN·„©―¨’2@Ui€Ύ°"υε˜Φ/YTΥR&U€>σΖ…€Vrή&{ΩΏ K½kA *4Ά©χ‹T τCΔβ~Ι2ΛΟ§π W7Ήώ[Β/ όΪFccΰCnβπΗ`«T$ώkq]0 =ž4s,°Ύϋ„MzΥ,Κk‘3z]`=όΎ²4RΤXΩψW>YοEXμYΔBΙΊ *?ƒuθ…˜ωk•…-2²€+pϊ…ύ‹hΐ>k‚:τής”ύΡ\@_ό.mi[,Y 0Ρπ»,€Έό&@ρ&Φ\.T ω‚4"Ιj³*K•°V§‚±€λZΡΡ€`mα’όdΙ#‚θN"a a@AvςΕA‘«mI99–τέJ%ΐ»7E +ȝ±½ŽϊθkIΏΈ1Ϊq[HΩ’GŠ―W πξ΅Λ㍰ŒsZ»‚~qνς΄‹^‘“R―X πξ΅ Ή‡{–H+ν +ϊε΅Λ9l+LŒ*»Z%ΐ h—uΨόbΘP°%θΧnΑ,ΉΕS.όi·ςγt† +,AΏΈv+Ώ Cv©ͺttAՎή/HϋχΎ₯ŸwN?οœ+?οr*?CjίB²iλ‘ώ@::‹Δΰxqpt¬˜Ζ΄―†ŽΞ#£Mz’ζmAσL1yS’oε蝗ΑΡ;οΚΡ»€f[3ΧυτΗXcΑΦ >Oύ?σ#Α‡σH…τ•»š”‡ͺ‰X‘ξzh7žω?άΛd—Η Π‡•@PυθΤθ 'ΌhŸΌ«Π%U3@Œ©˜)^ΠζB¦³qG»ππZ•ς;»C™Kr₯οp³;μάafw\έΐΧΖ„δΥPέXΐIWυ³‰Ί^Π]>Ϋt«α±W$ΨWu5»0Μΰ•έωέU™ ^&μͺ6‘Ζ7Œ‰h΄ΙŸΌώέήӏ¦F‹ρATεyΜχ\ΪT]Ν.h‹uvη‹MW”)eWW5ε£c$›Ύ&δ”gΑΨ.&yΖ’qa°#ΛS EΉ‡¬(;š]λΚμόΒΰ)“±Ν„WΩΡ΅{νŠ€‚,Σ5Ψ±‚‹—t"+΄x)[δΫP–'¨FS°½d,+] YΝ&ύ}œ»ΆςvςνU);ΏΩp€kΩύ&ύ}tmƈΐ—Ήi¨8†Βλ€˜ίΣ!^2Δ‘^Ή`7ΠΊΔ!σ+κ¬πύ}Pqζ‘φ-Stc, πΫ4πPϋΥϋ‘~`ΠxΎ>ήΖ ίβ€8ό>’χΆ –@’ν½ηǚ1Έ‚λP&ΐΈ±ά¦ΞΌ<:ΦHHέφA΅%’UekmcΚ‡8tΝ—φ%€±bί.pΰ'N>π ’jψ²ΔΤμϋ`³Όαi2Zψ–‚»s茨5ς;XZ‰ά›Ίθ6PU©τ”‚U­ŝIiθΉς΄ηΖΊ„ΜwEΩς›Y?«`† Α<‰ΨČΎžcFŒΑ›ΰOGX‚*Έgœ‡‹ψ’“+Θb€\0Ε2‚¬‹θ"gτΣ:R ^Re1€ λbu‘£ςŘΓ/XΤΕB&] ΠβΙΩ…«Wrή&{ΥzAHΊΠBTjlKο—)θ]‡‚…ζ’Εά–yΔF–Φρ±σb€ί|χγ_Ώϋq(Έ@lœ ¨-؏ΉφzKX ΪlT<ˆί° ?ͺ‚θ λ/2ό8DŸ\J­u((±.PC{l. +J8T;§ +0 ²XuA’~(‘–* Ψ ?DΠςχ‚€BΠQM‡·Ν²  Dθ7UοP@€ŸΓ{ˆ^PJ[*E'2w±ͺ(`[Λο΄( Δ„T"dδoΖύ„š€€j–²Vπ`bxΣή;Ύ`jΟeθH1πΰ)Χš”vΐ?q*g–zlΟχW‡G[Š g;΄¦νΔlΒl:NP {αΧΘԘ™ +‘K ΄—Jœ#ρn׎βgΎί홐Ϊ‰q$υΐΛN:±Kp§ι]`1–ΠΞ¦–uΨ3Ν8ΟωlΤΪσc{ˆ‡5―tz\ιν Ν&½υ/Όs;άρq\DRλrmL$λΩX"”β1Umλυ=ηϊwΧ@ΊΦ™Xίyμ΄3s¦Γ­αΔ»ηšqFΏStπήι’I+™Bϋ| ;γή~K¦_~ϋΓόΩgZ¬£H a¬΄S="ϊjΔγ§Gω™NO0’tΩ†ρ‘Pd‘CĎaΘσHήZ|‹΄‰‡υΣYα*Φ²ΏGc +―‚ΫKβj`ήΎ’ωY]„Khaν3k 7bτΘoΎVWρ[πWθΈ έάεuΉξWΉV%t"Ώ_ω4–RΉCœεIΰ’c‡ΖŠ‘Ωΐœ@œ…¨’g–oœ‰Φ‰J ‚άU&zΪ<(CŽ…Ω„tΔώπRyζye*` ύΈΚοΗo8εo>ϋςwΣ‰†Εk!V―ͺέ’Λe8y~μiβp΅ΗΗ)εμζχ!Ψq&gνσl#O1ςC_ΆεA]ˆ·3?˜Ι.gH[† εΓ6©%neφΎΑΜ-5Ӟ­π<•ΔέβB«­εGv +/€ŒΤΤΉo͜Hš\–thM TΏxˆπaž**L^™―3pkΤ0^Αν₯ηDΌ9t…₯φD πzlΧΗQΤOμr% + ξ4}XŒ%΄³­ε!Γώ‰Οβίθ όΟίΌίέ.ςmν‘M>Λ.ξΫ#wΙr_+θ[uo`ΕpΙΘ·η!,>L€RC DΕtΐάαw4ƒi*ͺ&ξ£]δ·uΌ3¬έQΤu8vΡιψ•›bμ}EΗΆνŠ …β{oΓcΤ™Iž-Vil’ΡC/X!žMpΪιΨΠ:IΛ?O’γP™ΝτIqx Aς<ΑHπŒ]Χ†x’j[ͺ“=w5K>€M$Σέ‚B@eν-ίx‹΄2Fw˜ θ“'!ΨΦRΰΣϊ²ρΛ―ΏζՏK –‘―οξώΉΔ_τ…εΑχ&σΛ|–·»οndΆ™‡£Dμνυ,αW&saΉ €—ΜG"¦™iUqiG^%D4m>‚T<M˜―”u@ajE‘τξF²ηξr’ι ‚ιΥ™…xΒο…2Τ,α3P4•―³ι@ήΙn0 v€Ι,$"GJnͺ3œ6…KU€MΕ‰ž…’Κ$‚iaΐ“xΚ[~ρ3€ςSζμΘΐΟσ₯fH5b†Όωϋ¬iKέΗΟη-7ήu9δ©–ΩYΥs¨}%ΔΘΗΊ©M‹Υνη-ŸΒ*“α\αAYΒΦΕuPαš,μ¦.tgS½wΟTάHGVά―nίDͺEΝ&λ°+‹¦_t{Oh8=•=c?eJε#ρ&?–EršqΏ-¦Ÿ©Εo«Qτ.•.ΈΡFκΌK›vA΅lωΛ[Μ-hά,m_€ά­εΛL.Ήό> /A2 << /CA .7 /Type /ExtGState /ca 1 >> /A3 << /CA 1 /Type /ExtGState /ca 1 >> /A4 << /CA .85 /Type /ExtGState /ca .85 >> /A5 << /CA .7 /Type /ExtGState /ca .7 >> >> /Font << /F1 668 0 R /F2 669 0 R /F3 670 0 R >> /Pattern << >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << >> >> /Subtype /Form /Type /XObject /Length 5487 >> +stream +xœΥ]ί―·qξσώϋRΰκΑ 9δ£έ΄*Œ’c}pς`(J"C²YMΠώυύ†ηž³3<»Χ²½{kθΜξ’œoΘασ-χωo^ύΝ«ΧΏ{ρΕό/_OΟ—_―~œΒόώώ<ϋω;όύcσ όύyςψυn +­η#S‹ψύΦόNΡ»˜ €^ύϋ/Στ§ιωη(βG<ρbšZuδ[¬4—β*y +gjq 9)ρ[-Ž΅BŽδK!Fά+ϋΫ|Ύš2•p) 1Ο”ƒΛ₯ΦJ x‘`6τ”0ξίΛw}Ωƒ2elφύύα=†6|ŽŒlΟaδ|χαΝίϋvς‰θ—<ί}ύζžΝ^€Άšcσΐβξυ³ιӞ.€ΔzΖB'<Ρ+Ρ§Ύ8…’:yx0ίIρ,«Ž˜ KΞX"Δκ‚-h‡%^ΖΪB€Ήa0΅T™S(΅o(ώΧΌρΌ‘xΏeΉΎU9f ’Ο.Ω?ΐ$ŠΊΡ}ίΒh>mηώηWϋnζ¦ΣŒσe‡ όΨΤγPL­}/'Ι†dA‘=…W(Μ‘τ8>ςg"t’‚¬¦0CRΐ„4SΔR ΧL¬χ(FζΝWIŠ€Π›dν‰)ΑηC=ΔωxΔ{Ω9Ž+Ι Šw>δ€E²π qNMž‹’‡ιΉyŠu£«Γ˜H(ΛΪ&ΉH‚¦ΥW@6gτγ‚˜a&iά·YΆ Ά0KχzεK†A¦ϋ,¦yΫΣs^βΩ.‹ _rΧs/,6ΛβΔ“‡2”%ΡΓ,IBΑ+$¬J€θUP―½δΗlr‡ŒΨΗLjh©Qߟ†]Š$Ά„ΟαeIΝ•‹o<f~™ϋDY‚‰¦X'Y(2U0–gΛnφέ―?|xύώάσξΩ9šυγοY?bηcf둃2H¨ΊH]ŠΈζκLηλ2&Ϊ/LaL½ύΏ4‡‰a‡@R&J3‹,βέs˜˜ƒaΔeΑ­πužά„"ΐΝC(ρή†(’ςΔƒEΨyrdΎ +±€Έ4²JΌ7² Y‘’€Σ#μ<9°•²μYX/Β½AΕZ Σ#yΩΈŽ|[ηΙQm²©˜}aK Tβέ ”ΎIœχΘqΝQt'G6 [: Λ(3ϊ"ί[Dφ…° +¦S΄χxŒ&€#A!*ΛνΧ3vžά|K,e(;Kδ52vTπ₯ ,*Tψ+κΝηP¬Π9vΐ…¦ή½>žΗ€iΫI|?hx %cΥ‰eœ„α+”„ΕBŽξ΄ΚcΨO‰ΓχaKΙ.΄ΐcβΊΤ€faύ2$΅\'¨u9«‰λ½0ΩktκΪΧxZUΪ#`O1§00΄άh―Κ9TϋGΨ,²Kˍ(XLΨbΡ+\I‹‰–LT9Gcrτ(ΑOΩ’ŽbwKeΙΗ`~‘½KΥrwdyo£`ϊΚΫr•ΦΤΕl‰u­ͺ•›ς₯ιΜ+•j±i£*e5—z“#€‹πΟ­ –C0aш +XΊJœiO=ΐ0.†Πp쁾`Ξ=ΠE­¦ΖwfΧνUtFΕ‘'-ςb7Ηr…ΐ"·¨‚Žΰv7ag†s'ω=ΐ"[9½•F\Τ Œ*κp`aΘ4‡•<ϋgoβp„Ή S›έΙ]₯<΅\eNM9›rS±jκφ]R\―8n΄S³š³½Ω!›#~Ή2!χέ―\LΈ\L…ι ϋΊbΫε‚5ΦR¦άV½4vϋ‚±ΦFΝq«©ͺ cΝψ(Ž+‘³¦Œ‹ήšΘWΎ2ξra°‘ΌKε9Σ¬—’6εΆκ₯±ΫŒ7jŽ[MUnάƒ―ΌΈOΧ=‘NΔ =θ,7™ψD«ςΫφOA^Qζ°r΄LΉΥ2―Κ?Εαδ,WΨ«'΄κω,ΆšσšψSμΪXu†*™YΝ$”₯WQΔp:Cˆ‘TΖΐ¦AŽQŽ₯΄ςρr" ΌΆκ‡#!t½:­Œ9ΏΈW†ϋ1Φ +ΙAΖCω)8Βz0Œν‘Υ4<ίFΖ&‡ΰ©ΥM±Ρr +ϊ]Ρ0—lΣ` Iΐdg£1ΰŠSζGpΟΥΆh’cτ΅Ž·™Ήeηa°…ΐ|lB€ZƒΌ=}¬1c’—i΄€ϋ +c£~e«Μƒςs°>-1rΙεPlaQΨσ€ωR­΅Q†„εOοgΜAzl‹‡KΛ=alb‹–ZΛ‡~|$c3O·•v“έ›Υή»νή°γσ1ωΐq“Θ3¨η'©@_ϊƒ"gh9g‡‹œF€ΨANοΊ’c5γΟ…ͺ`€Š ‘冑a/\8ΊNΕ―ΠM4$ ­“‘itο}bZΔΌ.NΊό傦kΉBa©T#Ά†ϋ.€₯hf!ώνΘΪθΓ’’œ†xυμ‰Ώ6ΖΎ;ρλd ψ{α+fa¦6FsΌχΕ7MΗώ΄‹M…Π|ŒhζlΆŒŒ GHxNύŒEɐyΜ•*;3 )AB ͺ^N…XX˜―""…εψMːWΟα²pIΣ2z(xd,/΄ T)O’ΌfxB…ΏG cyPͺbjΕ#E3&νTΌμΆ]˜/Ζ?ε€MΝΨ@ψ)Ή]aYγΊ9#Rψ™γŒ3n΅Ÿ$%ί‘1δ %·δ UΠ9γr€V' =bV#gažvβ£™x.βέ³R9IχFO.αΑΝP3žήEΞΔIύ`Sc %ίέE˜ JͺΓϊ0jΖΣ£+οΜΔ*.Η’«δ»£‹E…Ππƒ|°¦HΝxzt‘—Γΐ–5šAWΙχG·b&γΥd‡Q3n]hΪςtJΎg¦J•Pΐ―»z†tcθ’lΩμιοИω~‘οŽnΓB G9ƒψQ©ˆΧͺ’r0Τ―§f<½U e©*4»’fθθΜP3t<7R3τC†š‘Z£fμ/ fδdS‡°:°Τ ¬p*Η"lόKͺC˜;[Ԍύ4y„q`±#€ +›g—˜ +νΔΊ.ςHLΠμχ8TQλ ψ½€ΩwΆ ’Ηλί9QN©}ΑB Š:‚ΗΨϊ”c( +Φ y€νΘΉ0pΒ‰Ή`QEΜρƒ†{΄–αSA3!\Γ‘ζ‚Iœ"μ•œmg­l^Π)X]¦άT­»}A—T6j.[MU­'otΰΛΑά ³:]™±ab-ύ3ƒ— ‚&o₯ΎΗ·yΑšk)iSn«^»}A—Δ5σVSUAǚρ1œ—œΉ‚ °ΣWŽ€‘VΗΡΈϊ‚†Β\9{½>pAC¬KΪ”›ͺUc·/h#Άυš΅ά6UtΈq&h .wΕyάδο›ΏΘMΏΕUωmϋ§Π’œβOˆ3-γEn΅€Uω§8|ƒύ‘°)ƒκω,ΆšσšψSμΪ¨ςYš({σ&έΜYΎεΞΙ…!§-ΈBErƒΌ:Ι!₯Δε(€FΐR«Ι6sιGΦωXΗΫ«Ϊκΐ—­+\†ζd, »ΕΈΕˆf#°AwΚ~4†|₯«Ύ+ΤRͺŒ%τ —lT‰rκΨ֏rόοR―5G? +°ΠpυB@ WΦΖ’;‹†Wνι™΄<|ωβSτςyΘr:ΗΉ°ς8δaΘΘ†+ju8΅%ΘqM˜±γύΩ5)f`h¨z‘δH 9α² ¨[–7lρQή6―T―šΣ„t#ομ*‚ΖVn+υ&{8«yΌwΫyΌaίηcr‚γV‘!hKΡ±o»νIΠ`ωjh )ΛfΫAγίψπΧ> $δI£ω(§OΩB1$Ϊΰ%ŽΟ†€QΕ—‡H₯’F-,_d–/vY’¦'9s΄+‘IU>α Hϋη+’†œ’^ 4"C¨ΒχFQσ@ΐ\/Gά7 –F•ο!e>}s‘iTa³9;b iΤ,ܐ”όΘΣ½qΧ xrfG‘ΟOWΝӐΣ9Rfω·εi¬ƒό€< Ρ8%β,ί2ωhšΖWΣρΪW +endstream +endobj +587 0 obj +<< /BBox [ 0 0 539.2639 206.37436 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/main_eval/accuracy_comparison.pdf) /PTEX.InfoDict 671 0 R /PTEX.PageNumber 1 /Resources << /ExtGState << /A1 << /CA 0 /Type /ExtGState /ca 1 >> /A2 << /CA 1 /Type /ExtGState /ca 1 >> >> /Font << /F1 672 0 R >> /Pattern << /H1 673 0 R /H2 674 0 R /H3 675 0 R >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << >> >> /Subtype /Form /Type /XObject /Length 3039 >> +stream +xœΝZKs·Ύο―˜KͺΘƒ@tΗQrlΊ\εD²Y•CœƒŠ¦e©–’CΙvςοσ5f9fwΈ‘ͺπ χ۝~ύψƒ³Ώ^ύρφςκ‡σΓW?nΞκ§Λήαί›Αοπορ†sό{³±ψt½— —ŸΆΝ'ΆΑΈθdΐΆψλfσΛζμ9„|ΔS盍sƏO‘ ‘όκzCžMp19ΧΐΫ¦`‘m‡O2:΄¨ϊχ°ϋ’rdŸ'q)E—²Ωώ g δ’ ΓΝΥπαύpφœΥH22όΉ±ΖΕΰ£…θσαμελOŸnήc†³oiψxωžΐMΛήΣπ,‰!Βb]%˜C΄άΒΫ†ΒΙ‡βΙ$£EαΙ‹Ν«αΡ|q)±ΞPcMd’GΡδ"‹Ο0λ‡Ή›<Ί)ΞD˜Rοfˆ&z“{7+μ!8Δ+·2ZτQέhξ(Ηδ]ρoί77ϊΙ°K6ϋή7όΝAϋVα#\HVΡ*£E{ ‡cΆ&₯μςlŸΘz“π'μœiπŒJQα*₯EŸT@’MΖIdžν1ηΘ§ΩΆ5xφaKm©Rτ’Δb‹•™s¨Ά1gG4sβk²Δ]ιœΔtπδίΞN‹bOZμM‚½(ЊlPU#ž@΄EΊ’” έV]Θ#μι‹‹ΝΩ7° K($’†‹_6e™,₯œ aU£Θ.~ήόs8ωϊ?―/?Ο8ΒΜ”αήΒ³“ο_ΊόυtψΧpρέζλ‹M1pC$Ζy―)Ψ΅ +―0­D@Hl°@條|C§ΓΕ»ͺ|κ:Ψo)yUžgΰ\qΖ/Π€¬Ν;½ )&’­'Ξƒσ&3*cа§θe» 7dΨslΥNΨ:­ΡA U+Η8*•₯dK‹΅™Z΅ ΊNqHqxe$BΡμ–ά%GΨKΛδ:Ν]§™ŠΘ +²";μ&§—bΔ";ž!sρ4jHF₯‚ΐ0ˆŸΠΡlΆθiΧ… ρόόςςχ›S΅%'‡ί;ΗΓΙλΛρŸNώςΣi qΎ‰WΙŞ˜dM‡iΠζΗΓtκz‘Nα‰5¬¬ύy#ηNωΆψ6Φ*•ζΝνš'μž ++£KŽ„ žS@‹œv‘€ή' •W7@;gΫ6!l,Φ]ΐ¦’Όΐ*‡Ώ΄ƒξ2Μ€#"Wb‘‘V+φ4漐 :ŠfΘγνDPεtxιk]ο½e’}kΒΚ y-6εΎνpμE˜πFN‹«ϊ°?oCΏ%%ήΉG!ηF9ϊη4/Ύ―•zZαΓŸΙͺ6e-˜ΚcrθέZΰψΆΓ[Ÿ[9Z F5I*(φΧΕφε«eυ«ΧΏέ\}όxυs­­ύRΡ|±ξΑΛς&ΖD’ϋυŒψΆΓΫuhετΚ+ΌRy²ŒΐΚ$η<*—eΟΙκΔβ#Φ¨Σήΰ+Υ“ϋH‚NC£ώeη‘τ(ΘleΆο ΎV??”Ώ”1œζρθ$άα?Š™€μμlλ|­~±Ζ’ά†gw1ΨH9<ηΤXnNΤΎ““w σ³s>GˆΙμ{6°maέhc9Cm„΄πD<˝ΡHGbbŠϊΆͺσ”3uη–ΫU‡ŒξO"Zψ‘ύό¬£η ςJ_PuΞ!₯a₯RΒΞ» +ΓŸ…έψN|Ύχx/.Œρ± Z;Hς”¨#‚ΫE9Β°ŠέαΙND ?±˜•WΒ>λvN-φzΩ;Zα€νΰ*€…ŸBT¦h’/wϊj‰‘“ΚE†ΎZN°:J$;―'!-|Ώ©Ζq9αέ € +―xT +>;Άξαc ζΔ%υΙyέΒ+l+A[™6 L+yžΓΗψ%ˆ₯)« v€VpΡmU£Μ ¨χTWx₯j/˜€[ΥΌ¨«Lv_u…WͺέiUϋEΥεήολnπ΅KŽΤAΡi΄‡e펍ψΪ+ΎV;§Β±ͺφ΄¬½γθφŽ£ΡnQΩρ,8’WΟFνdο3Ν,fΐan†Ήα Λ»^dy:¬’‹ύέJέ‘cΕ4Γ9±Ι ί:‰`šΡ™ƒ8οή'{„Ό; +ˆΗJΊΓ(ΔZ­ΉΏ~‹f”18 εHy_ey•Œ;zΌ +“u ‰ϊ)¦*B\$ƒ¦ΗΠ‹υͺΎ%ΏJ„ΎΝ©ΌM•Ρ +ݝ"φΖ%š™”Qyθ­θωKNŽΏ‚Α0ΞfυX°5Ρ—l³χςΫνΒ©‘qx0™~Πzό—€€ γ8·“ΑΘ‰š ήι}Ηr”_ρm‹£ ˆή‘ Ε£IL Oμα±z τ%,‡ρΆ`λ«ή—ŠV™]οkŝ†ΰT–`ӏμληAΌS9ͺοŒIo>Ž`[+(¦ςZ’ӏΎ™χˆN0νL2^lΚQ/?–ΣόΞ‘Š«>FNΕ£*§ΓŸV| +θ…KnΌ9Ψx+ziN’Dξ½mπΜϊ"ΐpΣΒO >EοyΙψΊ·sœ(ρψΆ·s°βz7R«Ή+—”«œΏί°βυŽsrσ!ΎW ^o(”%½Ώζ<¬ˆήtŒv>Κ5π +ΫJ dds‡Ψ€φ†•¦-iί²zοPF{η>Ζ½ZC¨Ρ…μl$}i0ž…“,*/ΓA ε-L£ΌΒ+•—™!ι i»; +ž_άl2ό‚b―ΌΒ+•λW+ _ΔΘΑœ3:@HΜ@3υ_«Ό;.Hw½£ΥίγΌ‹Έ/%ξŠ|ˆ]/q %ϊk¨TϋŽ΅/kXAΪ=λ½9 8‚dΉAΒι>˜λξŸ^U8ΒT½Χ½ΞΞˆjΦ‘!Ŝ8z?2UςG―3z΄\α¨ϋΊ: g½ΓΞΛΎt܎„Ά γ#˜IΨeΓ€“,Ϋ]"F?<&SL€5:J²^"CqΟ((;tT†r–¦χ:z£3E„AŽψφjεό…Κyχ S{χo?|ϊνΓ§Jΰ_mώΡ ; +endstream +endobj +588 0 obj +<< /BBox [ 0 0 327.86253 142.22867 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/main_eval/latency_speedup.pdf) /PTEX.InfoDict 676 0 R /PTEX.PageNumber 1 /Resources << /ExtGState << /A1 << /CA 0 /Type /ExtGState /ca 1 >> /A2 << /CA 1 /Type /ExtGState /ca 1 >> >> /Font << /F1 677 0 R >> /Pattern << /H1 678 0 R /H2 679 0 R /H3 680 0 R >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << >> >> /Subtype /Form /Type /XObject /Length 1454 >> +stream +xœΝXMo9 ½λWθcQE€(J:6ϋ‘’‡E³5°‡EAκ¦-܏m²νίί§±=#Εv€EΣCΜσΜ£H‘O€Ž[|~s±ψλτΔώϊάOOW†μ[ό]Zoίβο‹%{ŠΏKγρτΞN.+GΖΣ²y"aǜ5 `ί?Ύ6ζ•9~ ’+|ujŒΣα+N½Τ·*³ίB— J^]5<2tθΪ― ]bΩpΑe8³1™Ο1SμL7¨w΄1mN‡/ζdnŽ [\QVρΎ΅σW&‰S +ΕΙEπkτbPŠΑΞ_š£?ΟfvώΦό>7Γ Iv΄άpΊ…omšΈ8Ξ)G"ο·mcζ3K„Ο +^I”S΄GŸfφ‰+9'Ÿ₯(Ω£7ŸίœŸ=žΩvώtZ)γ-ΚΩηά­΄…oΏR\*Μ1‹'ڎι³jΏgο8ΰι­7π­­Γ¬ ͺ1¦Δ^ρs`*YCΙi°ώδΓυΗΧ7W°39%:ΪΚΝή:‘+1’fD~ež\l€‘`±A"~ΕZΚ`ή»ΈΗ² §·,Oΰ-£₯·LΞﱬΕε-ΛxGΛλSoyŸΟ…]Ω²<w΄œ³Σή2O>o(<(2ψΙ―( [¨θEΗEΌ&>8Ž>y%bηο%¬”Ο?./ϋΈ‘eϋt%šƒβτ’Ί;©vŠ˜yΎS +ίν“BΌ=mίnhΎΖώ―]A%ωF9CFi[NΕζ€ύ²Ÿφoϋ!xZΟ ‹ύ¨Βλ€’|,ζΤ?;ΏΎ^|z˜Ψγ'd―.ή₯!!dΌ7ΛO(σ’ +ΥΑeΆΥ0}ί’―kƜΩoY=r4δΜ!r‘ΊŽΔ„œR†žβBβ(Ε³ύλ¦cΌr gΕ’°Ρ8VΌƒVΕo›ΐ€NFwΗο[τ;8fι¦kœ²„Α£moΒΚ›\IΡsλ ͺΔeN\:o4+ΦH¨Θετ}ƒ}ŸMšRΜξI1Bv‡ΛkWΤ±€$©[ύ„ΆωΤPόdiFβ«^Υώ₯u.ͺΓi[μΦΉ …:’ 7—-E ?Tͺ‘’m‚v₯θΠφ—Ξ£ MU©c†β/[Šώa ‡$ιυ mŒG[Œ,‡Φƒνn’ψΙέ”Ch; ƒs̎Πψ³tΞM¨²«ϋ”γΰσHΡΒ•pΜ(‹Τλ<Β* +—¨±σhB‘Yh‰±ƒ£#E ¨„ΓΙν€τZ-pτΎ“η lσͺ!ψΩ-±ΓώuJVGŒNZ‡‰Ξ· ΡIbΈTα‰’…,έΠΨχJ†Μ'Μgθ4»έjPœ  ­|BΦP΄πθ‘ΊΡΥ LΓ|ΉoNΒΖΗ€%‘ΡFͺ©ͺη”ΖqαfοΌ5 #Ό…EO¬ΓZ(Hβ†BεόaŸ0­F¨ Œ– hπρϋš"λ +[N#AέΟ +ͺ‹Q ;¨(el„‚΄α>āΑΓg|3&έ”œf,Š1#¦CY:?ΝΘ€ϋωΒ^PȌ”0τZM8γQv”yά–q0ΊkΩ―χl+.ˆVAŒcUΔ{ ]6hπ:1΄θ°š^?NΏͺ!:°¬t4;]υœœ_-6‘cΊvσž§ƒ"€.©”„AΏA‘π²…[w’o‰yΏζΑΎιŽ0ΤΛΉU$!ω‘λσlByΪ£_v`;ξρΎ|x΄\ΜL¨h@ξώΩ£Ο3‹γζh±œΩfu›Χ™n†ω6]8N½bŽΤ‡– ΞΫψ²ΓΫ(Ά<ίέ’V<ιρ0Α½ρΙωυΕλi#ΞΜ­ΪάΝ +endstream +endobj +589 0 obj +<< /BBox [ 0 0 348.90483 183.81324 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/main_eval/H_hnsw_recall_comparison.pdf) /PTEX.InfoDict 681 0 R /PTEX.PageNumber 1 /Resources << /ExtGState << /A1 << /CA 0 /Type /ExtGState /ca 1 >> /A2 << /CA 1 /Type /ExtGState /ca 1 >> /A3 << /CA .501961 /Type /ExtGState /ca 1 >> >> /Font << /F1 682 0 R /F2 683 0 R /F3 684 0 R /F4 685 0 R /F5 686 0 R >> /Pattern << >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << /M0 687 0 R /M1 688 0 R /M2 689 0 R /M3 690 0 R >> >> /Subtype /Form /Type /XObject /Length 2108 >> +stream +xœ½YΛn\Η²μ―θM€αΒΝξͺ~f'E‘r"‰€v9¦$ ؒμ|~Nυ»έn?S*px΄Ρ" ωtίΩζ<Ψyξ; ”Δ$hίΤαWζ•ύ:Π0π2ΩονήmέFE\I8SΒG£C(πΰ`Β!ΑχΘ—JRq΅yί³HΑ‘TΨνh„C€χ=ώΣΡ£πMOΤπnψaψζδ[Gρ°5+δD›X„£CŽ•\R)™{ΞtΛΑ'Κ…ο;›QB2{<'Νψ›™ΒI|„²u’8tΖζιJqRφΚC@°½°js>Eδa„O»u4Β;E#Σ&ιxΚ7—SIŒΤLA^Gb]tqλΊ;»*DEΉFυ ED)ά΄ σΒ7O/ΜωsΘw»θ}kΩ^όbW*μdŸ =—₯¨4–ώqqeΝωΜ^|0»0]ε}%XOWr+5ε\ηӞ`•πJ²¦ž*MΒU`u4—=΄)ς‘*ε-J\»Ήa/Δ;Α΅Β΅HBOΤ‡ ‰2όL(¨].“‚Δ7W„C Vδ‡I¨˜! ΉUξ²ω¨μHh#λθmE~ lR +b]ω΅έρpI…ϊNJUˆζ@Έ›ΧΥ ϊ +J³7}ό?AΫ·WΛO3`ϋ|;C4ooώύεσςΰ7³ψyΰπό9ŸͺX@&Η–0σεŠ4iΆψS·τόy<Š1J}γˆv¦YΏα•lrhtθϋ 8ς +hD˜Η +D­'*•Έ yΕ «“]ίΗyŒ ΙΔR³ϊωlLi΅ΕΖζ9»UΩΠ;°„τNKΫNzΊ6†1άTfx&€Τ'ΤφΞtQ>iϊj —μςv=Rl4ω`#φkο!h‚y+RFδhγΙ+#]{†όΗnZΉœ•ΞHՁMΖ\Κ\ Dδq*© ˜RP \JUϊπ₯ΑΚ%3&-Τ;Τy€8ΛΆ†’B™PΜqςCg°#‡Ύ§ωjΧ‚”Ν)byS1$ΰOLΪ‚Œb_(³Jά™₯β•XΊˆ`&£f$jŠ)†–Ё„QΎΜ2ΐ|tε2c•&*(ςδ-Εuς«@Η@)«β5™₯";‡‚Λ!Y|ΎXO›²Β[ε³ϋ%JeάβσΛέ ώ9ƒΪ/ί<YΌ\~~w{5ΓδΑ°5°ms¨}μ; Vtˆ₯±_!^3τθu–ώΗΎ‰ ΪE₯΅Ο;ϊTΓDλ8K~ˆϊ€8ΌυH{ύφγΥνΝΙZŠωκΫ1 <κύDE—: +]Τ4PŽ»oύ΄2ͺΗςOΏQ;‰NIΏ§φt5hΙύEˆγαhθ_5ρ}gπœ"Wƒό=‡I@끽&؞οƒa­ΰR/dΒ’ Ϋ€πΒΘE!ΐήΡZs΅Gdυ{+Α—π$\" ˜ΦΏ ΏΟ𝀭@Mg{X§ΰΕ-ϋρ•w'u₯DͺӝπΜ#„κ2Ύ θ[“ °š +φ¨μΣ@…V˜8€wuS™ύΔ†ηΈοd +†γαα€ώιΊ‘0j9I^ b§σs+Έ0*€dΙRΓύŒIuνΡΑΞ #\Uδ•&·δ|„‡»»'&š|B=x·„…X’Θΐ€Y3 W+–Ϋ"―-Wfr•³}Ψa{kΨΖ( ΐήΜ2­J4Ή7υ#Κφ8M_ tΔΉ 8–> +>šώΓ8L,¬0υΘ‹ΖεΡΫczξH¨s;Λ^™!9Ϋ +endstream +endobj +590 0 obj +<< /BBox [ 0 0 449.05213 244.08 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/main_eval/degree_distribution.pdf) /PTEX.InfoDict 691 0 R /PTEX.PageNumber 1 /Resources << /ExtGState << /A1 << /CA 0 /Type /ExtGState /ca 1 >> /A2 << /CA .85 /Type /ExtGState /ca .85 >> /A3 << /CA 1 /Type /ExtGState /ca 1 >> /A4 << /CA .6 /Type /ExtGState /ca .6 >> >> /Font << /F1 692 0 R /F2 693 0 R /F3 694 0 R >> /Pattern << >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << >> >> /Subtype /Form /Type /XObject /Length 7294 >> +stream +xœΥ]ˎ%7rέί―ΈΛΦBlΎήI #{F ΜΒπB΅4Ί$X²f~ί'2«Θΰ#JΥ’tΪe§΄ΣA{’7(λmIƒαέ3α^jόρώΫθq£ό?ΣcMrdε²ρ&Ο΄«ΕjΤ‘Rr4\¦έH πΓ- ψΔIŠf±Ϊ(§ iPˆΑϋh!1†^՝ρ³ΥB€uΙ%εI’ +o€…ΐZ$•JLq~›…H“ΡLNUx#-Ζ·¨Uqθ&)ͺα(~cN“@ήG /„@›ŠyV’Z¬Φ*Ÿ‚ιbθFJ„!ERΦX£ηv³Yˆ΄σ±θ0)T፴?ܐta’YL)*o:Έ[θΰ”ΟEk7λP-DƒΛ€‘η(O…7B` -’Š6ω07šΝ‚Γp`‰Ό‘chQTΒ‹¬LZT‹)Ie³$bπFZŒnΩͺ’½σσŒ£Yˆ΄v.ƒ©^’ +ο£…ΔZD₯½Men7›χUVηG‰Ό‘chQ”ΙΖϊΉνlS‚Bš(ƒ7B`όp+V9€+sΫΩ,¦xlΆΞ1x-$ΖΠ"(²qsΫΩ,¦8}¦yX/ƒ7B` -Π/΄…sΫΩ,ό1υ(.1x#-Ζ7ƒ©UΒΰΙ͍'3τ9•A$șδπ`δ|žΫOf2σ±β£+£L ίI4ι‘•6hζ6”™ˆ7šΝPΚ¬SΕwC =ŒQeζv”™ :‹‰idbπFjH”I ―lΖ•sKΚL&Σ§ΞTŽΎ΄S‰α;ι!&=’ΒXΚ¦EcΪL&'ώejπNj”‘†₯«ΑhΡ–6“Α4FUbψFzH€I§(;iΡ–6“ΙAεˆ?Γ¨ΓwC MzD•½fΡ–6ρΦΑD;Λτ@™Τ(ͺd£γ’-m&“Ρ^$κ^G•Ύ“iθαœB·Μ’-m&“ιs— Ŏ:1|#=$€GT6€­i3™Œω‰΅υƒλΔπτH“EΉ‚qњ6¦σͺD 8Μ¨ΓwC =ω€QwX΄¦ΝDΌ­sΖΨY§Šo€‡Dšτ@n“szў6“Ιτ€œKubψNz€I¬2ΖW‹Υ%f2©¨μ²)£NίI4τh‚Ά‹U&f"ήxR1 *Ύ‘iͺœγb₯‰™Lʊ–”Žο_N ίI4ι‘:L½Xmb&“FεwubψNz€‘G4΄:+NΜDΌ­ΣεQN§Šo€‡DšτπHŠΌ,ΪΣf2)ͺPt9φvu:1|'=€GRΡ[ΏXyb&β­mΡη§±^§Šο€‡@z C)λΌX}b&t&* ΤΣ¨Γ7C"Mz8ULqσ¦ΐf1Ι«‚VB»Q%†ο€Ζš2‰‘”)εy3mW΄Ι³JήI 3δΐh ­›w1)]LΗŽN&†o€‡DšτpΚ9σβei&βν’±9Μ:U|'=€GT>9γζ]΄Μd’UΩ£7q£N ίI4ιQ[³Ψ?Ϊ,Δ3{­Γ¬RΕwRcMbΠ’|ΠΪ.Τh&“hβ‚·ƒJ ήH ‰3ΙTΞΩ§E[ΪLΔότΉ‚έΛTρτH“#ΛXμ’-m&βm0}-nΦ©β;ι!~ΈYΜΗΰά–2Σ1ψ*&[7κΔπ}τI“A΄’γΙB¬Qβ1;TͺψNj¬)“ΉCΎηΆ”™ˆΆqэf―R…wRCΰ 9 ZΓ¬“™ΫRf2±¨l³΅a‰γι!‘&=ΌJΘcœΫRf"ήΓρ”f*Ύ“i#)d cν…ΥDΌ}Φ1ΩY§Šο€‡@zXΪ‡yšε¨ΖΊcό9¨TρΤ(“Ν‘zΡ–6ю‹š4©TαΤ8“IΩdKXUŽj:ϊcΛƒLίI4©Χ4σzΡ–6Σ1ώ,!»4κΔπτHw>f=šϋŒ*QŠΥnΠ‰γUΧΉΣ΅ΑΧws~fψώ΅δι€Γ¦9:Έ}|ϋψΝνυοΝέΠ|1˜˜ Xήί|usE™”΅MΗ”š–ΤCΎ9Ρωš7_ή^ιξoΎ½ύΛ›nc,f‡Ώμ‘Η€\ϋbQΚκ‘Ά=•}TξžΚρ—=•Φ/R.¨C˜1.λλcyΩτ$ώ²ΗR3Bφxω΅Y>6ΦΗςςKͺ@$Tξ‡›Λ ψyν―w¨Ί‰tοQτ¨dΗ3ΝωΜΧΏ·wά1i΄N:•t€…L:”Œκ‹)΅WΞαeˆ1Ή#aXδ5ge‹Ρ9φymπϋδ―‚v`θ ΖβΏ.―qΞ«₯§e­“οςΚΰχΘ+†ΌCΜ(1ΫώUyM«:i…š»ΎTψΘ+Kι«²ΦeHΠΰ1}MΣΖ9Σ'hπ” ’‘Κ΅¨OPα)οtκqƒΗΩPkιl*S…§h^Ρ“$7$¨π” ΠΑλ)K žDΌt:Δρ ”DŠΞC‚ hRžŠv&χ΅²ΑSԁœτxύ:]ŽF/UJΓυPΝG/fΒπšTxJΰPέι-θ―B§ΛΓqκdΜPƒ§γυGώ–|z?=9Γςβ„6]0€8|δ`Œ‡Φοq6ΡBŽΧα(ΛhC8Ώι²ϋpόΡε“8.zͺ^ΌiQ_όΩφΌ«t?όsΗ‹‚©ό1΄sG'LeώΡwtΞ₯K;³Qi_ύνλEaξ-PΊŽ’Ωc*όκ‡·oιƒΫ‡ŠZA까ΉΏ2uβvΦ ©n’M°-CΙ)€»uζQk㽬‹Fυ(eŒS“fχWί|ύΝw_Ό[δξ_?ϋόΟ­&>zΗβ·jY%ϊgŒ΅ΝP!Q%’K‘˜Ύ²0Ό«μ>³Ώ,ΗΆ ŒqζW μρ ++:ο‚Επ?,Σq+žFσΟ_B€QDhρ[P-ϊUΜτjώίL‡³ +γΓ Χ›qψ’ΔvDyΧΑιy―3‘?†S:ΌΨt‚0ό’Τ~ Q§=κτ’Μ›‰ +·DΠ1£$ Ώ&u‰QGΎrYνπa¦£φA—άKα₯.π#κΖ;ŒωW₯^MΗΰΒ§x8yγ’pό’Τ~ N+€ΙϋΥ‚Q3Ρ8ŸΐΉ" Ύ&q‰Η43‘4oe&4ζ˜*a†ηA8~Qκ?’ŽyG(z΅fΪL4Ω(ΪΪμFI~Qκ?PΟϊ˜Ι.Ž₯1΅γτMηΨΞΚξΦαΧ€.ρ#κσYΎϋ˜‰6 {©˜Q†_”Ίΐ¨GL ι+Ϋ‚z5ΡΎ‘ έωͺsE|Qβ;"NώΓ²]}…n&Ϊ?žfT\Ž_”ΊΐΤQl†fA‹2o&ϊf“fga”„αΧ€.ρ#κV—²_”z3k ©œσn. Γ/J]ΰGΤ3ϊε€kΤ«‰>bζoΞ© —„α₯.π{Έaς₯JAΆζRg&ϊЊ7:^*Έ$Ώ$u‘QΗΌΫΡ‡Ευj’=ΦΑX \:I~Qκ?’މwƘlUκΥtœΏp±ΨW¬:I~Qκ?’Ž™7Z*½*υj’½6:|‰α’pό’Τ~ n1υ.Ξ/B0-l‡τ0IΒπkR—ψuL½]HeQκΝdbTž—„α₯.π#κ˜zΣ‘ΉU©W“‰Τ‰…cοP§Hƒ/J\`β΄­Δ.·q38υΙ7κΡΰk—Ψq:μeγβ«3™θTΤζά¨ή Βπ‹Rψυ¨œΖ;»*σj2ρπ|SΟ&$ Ώ(uQ/τέ.ΎΚ1νΧ*΄i)Ž’0ό’Τ~ NλνΉ„ΕW9f"Š΄-#,$©ψ5©Kόˆ:&ήθ₯_ε˜ o3Ψαφ½“„α₯.π#κ˜x£‰^|—c&’ˆΦ¬§΅I*~Qκ?PG6‘yΏψ.ΗL&d<μ Η―I]βGΤ1ρ9-ΎΛ1Ή)ΦdFI~Qκ?’N{γπάU©WQτΩymgI*~Qκ?’މ·GW½*υj"ŠΖъς,IΕ/J]ΰκ4(Ιh­₯ήL΄‡9»€FI~Mκ?’މ·Ε/Ξ‡1QL.fI*~Qκ?’ŽΉwIfα{™ˆ"rμB’Š_”ΊΐΆ^Η₯\Y”z3E4ιΩΖY’Š_“ΊΔ¨; +y‰:» ^MDQ]Žcƒ$Ώ(uQGΉYΠX•z5c:;’<α₯.π#κτ ΪED f’άgΪ=FI~Qκ?P/VYL,J½™(χ)ισ,IΕ―I]βGΤ…yΘiQκΝtRŒΩQ’x偬ď¨gZ,4 Μtδžme–€β₯.π{ΈyŠΣκ‚_ψ(g¦ξLΔ(Ι~Iκ"?’N΅θ Ξ‚z5ΫδgEžΰ‹ΨρHG8tX•y5 ­u‡OΊQ'ό’Τ~DSοh_•y5Ή/δ»`–€β₯.πucUΦ>.Ό½2ΣΡ{gwο$©ψ5©Kόˆ:ΡΒΈlQκΝDΉ/UάΞ’Tό’Τ~DyλV₯^M4\5:δ#ΆB' Γ/J]ΰG‡L1υv&,Ό 2Q΄ωΡ­σ IΕ―I]βGΤ1υN./β81Σq¨ήλΣλθ IΕ/J]ΰGΤi§#2Ή*υj’/pmΊ£$ Ώ(u¨Σ.ΓλΖL½™ˆb0Ι›FI*~Mκ?’ξ”Χ:-ΎΜ1Σαέ>Δ0)ΰ‹ΨρH‡8V‘Χ™ιXRΤޝ]„α₯.π#κ…NqΨΕw9f:Ά t\Ž_”Ίΐο"VΗPrςΟ:_rΖ+;tψ =>ιL*,’&Ϊ&ϊœχ%—’Κꌏεψ ‹‚Y“‘σς€ϋsή—όa,ftϊΤα/{,½ΰΐ‘/9/ΫΌ/uEΘά/YŸψρXž„{AβI7Hά]wFΔ’ŒήˆΊŒ1Ÿ@]Ζz§@< χ +Δ“ nxξˆ'uI˜g .Iο¨KΒ|uIzη@ΘΜ;P'rο¨KΒόuIzA]ζ!¨K»κ’0A]’ήIOΒ½ρ$ƒ› ^aΈŸ ^aGA]’ζ)¨KΡΉ +κ0_A]ŠήYPW™· χξ‚Ί$Ν_P—’sΤ%`ƒΊ½Λ .IσΤ₯θœ­υΌ ™Ÿ|ο,ύ¦<ˆ~SΘΓΟ{Ί`ι“t{žyΚs{œK4«Ρχ{ށQDyϋήaΗΉc~ŸίΒaέΟχΣΎ˜”Γžςσώzlf@Œρςμ°ηπΨσΣΚuΤΏ/όψότέ7ίΝyΎ½ϊΓΫωλχ_Άμ<ηΘ§Ήpςθ1„ ₯χΰΤΰή%ΉspζœVΏQ s>ένΡ_ίΛƁΘόmβ₯Σι&EKƏc¬ηΡ·> Ε«oΓΥ\€=ΒκνQƒ²ήž§,˜αέ3α^;Ι±ζ5(bΉρηtΉg]-…>π. ™HάGνΓ Mhΐ˜$ΖI‡f!/Eέ6§ΫΘ ‘… žΦκθ]Ÿd¨–BΞ͐‰ΓΠ}„θBˆ„ΡSLq~η›₯`NZŠ·‘—‡‘ϋ!Π}ΈEΜΡp 3ιP zFϋsO»G·‘A 0§N˜˜ΟΝd³€±ΖέtΔ©ΰ>2l‘ΖΙΖ=7“Ν‚ΙmHώˆ§ΒΔiΰ>2dn%%θpανdhšγϋ|ΜΧ;mžΐmdΘB +I_?>φ2TK¦Al +Η葉ΓΠ}„θBLElςan%›”Α$«F<έG.„(*αu?£νφBT •=Ζ ¦ŒςTt!Ί·LΌσσl’YΠ`–™θBL†n#„DBD₯½Men*›”SΤαΣΙSΡ}„θBˆ’L6Φύe³dΪPbό1Z`ς0t!Ί7̘œ)±Μe³€@NΪΣΙΓΡm„θBˆ@K¬Ζ͍e³ΘoΕρΉ΄έ‰£ο„j¨KνkΠ‹#ƒΨ BŽ­wx X° + ΒΘΰ>± 2?q€{dƒ_ψΘ!&ΘόΜeHώ3[…_ψΜ! ΘόΜU<rU_P}؊†ΎGΤ +lt¨΅x…rϊΝƒ$―l6Iχ"ϊΕ¬-^αRπNζ‘@¨~Ζ€γ’’ο³Du –J2ΏyO)-š·Ύμ+:„J}Ια”œ_ήΠρς¬cΎάΠΠαςH'ς§Θ /§Sμ|ϋΛ+:^žT@sνϋΐ //4ΡtyΘLE‡Λ“ΨqΜLCΗΛ-ή.C1ΊΛ+:\ž1Ξ·h±ϋ +ΥΠρς|œvuύέΠαrϊτH‡ +ϊbjθxy @Έtό¦»Ό’γ娀6—!LCΗΛΙο9 +pΘLEΗx(Uή†17 ž8ΪpHA:ϊώ%ρ>μΠ―!¦ηΐhλ^˜­³τ‘>„5œώϊχZςιγ‚<έη™»?αΓά-‘“Ύc5ί±ΎΪy4Έ‹οA…Ρίλμ.ώ‹γ{Ψ¬μq?ΟΧ"bˆ†>΅8χΉZˆN–PΠΌ»”ΠQ˜D2£-+4ή;+ίη_Ό[EςψΓ{‡ρθjZ[ζu―,·2ηλΠ‹υΏa'Ψ/vŸωž"νΩ„αi‹ » MŠ˜ ΆaχϊκΫpυνΨIe‚Λfž€0jγΐϋ9“tΓ­T‘ψ‘*^y§1ΆZ¨RMτ=Ζ bqx'5Κ€˜j…ω3¬Ρfϊ^$@™’c`Z«ιkΜ¬F3Ρ’0Ζ{Ω "1x#5$Κ€ζX>> Τ¨&:Y@'L"Ux'5Κ€FDεƒ^4™ΝTΌ9=:‰Α;©!P¦ΐ”!\ΎhE› ƒ‘dς9Qγ"1x#5$Κ€FηγšE+ΪLΰ€qZ²£H ήI 2©q*λ…ΥDŽω»SΕvA K:ΠΆΡlΛ’ύl&ZςŽzyΌ“e +Ё™IΒΔdΡ~6…δˆV?†£i"1x#5$Κ€}Ω&/ΪΟfʞ– Λ1ιη"1x'5Κ€FΖμΡE·h?›‰–ΜrŒ1 "1x'5Κg<]¬ΞσξfΚFΡ­ΟΐL$o€†Dω ρbQσ^=fJδKOΫc[»‡wRC |F}9|$Ξφ˜)α/ŸNχ\$@™βbtZ¬57K"Os1h7HΔΰ}΄“θ<ξZhQMΕX‰Ί“cƒόizgη”™’?¦¨eԈΑ;©!P¦“’t΅MinA™)‘ΕdυXώγ"1x#5$ΚέΉΡYjJFΡ§ϋΣ=/‰Α;©!P&5j{‰σΐ«YηhэšA"ο€Εš0…ΰΠ΄ mgD˜‰VXΓ;‰8Ί‘cΓ)ŸbXa¦˜ιγg>Γ0Ό“eR#ͺh‚^œa¦c­…A$@™Τ(h_a¦U@Hf‰Α;©!P¦ˆLjΕ6Ώf‘ΪΞ„ΣΟ=“ˆΑi!>γΊh§έβψ3EO‘kΞπΊL"†ξ€…ΐψŒτbbΞ‹3$Μi‚χƒBάI ξψ#J­g3Eό(hΫ ƒ7RC’|Ζ‚Aφ’^΄žΝ| ƒwRC |†‡‘ ‘σRA³ϊ L[U{‰8Ό“kΒgΈ˜dm(‹Φ³™BR9X1ͺD έH ‰1‰αι0€φ‹Φ³™Θ½Ά ΦΫA#@™ΤHͺ”B;Ύf5ͺ‰’ΘΈpNΰ™F έI 0Εά0 +Ɋ_΄ŸΝD>’Λ$bπFjH”I Ϊeg+šΕ>…μqvŸέŠΓ;i±&LR$εΟnΡ~6“Κ»”—TL"†ξ€…ΐψαπ9»Ε3yr£7ƒF ήH ‰2©αΘcsZ¬!1“Χ + +%"1x'5ʝ »YjΒt$›L[Š;‘8Ό“eRƒφξηΈXCͺš₯z6a +5t'%ΦtΟ(/F'½ZAj&›UΆ!ŸΦλ½8Ί‘γ3ξ‹υ!¬VšΙz₯αƒF ήI ς +όΚj©™LQθ5N’μfήI ς&XL/νg3ϊμά«›‰ΔΰΤ(Ÿcb€έ­ 5ͺ‰<ƒ„Rz‰Έ“έ3~L*Ε™Ε`œ™θhg +.φqx'5ΚgH™‚1υbi‘›’JΑšG5ΪΝΌ"eR#(’]|θβ&:έe£χ£JίI4ιA§Φ|\ »ΈΙ +‚ΌtβψNz€)@‡Q.8δ|Φƒ›Θ ―9:q|#=$€‡WΎΠ΄k‘G3aޞwƒNίI4ι‘TtZλEkΚL‘žFGJ8Ύ“i:M¬UŠΩ/\“qSΒ ’³G?ΒοΖρτH“NΛΒG7eOJ ƒNίIτES¬ΩE{ΚLq‘htβψ{†FθN₯7ψ₯RϊΐΟψža;±ψ#όRo7}T„ηœΟ°έ3Ω7‘>³‰πœσω ;δέΰ>sˆ‡πœσ^œΜϋ σ€>ΈŸa Έ–`pϝ3o,ά~οŽ…g‰9EαYκ½’°ά- +K0ψEα ˜cž χŒΒpΧ(,Αΰ…'`ΞQx‚ή; +Oΐά£π½ž€9Hα z)Όΰ˜‹^p½ž€9Iα z/)ότ8s“Β ~Rxζ(…'θ=₯°άU +K0ψJα ˜³ž χ–Β0w)0—)έ ΡϋLι’0§)]’ήkΚΟ<οΑΙΚΏΔƒΰ_’[π7Cl„ζ³DΌ ƒpŸ%,–χYΒΰΞ7 »Λoβ³dqpŸ%NGT±¬+šBϊ|–όι‹οΎόώαΕ1ή6W&’—‚1O O_Vk΄PxΉmI…v2<ϊθωώΛ·/¦ήžχΗΫnS8L +endstream +endobj +591 0 obj +<< /BBox [ 0 0 270.72 163.62 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/main_eval/plot1_em_f1.pdf) /PTEX.InfoDict 695 0 R /PTEX.PageNumber 1 /Resources << /ExtGState << /A1 << /CA 0 /Type /ExtGState /ca 1 >> /A2 << /CA 1 /Type /ExtGState /ca 1 >> >> /Font << /F1 696 0 R >> /Pattern << /H1 697 0 R /H2 698 0 R >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << >> >> /Subtype /Form /Type /XObject /Length 1520 >> +stream +xœΝXΛnGΌοWΜ%yΠ°{¦{G9±pΩrˆsYΆAʈ€ΨΙί§fωΨΩε2€b;πXΪνχtΥhφΓυΗw‹λ—OΜχ―šYχmqί°yΟ!σŸO†Ν>7 αΫͺq‘ltψuΉύ•ƒ·Αα;uΏΎmš7Νμ―έγΡ‹¦ρbΟš G+š\τ°Ε>Ϊ@.f­ΰevbEqήΩ¨ΡΦΥfσGΌD‘3η²j6LήϊΰkΘΉ| 6'jξΝ/ζΦΜΞ’mΨ*r&+Μ1H$Ό~af?_=<\ίέ’>w£\ΪΌηδΥANƏ‘φή㝝΅OmV›%ƒΥ²©4*‰Ω•Ή/…›Jtβƒβ‡iΠHήS\GNVΝš]qL‘q‚±k“ZA)%eέLš GL€ΝρΉHrΓ@1§υΔΈ`έΜb1fTv>SΆžηmcd?—½Α‹ΰPrŠ\νi]T Ι― ςΣεΦΔF¨Υ’6’―α€έΥ›ϊdβB)p―·[¨n_ύφžh+$=ΰ‰ψΡΝ’Ι &<α gT«OœT:ٝ‘ή‘γΞλ?Ι€,6h!¨~η0Nμ°Ήϋ +΅‚S²+ +ύZYž=‡1 +ž‚ο+ ¬ΛΔ’ϊ⦆kS[7ί@œλ}Θ”'DB1jœΎΖց£Ψ’km₯ΖO“qœ‹nGίΫ½‚O—K‘DδάbŸ­δœ8ΘΰzRΓ'‡Aa±'=ž.W”£RξΐRΔφΕ}gͺ$π—£z\ΟUΞU HΖ°οΌƒ;‡ώ“–w]Ή(YL(ρΠyT]Uή±ψ@•δsί{?Φ;Vm*b+:’΅€”χqms°γ΄5±G\«Qβ*Jθdζ<\λšΆOΤ4…eΐϋAΨ E“ΥβΘŌ±LiSŸγj+œ½’©˜xΨΰΒ[μ°‹B\kΨtΤFY—Έ"9Ό˜GΉ#  Ψ_Ό6"Ρ₯πžQ›8BοJK²Ψ»*^§5¬e Πc²¦€γ4ƒ#ΐVqDΧ`Μ§x―θg€ΐΥL ¨Ο «0Ո˜‘™ΌϋψξκςΌ[+›EΈUA­ϊq8šΈγ¦Α.ΩΊj AΊ|‡.{h€-^ίBνήέΊ:υR‘α֐²υε +"ˆ»Žχΰe.²zΧF*Έh;Œ\-Λ²€\ΜĈ¨CiηMnΟξWWΛεθΕϋυ­eWπΟϊ‡‡s‘ό'†άΖ!ΌμΑUj#ŸQφ Y˜!ΐ£ε)υY|Έ}›Θλ©qenΑΧW—l&`ˆˆ°Zp +πΠυέΩ +΅½[|˜6#ΕΕυv]έf]έΛζΌIW +endstream +endobj +592 0 obj +<< /BBox [ 0 0 224.92906 163.62 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/main_eval/plot2_latency.pdf) /PTEX.InfoDict 699 0 R /PTEX.PageNumber 1 /Resources << /ExtGState << /A1 << /CA 0 /Type /ExtGState /ca 1 >> /A2 << /CA 1 /Type /ExtGState /ca 1 >> >> /Font << /F1 700 0 R >> /Pattern << /H1 701 0 R /H2 702 0 R >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << >> >> /Subtype /Form /Type /XObject /Length 1224 >> +stream +xœ½WMo7½σWπ(Lq†rx΄ϋα"h‹:ΠCΣƒαΚͺ ΙAm#A}w%νR–κ( jX†ψΌœα|π½ΩΩ·‹w7‹·—φ›wf6¬nž Ω{|–ΦΫ{|>Y²—ψ,Ηjm˜£+\|,Wγ%₯ΰσΓΧ?Ή5³slΒγ—ΖˆΊT2QΆμ6)0J]φ9π^aβΰπ…4Œ4pημ/»ϋ/S‘=ΥŠ•δiΰ]ύ)q>4ΣΎνŒ&ԟρΣ¦v‡ΎmΓΩOζbnfί“%rE„’α$σ[“Δε€%"ςlν«r”Ί˜a&?^?/nώžΪω½ωnŽ^nށΕgy€q;ΪG_=JT'β‰9e–Δ•„ЧL’Ί³ψέ)κ Fž…]”bh<θ‰ž±bΠ{¦cž5β[ ¬η=Ρ³β^ hβj¦zζcž 5Α EΧ#ψΤ|»”}`D^zηaλό˜0š=Γm±‘2–#ώ#u)ΔUΘΞΧAΒάovθ¦3ΞΩ-ΰθ‘δΙϋΙΣϋιΤώnηoͺCΆo:žmψp―a=σξ0}'vœDΓΝσ#KκΑw‘-ϋ ϋ³ImA³sŠή—έ΅sIV3Ϋ’π…3‘°Τw€£ΈW}ͺ.g―)…Κ«δΥ© >φ½ε‹ +3*Lh]β"ΥHF;uŒq φ±œ¬3ρσΥΦΐFΜΖι7i(ˆK«-_΅]JQ·L9ΐMςFV^Κ[₯Κα4uώŽbt^D%ΆqIM“fΪ‹k€%C ΐLTαΑΘή ΐWκKdŽ Γ’žβ^υ48€χΤF9ΐ ¬œ tέΐ$P§½ 9ΐ'H|γΩD1w“ΖιZw¬Cq΄Ώ~–Z,pξyy]ξFΞ…œ†¨L­σ>ΥyDm²(kLΈuoδυQXΉu>ΐ§:Ο°‘ ³2Ϊψ θ/>„Λ’cl½π“Oήa*ΛBhuο° mƒΓόT…θΣ­2]έqeΆZ):ξγ3₯¨^ ΉΆ +”RΉͺ@Ύu«Eω-’:f²zHeΔ@VΕ('ΜfψγΓVŒτ51κ .RPK’—r„ c>EAα)ήμδγτ2¦, ίh'wο―Ξ‡qcΗ=½xUΡΒDΑ‹86Ϊ’{Ώγzδξ…n‹‰‰υύ€ίΉ]uόΆ΅ΩΚR‡rPΥWΩΤ΄:†V;(c8Ψ@›}#€σΏ­2Θl“dE₯ΐ‚Ιkϊn‘Σ±ŸΰsΩδvωΌ8{Z_―Vη·~Op»\~ωλ»οU0šD@=τΎjρQΖfώC^ΘW“q‘J9 š‘›Ο»v+ŒΔE»-¦–kSτΌΠνΙG  ,οή{N4Y<ž­‘έΗ›Ss ½ ©>Ώ¦Οο•ω†”qJ +endstream +endobj +593 0 obj +<< /BBox [ 0 0 504 432 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/main_eval/disk_cache_latency.pdf) /PTEX.InfoDict 703 0 R /PTEX.PageNumber 1 /Resources << /ExtGState << /A1 << /CA 0 /Type /ExtGState /ca 1 >> /A2 << /CA 1 /Type /ExtGState /ca 1 >> /A3 << /CA 1 /Type /ExtGState /ca 0 >> /A4 << /CA .8 /Type /ExtGState /ca .8 >> >> /Font << /F1 704 0 R >> /Pattern << >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << /M0 705 0 R /M1 706 0 R /M2 707 0 R /M3 708 0 R >> >> /Subtype /Form /Type /XObject /Length 4006 >> +stream +xœε\M·½Ο―θ‹ι`.‹ί<Κv"ΓH‚Θ^ Ωc-ΛVvl 6ςοσͺ»gXδtO7g΅ `„ΥΊYUd±^={o>{υΫ›»W_>dψτ«ΓMωνξΧ oρου ‡·ψχϋ@Γsό{}ΠψνέΑk‡ŸχγOg ώ§ηŸ?ίnžαΡ_ρΔσΓΑe•ƒK)ΖFecŒΩ`cIΉ#%)Ώ—r­ +Ϊ„” /ΓTβY™Τ½†‰0W% ε,9D­Ό6ΞQ­_Š­JGυ‡Oΰυο‡On7₯Heο)$’†Ϋο>*?‘‹0ܐJ>'Θ‘nΏ;…Τα’vFΟΉ‰J%ΚδkυBά©ήdΝ€`@ξyNxλκCRήζdυEά«> ½"ˆœΒ¨>υΗ1ΌΚρβ°›‘ +°!ηΌ±Ϊj…7οFΝXc|7BΖ|9<ωΫ·ο_ύxχŸ§ΓΗ&F•³ΆΘψœεoΏr΄}3ά~Α:ΝπΕ„#δΤΊ…+@vψjί­B"^ιΧκ 1ΦEzτq‚Vυυκ\s°Kt²Cεi¨θ#π’μ/Žλυσp²E;'=p> „=€NV‡Œ‘Θ=){„F~y5όkψq˜ .TŸ”! Z¨η"SN‘ΩυEl-^Lž ‹Λ •ψΤΓ‡2ωπv¬₯Ω*kδ“‰P!δs³|BnMTΡy‘Κ0•ψ‘Œζ5Ζ~L± :δ(EZ«‹V*MήyΗς2N%,³Š@Φ€[³QΗιΰscv‘[rŠLv˜ΰ{9N%,³3 ί™hΫ€`ς¨3RcΆ[@·¦υθN§’Ν>ni6ή‡9eΰ€Τ¦Sv¨³8ΎP€9I'ύœƒi2p;DF– Ÿ"‘°XkΞιΙΤΡώˆ!d7Ζ δ'$šΔΙ&Β5xI¨lΞΐΐƒ 6Ω)λ±K$–0ϋž`cΚ<ˆ§AŒ&elζ“Α F‘¨AΆD Σ<ˆ=MΙΜ„$“3“:½GΦ Μ›&e y•£Ε8άΘδ l†#Ml ρώ’QJε+j£%G?++βͺƒg}¨JάΣ‡’Ϊ‹Τ&p«ΊP kρAM(βνιAI„ΈΓ…¦uζBjΙ‰ώSεFwΈΡvŸΞόΈ¦ω΄δΙΕήSεGχψΡtžΞύθo<-Ήq±οTνΚ"ξq£ξ:yqMΣiɍ‹='ιFιzΘάΧτˆ*Ψ½ '@$ι‘JέEΪ©;3%Υ@¬κt@FλΊiNU–1LGρŒjΪΥνJήΧή!€ΝwΖ‚ϋΞ ?n΅Uκώp°ΛŒ(šωަ$χυwΘπy +w$Lšq±―ΏCΔ+¨MΙEζAμέhϊ;?e,™ΕΈ8bΒΎ!΍*L(ΗFre…ΈΚΛb”NΖQΪ8U`€…UΫΫΉŠr4­}¬Ctp€BάαFΫΧΉŠv4m½ΌCto*GŠΈΗ‘¦§sοhZ:ϋx‡θάTQ]Δ=~4ύœλxGΣΞΩGιύΏz/ Astϋΐ:JtͺόZrF„˜+a–λΆ„―6?ΠdΡL¨L.½‡Κδ"VތIΥ’¨δetι$TF—ΖCet#§‘Δ γ­S1ˆ?–Ι₯‰Pm–s¨ +±"FQM‘P ם)~$“E 2Ή΄*“‹Ε8ρυ¬T7!€x»a§³7ŒΞΗ0άMΖsδΝ₯ΈG”mύ#*ΑQk;°™h<˜³ωυT‘Έν&χhƒ8‚Pίs§“ž„PΫΝQ°rΖ£@€Sœ›΄V&[ρΫΤRqfΛžKλcJ€u7δŽ€dΑ΅δlˆVxZ»ΟkF–u~;wž¬›^‹`yfΟ ‘Ÿ™QDfώ"οΦπq΅0Λψ, ΰCΥθs̍Eήm@Oμsοή5ψ|f_§ˆΡλΨ¬€χΐ!Ξ±˜@CŽ­¨ x°”Ω‘Y!ο6ΐ‘B%εψ nδa»*”?ΑΥτΛ‡Ε%&<9`NjNm%’φ*Φ|ΦξEΞ"’Zη%T`Aΐΰ0[bΟOΉ·.‘‚δ&Ηuά|“5¨(‹­‡ίJV‘ωr@ξαTnψΣύ]Xk_ΫΙοΤ’Ζ$#ωΤJ4έPyO³DΛ`“ι §Σ/Ρβ‚Σ£€˜ƒηζ™-'!Ώ/gσ8"~>p;€ϋ'λlͺnŠς$ά½nώ‡Ο~:=m™<#z:gζŽ%‘\FΚ1›OΎm9NBΨ›?Θρ ž[ά[O9 Πΰ·ςτ‹q‘gv»z3ΚBΥΒ5§ ~Λη„όκŸρΖωφHύ{€ώμiυoc4@b4 {ZΆ±Gή“Ψ# θΑžΖ€Ψ# Ψ# θΐžΦ€KΨ³Ω›CrQ¨£₯h <φ{Ffp§nΞ}ϊνέ―ZsŸΏyΏΨ°ϋhoΓξOrέπςέ ΰΚΖθ2₯Ӂ[ηΥ ΓΩiͺ…§1ψ:CίΝ Ύmj +XEζ―(€Π]7ƒlπJcv’΅ιψq +‚ψΒ‰Γu(x`tΉŒ‚+ίUΘ―27^4ϋήWgΐΔ‡NΑy†Gy¦κψ`"šρ{yF‰ Θ|*vΐΰΙ<‘Ό„ƒ΄Ž>άΩ7¨Ι@d7ŸζJΑςaωφ³\ΧbΰΪzšOα BΡΕDN<-Qp­Ι^ͺѦψή<-ΚQw`;Ÿ±―"ξUŽ€qv/χΚYΉ|N½ŠΈW;jiŸ;‰ΧΨͺΞηΔKΘ{υσ­+mχ/ΒΡωœx y·ΐΝdDΌώ·D.“.ώ!δ©Ήl/[@Ρ6†uDέ£#ΠΞΛF«Ξ«ιQ{$Κa&nYmu—ΫΖ°γΛJΩΩρM1}qηƒάθ€\+Χ2Q,xΎxjΖΕη`H`ΰ5γ’*ηsΚΉf\άJ9JW3j +Ζ”mΦ<4Ηhλν<.< πH”¦ύφΣΨϊ‘―Οrώέ|u¬ Ψξό₯/‰§WWݐ9ΫPΣΦΪ[P#”K¨Κ; ¦QΎ 5B»„‘½jνΫP#Xj„ϊ¨iτο€i€„i@Τ΄\‚š^ž…‘ύίhΦ΅z™b!φQχΗΜ·Zηύλ£X„μζ6ešdλNUΓ±ψŠž‰θΣ|§ŠQ§‹cQˆŠηΔƒ@Ώl€ΒΣ>Llκ-_n ΞΎι“j‚Εm]›ΓτΗoΑβΛΖ蠚_QΰΛt!=€^ƒž½ΐ€Ι AΨȘΓΝ§ΟΨΥ₯gƒ:‚/†¨ΓζΣ`Wδ|ήσβι ςnžΉκΦφύσγ­QΗ©6ς·K9©h‘ϊάΨF…yζΏ!QδάUΕ‚EX—rΤΨXΖωι¬ψ»ώsEΚ η§gοRŽbPOγ +R,Ά#©—’ ›Γ μš%w‡YΖχX‰ ν\WO²ρ:λρ©“†J:ΪΒγY±ϋ^Κ…—B“˜‘εy½›zΉσ-ΐi9N Α3+EVΣΰΚΣύ]Ώ Ώ―δHPt’‹q€|T|Ίw(n† pΖ²ŠlφΗΪ{²Ή(₯œ”¦Ϊϋt~J"γώί:b1|ό,xρNŸέ3ΘΰηrΎEI‹ς1³Ÿoζ5g ΒΚς0HΔ@†sgΞ‘ΟFώd†“τK8/ΎΨ=όύΟ +endstream +endobj +594 0 obj +<< /BBox [ 0 0 502.1103 119.88684 ] /Filter /FlateDecode /FormType 1 /PTEX.FileName (./figures/main_eval/bottleneck_breakdown.pdf) /PTEX.InfoDict 709 0 R /PTEX.PageNumber 1 /Resources << /ExtGState << /A1 << /CA 0 /Type /ExtGState /ca 1 >> /A2 << /CA 1 /Type /ExtGState /ca 1 >> /A3 << /CA .8 /Type /ExtGState /ca .8 >> >> /Font << /F1 710 0 R >> /Pattern << /H1 711 0 R /H2 712 0 R /H3 713 0 R >> /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /Shading << >> /XObject << >> >> /Subtype /Form /Type /XObject /Length 1176 >> +stream +xœ­WMo7½σWπR@B[j†ίμ-vR·FΪŽŠκR{γΨ‘δ4r’’ΏΎ»Z-Wf `ΑΪηε<>ΞΜzφ²ω|{Υ\œΙγΧb6<]­Λ;|n$Ι;|ΎH–'ψάΒΣR8Š™ 9<.ΚGζ€bτΡZΰ4~|'Δ[1{0k,;"©δ“‹Zš ¨{k) kevαE 3ΎϊίΖ‘*έQέ`됑"„€8#uξ-hIΉ>–8ΒI|Gs1ϋ‘e~E{K”’—σ·"(-΅W6F§Mΐ/9ΏšΚωx5-©πQEΛDfΔ6 ϋΣy£ q4Ρq°b½ΓΜ:¨ΐΪj;’.ΰύΉY#ΝΑh—ŒΧ±BnwΙ£Gl­“πδ‘Yλ#iŠ΅#χ;δΪ:eΣήΘ xrm€°gbrΖWΘγΉ!« 8bΧφ@žΌ"Ι$ΈΒ +κΙ7Ψ¨dƒOž±¦=ΌDJ‹>%„–LΘ#…‡6Ζr2Ώ]6Sω½A₯D„-Rd9Ήœ,Χ—Σ©όSΞO3ƒ–§'΄Ν4φŒ§[jԟβu½Ο—ΫηXqˆ]”―qžŒ·μ—”χ6‘Y€ΕYœςςc#—+œΒivDΙΚe[Q–9xΘ%XΫμμΝΓCσq…c‘³ŸX―Vbμ.࠘χc”CY[S ‹eJJ;²qδuψ.—ΟΉ|ήζMΔ:§“epΠμp.:ΐέα&hgiyq"ΗΊt§«&a)Π―Z³χcaZj¨Λύ +Κ²œA›Μ]­i%=–c:9΅ηΦEγ-8JΤ—[―Λά +š½0έdκφJ›Ή”¬@sZX,fiΘ§ƒΆ]™PŒFΰ’“"δΘg°X_ ΅ΘΕ8ά8š?ϊ‚B +­qB$όΨά%A«˜Ό‘`5”΅Ύ§hΉξέg£ͺΌ6lUY£"¦μ¦T…οΙ!‘₯¬-ž―ΛΒ‡9ƒ’š.φJο'Œ#Š*Z²z€ LΚ΄;’VΒeΊ _!iˆ+B4άΘLE]ΐό-ΥιR]« +C#„[WM¨'l87Ms°½θ π’„q TΖΊryAJΈν‚ž΄bž²jžF'xD lθαΫΎ@YΠ.Ό(aŒGΆψe„·{zβˆ#Θ›ΰ)vGllDΫσΖ“r(Nl\nΌ”8Oԟύ‘¨X…€“ω΄½§δ eαδsWgcɐ1INώy˜ŠG+Ώ­D;;―€ΏάίΏτ3[t3{/ )'AŽ΅Ή\φύΝ6_ +c+'ίUHρζ7εEa[N&Œα3J}Žίήd:°XΎŸ—rΔ±‡’'Σgόψμ·=S~X …δŒCΩOšΥνΏS±sM%\­^ήή¬šJΩί/?|zh,”Η 2ϊΰΨlΛ‘·Ε=κ―ςΑ»%±Η`.ˆ½βά`αγ:ΰQ%8‡άΓC~^58›› +ΡKύ…ϊ€^ ―–5ΧΧ·«›Κί.š«m²δΙΒ4K˜*ΤωqŸ¬ήεχH^wοΉψ_λX +endstream +endobj +595 0 obj +<< /BaseFont /PNRYLF+LinBiolinumT /Encoding 714 0 R /FirstChar 27 /FontDescriptor 715 0 R /LastChar 122 /Subtype /Type1 /ToUnicode 716 0 R /Type /Font /Widths 717 0 R >> +endobj +596 0 obj +<< /BaseFont /TCFTCN+LinLibertineTI /Encoding 718 0 R /FirstChar 174 /FontDescriptor 559 0 R /LastChar 174 /Subtype /Type1 /ToUnicode 719 0 R /Type /Font /Widths 720 0 R >> +endobj +597 0 obj +<< /Filter /FlateDecode /Length1 1756 /Length2 103156 /Length3 0 /Length 103199 >> +stream +xΪ€΅p¦έΆ-ΫN:ΆmΫΆ:~cΫΆ­Ž:NΗΆŽmΫvnίώχ>΅χ½η―[ηΦ[5λyΦs¬ΉΖσyɈUθ„LμŒβvΆΞtLτŒά„²Ά²FGg [€ͺ0!==+ ™ͺ…³5ΰ?waΘΤŽNvΆάΒ‰8 ¬ˆ:ΛΩΩΚΫΉ2³23r³pp³±όy`bώ'ΠΞ‘›PΨΈP`k °u†!±³χp΄03wώϋ(wFhδA¨hnamaoO(IO¨hgmMϋox{€-α_χ t±58ώαt΄q"΄3%4ύ΅s³°5#wUμLέ ¨ΆN'P”%€”Ψ ­ ]Œ¬-Œ‰€"t³p6ΓχG+€»1ΐώ―‹ښ*ˆΛώ εσΟ\ϊό}{€Ι?Β‰Ϋ9š)͝νΉώ:Βτ―z'Sz[€3ΥΏ%«ό9ωŒŒ,΄EΦΏ#Ϋί‘ύοΘρwδό;rύ™]ͺ?+L΄θα?ϊW-ώ―Jώ‚0233·%1ό©‰LΜΦDΔΞΖζOλœ`˜ M,Œ fΆ0 Q©zΨ™M¦x—3tv΄p'Τa€gdd"dόλχ―§ο1±³΅φψ/ΈΌ‘ €AE]HYSζ?|ω/°°;‘#'3! 3'!;!#+‘ΟΏS*Zό%1ώWΊ”­©!Χ?*7q±gυπ5!είΎ¦"όw&y;η?]%€όΏ°¨ξ•ηΞό+ϋ/Cκ2²1ώŸ=ωg‡ιΟ–§ώonό+‹ώ/ςΖ‡%ύ7}+υ―Μ‰ Λx‘(Ωέ΄?Νψx²›iώΜ0α›hλbmύχTόoΦSύσ΅6ωO£ŠΪXX{ό7 Φόυu €ό?I9ώ‘KΘΦΜϊ_Σcα$nα0Q΄p6ώ£œ‘΅ΰλjuΖϊΟ ŠvNλHΗΕω[ͺζΖVΆ''BΦ°ώHύο'ŠΩΫ™όΥPη?}0t4ωΧΒ_ΫΖ.ŽŽΎ:νŸάΎ›Zό©pΓ¬,Ϊσ[Φw<Χ}s£;ψ &²λγΓ*λ6F•ΚΟϋς5²ƒβ;F$% +.™ΩνΪ–&΅i’ΔόTa0ΒuΟAάωλ{¦ξέcόρ@2Πwleψώχv'³Œ2z$ ΙΙόqφ™Κy1Zΐ%o†dΘA$Hξχ—„S›ŒvΫfΥζ/<Η…ΙΉΝ§ΑΑaζΨίL`ϋƒΦΩα¦ΣΥRHesυξ+βz ©ΟωΨ”Θ_,VΌn )Η—υbWϋΐΠΜ―Επy2ΈοΟ‘s]―ϊΈE­ΝvΊ±"ΞFώ²fΖ\ωM ²Xή™qβMτlΞk…¦λx(ƒδΉΖΪetΞkͺqτbu%΄]°(ψωκ‹ξ>#Ύ°ξΤT—ιΠθύFf±ζ«dφΆ\#U’Ϊ§θۘυΔαΔj±5ΐ;ci₯Δ/Υ„c/Ε΅!½P’šš?\~™Q0Ϋd„/׎TΛo‰Š%YyOJ^!Η˜Γ±|±Iω¦RόbDΗΡsηN†0œαΨ^δνojφ8ž k„:$­m'Χ‘λΕ™J‰QΏ==;¦(ψΈžφ ΜΘ<ε +ΙkΛΫϊzD&ήΗƒBd›ΆZήIr`ΡuŒYŞWώd¦A2·’ΫΆŠ—…ύ•yύsLΔ_HU‹ιΡ°δ‰Βhj­[Σ½q Τ7½ +«­|R— ³ΔβbΧxgbΩ΅‚’Τcw©φν§¨hΠT۝o’Σ&Ν©Ρνέ/‡—YΨqxΜυlURΥ=t‡z"Y‚Ί Ω°Φ|AμAAWi$žΌΤί¨ν―Iu€χ‰"‡Ελ’/9^m²R° l2ΛυςηΓ™βΊοZŒ^2AϊDˆ .½ησ @ƒ›’Ηœή’€lž;Ψ,·υZΆ32<ƒτ_πΡΓkξ'"5Ίͺ)wJΐA':Η―Η| +Τ ”AΪUέ‘J‚θžρQ;9^­*hιξ@4Ά™ —=‘ΰqΙC$‰οžO`ŠχW§C ›θ‹Œ˜FΞhΝ‘?ξ½;cχ1L 'αΦkΖ„M•‰hDœGΑΎWβ`λ +!!†ržiF '©$ɏ+ξΟ8‚fԘέρzRΝt}™lG‰›}k όΝ$”3ͺΌŽ±‰€Ξ–ώΤτl˜ε2π»«$ƒ‹ε(N'1Ν#5­ΩΒ‹OyΪΤ r?"žΓΘ—Μ6»ΫhDηV”―Z?Fq4χ~δ€6™‘wί@i‰qϋΑ’1žΗǘσJ‘Κh™ΦEνΞ2—ΥΈ/dz}qΘΙm-ώ\\ί&†?(Qί|Ψ —>6tpΕd©Ί@1ϋ)vΌΥΕ–ΟšE’˜» 3ώ^¬³Χ2ΰ±Z/““ΡDεAΩ+Όxw-Ί1Λ«Ξν©άΐ?E¨άΚ-!\Χ;’—‘°1M•ΓίΩI\ψ˜Ό–4,K?"‹Μƒι¦£rΞΘ9Y=Ό«άΌϋ ΕωdO Ϊ'‹έ^΅^`"DŒΣŽ”€Ο ―0ε/ˆ¨φ!˜i'voŠXNε`|šμγ›?ΡΤΕSΜξΆm3ΨΩςξIJά―G䟡Kζ€–άY±ydΞπrn»ρ#άφƒέqV-Ω›₯֞ε)°< +‡ž—Υ$žζ–υ+€½ψl&ˆΗΛρ.°VWN,ΒϊjΩ͐Hwις…0³Ζζ*ργ©ΎOe)ί^΄5aͺ€?’Τ@μυ-dqHΰ£Υˆώž±'Γaœ…ύ•k‹mΪSŠΛψ›7ΐΈόaOΏ0Κy$™Ϋ²žQ©MP―k₯°tVl†mG9pv₯ΣΣ₯ΒάT+‰ Ÿ9Ša”=ω©λ•x·‹eDSΕpx^πύ4/ΝSόIv$™¦GΤ$―c%‹― {ΔTt›$φΈ@]Ϋ$μ QυCπ‰7Ž:,>,=5hχΈ0iΕ ˆ”ΕΡPΝW“•Nϋ#ܟaJ;Cp«5 e…*W?*{Ν%,yχ,^ε±Aό"0?5+΄FΫτ:Q/Ϋ9„E/ͺ²Ε²–(4π§ οXͺΞq——ŸM ;~κ“bzͺ¬ΗƒY¨DΞΪ(MEbOΪLŠtUα‘~OφzŠG[»R§Ο˜Δπ6 t¦=Nγθ™§αZ™―¬ Œ΄qΓ7X~Ξ6Νp)yŽρ―Ω Κl˜aYݒܐKP‡ΨWλκ³Χΐ%5Φ!βτ…i%‹™1/ME;DUW ΟjpŒΫ₯[ ~9νd¬±κ&άpΑFuJ˜ϊrgΖ_Š©R‹»˜1$ωD(‡ύaΗonΡnΰ/΄Ξ:CΏ₯`Ξg·θHνOvγ@έMD„ΛkFVΕΤe£n;;fφ["ΧhΞMΕTΜ>lΩVlQΚ°R·`¦Ϊ.9)/n.ϊι'jD"Ϊ=Ν'_‡ύ΅Κ·šŒήJΧςvΈΤ¨΄π-)^κŒNXŽkΑfήΖ”!wϊ&/ΕnXΨ’ϋf3}n!{³lO PŒFσj[ +ܟ$φ•Ξ€f<ΡY―ŒgpZͺRβŒΗ,ƒ.0b zSΑœϊ&ΐJp?l]ΒβLΩ·E Η¬fΫΛΩοC—“H&?ν²d?«BΡϋ7{UΡ L—x˜ΒŒΜ›6xyiy[Uh›‡|έκηj424·:%Υ b5λα~ž>Τ_h5‡±Γ0χ +«ΆζRV{#SΎ gΝa^[?υ©“Νuΐ|̟ˆΧηΥιb.Uλše7Χϋ œΖΠ£0`ϋbθŠŒ α=Λο΄Θμ±i#œfHΆ\cω#ν퇂ΐ~*»β8εg»BΫ…|[>ΝΛΧ!uZ@g0]D{ 9ΕV…ΣσKή•α­pηήh »#5βχ6†= Ήώδ™μE©OœΣ˜»D7ΞhψLΤρ7!:ζ}-Φ‘IώΎpύ½ΠΑδC’π όΧ£uUκ€ ρq3ςL+`ϋθ +}‘ϋŸ:¦YΑ_έ¦ύJυΟΙωΉvQΤIŽ%^Ξ‘Z5ŒRŒΈΥo R PDΩ½πΐψ"r#ΘΟ]ΙUΊ8AdE\Rξ8ˈ[f˜’/§ΰS’ΒP˜½Ut£BXΈ)Ω'ϊβN(―ς(.κσ;…ε½Iί0™O©/M€]F΄Νϊ]fbO½­>|`Β―›;ό4BOζ»{.5έoeύzɘώ™Ξ„œ}9\<}wN@:Ή;g»ΗX|eTΫ”lDqνfeΣmŽŠxϊ΅žuγ¬pcύέSN²Rγ°ξ¬¨ !n&7DύΗσ›Jbhμ4ι»Λ–φADXnB²ί²ΗM†(X2μζ™™2υpΐΣ·LauœΘξΡ&ΗΝ+ΆΕ9$“!w Π±ž W(*Bφgύ¨•Dα‹Qρ\ށ6©4 8.b~q{₯e½ζ€ΝθT> ΟτfΆΦχΫljsΏΑnοΒΥΗΠ †Y?f δΰWΞ\Šp\ΈΉYIηN„ΦͺBη2Ν€iqd 1ȝ²#ΡΝΞΫ $jωWΗ`•ψaΘΩIfΣoΚ<#/odΫJO…`L|DΔ΄…χr’Α©~2$b8χ‹λ4 £\”οΈΤ©>Fω6vofΜ§{3‚§Ό +¨A +Ιήξ}Κ΄z z‰Φ,’4%"Βފ9©*)Ήu9Ώ|κT!‘ΟΔ“L―ϋΙ;ςD^α‹'Ί£d [θrχ«΅^§Ž Φ’Rν=πLΟθlϊΓεGpΟυ=)n«ΗHiiΗGE •ρD±Ώw Δ—wM3Ά°r΅IΓt>θςk£k—ƒ 8ϊΡ†-}2χ½ί“wœrχ„:φpά›μfΈΉ'<ͺΗG#―Δw;ΦΝζΊΎΌ!†„ψTJΰΘƒ¬τ–jχq)#"5O"Β]7‘y’[9ΡNΨSc»C㡍c|k}%:Ϊ·4t50’Ι…‚{ηOΞ#Ιh …šRR +ιR?Ϋγqfn|9£$¬Mœ±Η»nKΆδ}’k„R­nΆΊνTja?Oσψ%ΐŸ…μ—‡«Rh\θt?€s†#ΣΜK_uμΕ|}( ΊΎ#jΩή/tόgΈδ±2(a°M•™ψw…ySΣ³α]‘φ“wθ|σ%§Ε”4Mψκ“ΥFr‰sΣ냑€ͺΏ“·_s‡Ρ 6‘ˆφ†".³fJΟig€R‡xΖCΜβ­Ύ…Aέσδžks˜$υ±X"wt³‚ ΑΚ…ωοv”σ±70§ε±ΑξΒ5.εΰιuο@ηεPΔQi Q;‘ "*p€™?ψ·˜ΧxΣf1Μ]Ÿ3"0‘ΰΝήR’FjκJ‘?RΈm¦ρ£F‚ΊhΡ _ο9© Νκω±fΆbΊͺ E?©cΔYE‚°ό‰4σδ·Κ7-λﻏώˆ’ΓMšjύϋβνMG‡†φQ„Ήλ6ψ#²ΥΨjόgπ³|Ώ›egCΏ‹¦?½£™ώW³/·@Zn`Ό½ΚVθΌ1ΐοlk–ΙΥGG sα;Εi(;Ο| ‘ΐ™jfΥΌτˆΗΆY ΓΜ–Ÿ@ X^ιόF썳!S?:Ν΄Hβω~O$ˆuy¬ ι»‹­ΑyX―)bΰnRΟφμjρˆΨRHύ*ΑιΓτ‘½“νti^7ί έz«FŸιώ*°ŽήEcωΙ;)ζ΅γ!VEr9χΤjCm‡ S,sœ`tΎ"εLίΖ†s“)_—bώΌ½ 7pσRξB bI(W38Z^ώ3jΐ Ξ#~&՟ύΒ0w0‡¨²0}tΨFŒΛ,H·P\Ύ£—) >™˜X^PωφΤΟ₯g$Δ‘&šϋκ£γš’ŒΣN“oEΜvVFvzMο[ d6'Pρg•,gΌ=ή΄/k!*Θί%ηI½ΣnοΌ6Ύ€ƒœŠω=7#TΫ}Q.~Τ@‹)iJηp₯;‰ΜΪ²Iμ΄Otlρ₯rAΝLE΄›oZlΓΣΪ +“ϋΔ½σ^χΗοTυG΅χΟΨΊΕ)«NœC–nΝͺσΰΘ}Ξ3ΠωhKqΩV<ΘJ$©"RΊΜϋη7δ)μΏύ9·¬ΘΑh]ΕΥ”+ΔT‘€IZPΫ#mΨ–eΚMmΟΡζa4¦RIΎΠVυcΘ1ž4θG־ōYu¬–q¨ςθΪ1*Ήβ³eλJeκΖ4―u±ύC2ψCΗψυ€¦άάΦ}U3x₯₯gζ~ΙΥ( &/LδΥξ/l ϋ2`δΔ€τ[H5ΥKRΧ“uRV΄Έ'ΡΏlώ|¦ͺHΊ8φoRžΚ\£Φί€TŸΡΏά;1#ΪͺCΔδp:1 lΧ²Uάήa@ΌjΠΡ:s¬4W%Θ~λJΨbC’²ͺ$LύΨ²χ™άάЇˆPω Ί-\Έ2/‘ΙΊΓžΦ*ύ…„ηD©`€ŽT‘Ÿ±Χν=mAΜ/,ŒQΞ°—™λŠρHΌgιΙέjέ›+Ζw…J~)mœƒH1e&œ'SζhƒG:ΟρY|ώ4n£½ϊ +Έ PŠΪDV―Ι ΚSΕΧ;ΰδ£/?TΙswgβί?DΝ/ιΆ¨Lμ[i}§ΛΞώ’αvΎNά>•hTώ―gp$6KΘ£"€=.dψ£[Žά X60ΜφUΪΒ΅εΈ”iuΡπ`ubUw[± NΡΆΪClφ:Κ>M˜ZΕΒΈT™ŠS™–x2QΟJtΕόvn"~4ΈΖ’5Λ4Μ~₯*i7ΜΙ‘ν­¦OlΚYΈfšŽχψϊδžΫj5p…ζ έtc’“@§ΗžδΧƒ΅hVΒEΈŸ/wFNYο4Ιή=)k£ r2 IίβωΧeΔαΝιGs@‹δΟϊΟ-Πs€rϋμK0™mjŒΡ$™EL¬ˆ’―[†ψΘC]7EM"ι8K1 '’§₯F%tΰzτ${ƒIŒiθ{Z±IOdΛ:Λα²]Νk0οΚ}άΝΨ¦Ϋ>βί\?ιρsκχˆŽq3’.š+ƒbW€i‚“σMVZκƒΊ=[zυFΊ©tζ΅ίπ_ƒΡΓ~·ΰΛ1} +‰Λ_(:Κ‚‘“ώ^€ol£Aήυ. ‘¦ ΟJŸ‹eχΎQ)jG5r’\Ÿb€•7\€yΞόΝ―ΝYΌΪγ5D:Ε΅πψbX3ψUrύΨΝνRόpΰ.xΆdoWΞΙ!ήφ$ͺψό³¬ρg΄s+{Δw ‰1f^p¬Π3eΑ0”t(2SgvaΟί]Ά†ΛΕΥεεy₯άά—Ÿ™ ΏΦ‘ŒO˜oςŠ’ZςP•š₯ΖΖφςŒγζ.{Χ՜Ω#₯γEΦ©91ωŠnG»ΣγΒΪΠΝ‘•—χJ*6'ft'ΥΦΑgι—Φ]©HOΕ^Β<ϋ-*&νO$²‘ΟSΤ¬˜FŒΥˆΈ>›sˆ]£‚—/ψ­YOHμΡΘ*±Ωd™1²XE„iνΑD8ϋyρ–Yٞ}w`Π<):U{ΦγΛ‘™šΗY"Mγš£ΒB += r4Σ…ŽΣΧ„yfμW§±bΩ7ν]|θέΕ+ £αŒ™Νk/žΠΞΗγn8~D-ž°#+[Θk…Έοz7‹‡ii―›0I’šς.ΉηΥΓ=λK™ΨώψW9™{Rζοΐ°J,ŠΊτ 8σ²δS…)‰>#ψΫψx&jΌ=ψΎwΈ ³z|ih F_ ίp.Υ2ϋ›‘?B8ϊξPlΉ‘ΉaΰΑ Ιο†j€ΑΕε2’’`]U‡(ŽφyΪ ~rOˆ*σκŠΔXϋΤjΆš‰4€°πuο.5Pι¬ψ›«0Ι: +’AVS}/mhτφψΐ|^œν{»)Αφ³8²€j†$V’Pθ §˜Ήύ‚}^&ΘΚM¬h§)τΤrχώžΗο =v5y?΅€ΐEφ[b'ΩWΑπάΛρΑg GR«G…gΚ –(βπ%δ–LLJ¬…EޝqxlεYω3.ΰ’dKέ‚m3Žh7ΝςPvm2(Β¦ŒΒ.=ιRΐ©­€)kυΆΨ;Ψω«O~£'8ε$+ohύϋiΩ—.± UŒ£+wE0Z³΄σ6ψΨZBη#)–εαZesU¦‘&`‘σ)₯•Λώ@dό“GŽΓαΝί(J[VρΨ½{ vpςΡ ΊΤΒ&ƒ~¦Qξ§‘„¦‘δ‡>‘`La0Πιάa5i/ε°DO/VΓY‚T½ΑQ­Δνσδz u >„ιoq-[l&` Ν"%χσPmΨ\JBώϋ2›ZΫΥ’1ςύ“ύDυ€GόŠ―ΝH:ΆV„ΐυ#28mΜ|T%Ε‹–2EβΎ‚”€xπέτεW@eΘ†n[Μφθ3BΏΕm¦€¨f­άgΗ5cj|Η­ n+λh !Γ­αζZŠtμΕ=μ³iztoάRήψ ¨€ςΣωπf$$cš!‰$Κ+"7J ά¬ Φ#˜,ΉˆeLγ-qOLϊΦ5[O‰άΘ½W=F˜$a’Bk,τΩ`εεε[©ρ˜ησε|…ΫΈΤ‘•ζΝohΊZΧGψΫ{KΪγΏ||―Λ½m•-φCΎ[}*ΔG‰P^€xƒgϋ›5Ώ*c :τ $‘“i[l³ΓCΙιά π%a—‹Ӂ·{Ώ'%LkΦ”ν?χ }δj^Mα(oΘ,|+„E£Ρm€·χχ‡4Zςeњ)Ž Λ―Ϋ͜]'«²ΒEΊωŽjLƒΑϋMζWB5‰)οκ=•ήΰkcjPλ–w=αC ’S~έ±ŒŽqFnΥ‡ΤυhI–}δqόΊŒIs+Ϋ‡ΒΊF¬§)ΰ’ΆB'ζσΧϊ.Άš<Ξ +Ν^έ²i<Θά₯ »Œ°gβΪ›αsσÌ―(ΗΪΘ8βΓοu¨°k Σ"―ΙΧ λΌ*().οͺ ¨; ,†έ@-8Ϊͺvοͺ©·ΰΈeΨ‚BːΒΚvt LδoAπδ˜Ο5 «Ÿ7ϊ|;Ί«§χ˟Λέk@H\ι­=ΎoΙ IμK‚K°8γ—\ΏrΏ]ω;΄zZϊ=§Ε31LςA„χΰ][‘•v +ŽiηΛ†‚yρY·πeϊΪηo>m$ƒ½4s‚«£υBR£z‡«™π™+«εsΛ”v¦ΌB )";ΕΚ$M;[Ύ΄φιw +₯|«™@?rΑ &χΕφ}%ΊΔb ++A™θΉLϋ<²TƒwσX]ήŠ-M™΄‘ρ[σ‡9%G/ ςd€ΒGκέθQb)₯θe!Υ±–άαN]š 'αί?QΎΠιΆ¨β:£Cњ¬Γ¨"ω[*Θ³ΎA>δΘ#T£Zώ5κϋ¦zuTKQΥ,Η}J-&PNNαΗtC!°ˆk„€uΒ¨&ι6Ό0isšl„>^[DgΆχžανZφΛεΑ“―…‚W@„σd€OZnΙΊφΛΤ€ωύ5MΧ°Ϋα˜aœ“±b·‘½ύε6ˆ:`“¨/,QQζ›‘—g\ΥΘ;Δ$$σhπ"§i.6…–2›#c·γΰ; ΔGόήν€Ό=@@\‘&€i”-’|ΜΏ +»ΰ„ΖŘ΄DYΘriO‰]d +ζ½LΕ·1ΈvΝίΑ^€XXΣ'cΧ-»&byε¦ΏχΠ΅E?ό¬*†R"·x τ0ŒΠΙCŸυŠΠ7τM"ί‡Ta4δͺH,¨³"Y¦‘qبΔežœ’”ΡzdΈ–t'n΄H„ι“gυN‚υ8sΧ_σʝ£9 ΐB’ζ +ρ5,( +Q5oΑ·Νf°Κjγ²Vώ,L†Iš­ΧΏ— ²64ςυ=b€3«Ε@ͺ¨‘'‘Ύœ¬{'Nt_™ͺcI&ˆQ3έ°κ˜εEfE吆ά©Ι«@‘ΎQ{§YτΟ™„y0`}ωh‘¨νε›PνΗ•WŸ>γOP©eΞD5αΆT±μގŸ˜¨Γ(Ζ†(=P(ΊH³–•_ξΤ0}ω,‘ΌI!5}ƒNŒDν[ D cFαJA©υš dΑ«-­}ΪJ}Ÿ1ŽΫBΪfΠΓ@ΘΔ8T +m΄ικ:g?Q[‘olE’€ΰŒφ;Qγ?ΆeY+BœEN”~ +nATš;Gίχω,Ιtfβ­!) ιz6~΄αη§2δΊ*"O±Ο=ΩTώΡ}Ψpo γ•_“Ό’ίDλ*]Ξ’-±§¨έV_]Ωǚξwκuκ賟³ν³ρž°ŽΫAηΠ'šw%>Μ@_—Tκ”{Zΐ΄(εΕΩlόν”Ά¬]g{‹Φ™ό¦Εθȁ3 +"Βkξ0Φ Φ½j0ΌΏ λCI%d³ηΩΛ―9^ο] +ώ5@#’‘‹Τ 0Υγσ™Γ¬-+Ρε‰)ιŽΑ>όΗή+¨ΔΉXU“9=Β‹ΰΜ$Μχ©ΐήΠ8»}Έ+Ό^ ΄ΦΣ‹Sdτ`[ψ}·KŒΡΞ™a‡T1»Ό@Μh8ΔΏ|М¬οΘ,š¨δ>΅jgΝNΒ6rΫ€"Ύ‡xΚ€—{Μψφ'˜Ρe¨[χD¨Œθ»₯ͺž@ρΧ¬t2†ΜTIπυ ZΖaΚΒψ#%Ν_ ‹"%υ‚3½MMΞΖ΅‘Γ°‚TFφΉά^Bc“[δg•ξƒI\ATžώX`oψ1Ο#ϋούΕσΗFBεΎ/‘Ÿ¨ eΈƒς¨Ω xnχΐ‘ιΘ›­VωηSαzΙ0m”:‚Ο?tύ8.S‚τf€t―(¦Ίg‡Rμ}Ω“φHpω`‹m‰HΊΒ|ςοΏξWR`°UΧ ΘˆŒjŠγΨτΰχ!Ο-Ψ τr Χ½<2,TŸίb-;Σ:ΦΛΎ”eΔ’v\yα]qKW—«²Ρφk…J€Ύb}v]ΨƒiΑ7:γ%€I#w/°d€ΐύθ`Fs§(Ϊ?–xεδ 2;Ο–4Ή»kω&•£xς 3τ£|Μ“Οƒ\ίPZ%‚f!ωЁΚSύžv³'y6Θϋ,B΄=Α>5ψΤμ» ήT’vpωΡ·X„α–pΗ“*Σ͟Šϊšg 8/ˆι4Ϊΐ΄»½Έξι)ƒ‘i θΔ+]Cy%ωΗmDJ―Y;O1tgA‘3ϊ„j²c„mΎ~©άΖn½ ,‡Q˜μΎGB1['Vο~¦pΘei0ΨηϋUΨΤψ½EμΨhœφνžƒΫ•@Τ9ƒΙ­•ΨωόsBoΌj•Ήτ3ΝΗ³ώϊƒξε +­ωψNƒA»ο)ͺ½”B9ΣΫ\?ΩΜ8“%•ΒΓ>NΌΘ?Θ΄i”,?λ·ζFO­Σd䀐ΤEΩoΙ»|šΧύΣzVjήQ,ͺe€Ό”ν!Ε'•°@ΐΉ|xίrζ4=Ϊ·ι―}AJ„KΌpO©‘­\ΣυΌ²ΡΧHΈ&j݊Λ"BsZ€%Υύq“ΪΗcκ7P²'‡‡‘£ς€\>₯³QΨλLΑ)=Ε’ZΎiψeΡ™D‘Η«ΗGΥ―.( ›JΈ'Ίt€ΥΌ=2Ώ0„γ§3Λ‡$ͺΏ7³uijά­4 φžΗΑqwθή5ψyP˜lτ +ς—IπΈhMΜZs£8φ:fνh k”‘SuC[Xέls·cΠU¬%ψ―Ώ4γΔΥ αΨr0}Ρ^ύJΩzž‘ω\‰RρKε’£·―Β„φь⟢χ‹3=ψqΐϊςνηAwzΪ‘:¬KHgj¨{Oψ ΰ’3ž4(μ˜“ΒφΖ¬ΤΫe£fΚ†dKηEςc»[}U&#ΉYV Οoc’dw’„N1σž#όΆX3χΕLΞRϋ¨π}F:kΏjυ£Η)A)›QρCΪΤυKΘp€ΜySΨ.PWη_Α“ Ϊe5¦£ιg³qηTχ΄3EzkχΐEαŸ=χωš Σ?3 ±W3ΒL‘Oο0α‚#ΫD­Π^C„+2–Πκz-IQVΌŽΣΆΤώгό+ŽLS0[”ŽΞ–Χ^zKژ=οΖζ3mά" zΕIΞο­Υ[ΰ› νΕ°Η―™Ί΄Π]χz/ +ΨΑDΉNΎC»yΣ]Σ›ΟΟΆj~g„Pˆελ"ŸŽΫA~ύ=`^΄ ―Rν<–όmAΊ§οVΔ‘-λQdάyΩy}αο~­Q­ίΠ*"3Τ”VΪ ξΘŽL('ͺΩV{ΘΦDN0!Š΅_E‰ψΠθΏ&Ε5pˆψP̈Kb €ŽTηXώESŒτDEdδœϋ²;ΏXIςeξ„eƒvpδS ΖZv1š]h›²yC˜Ό§BMoΘΏ]eΐ± ‹6»ψγnΗ†1S˜ΙΥΣρB¨’Ϋ+άΫσς©ΘrUζXμ±Θσ"’ΰrΘφyuϋA!žΩBϋ?²·κi°Jx,°z«μΧW²›^ŠMM†7XΒ#δ΅S ·U[;œ±χυ&Βp +m‡žͺρό^SšPFΏΞb«šiήβαΟ Η.ꄁ¨v +GB#Ϋi/ν°)8sρΓlΆe©]i¨Σp QΰV8―jέγ/vω*FΰςR1ϋΩMσ“Uσ_ΊVθΤ΅5yΌ_K³ΆQΪ}t†Τ¬_½cήγλη0*UυΒkΉ)tFΟ§&ͺ&χκ» Ι‰’ώ+ΔόσGίκK >”Y0*1a"‹ΞΩΥ?”’μ)H0ŽΧ€XκŸ” +Ά¨φΜ’8ζŸ€T?Άfg’ξγAυ0ΚδκŸϋΦπ:‡kΠΕƒ£T%©8τ±Ψη‘£#`Σ¬i3`Ž*θ|Ά8+Τέ/Ώ<ΧΖΒjb{ΊD@π}* ‘”ΜΚήZŒŠ£– DŒOΫ.UEα'Σs8ϊ‘Κb8>ΟεŒΰ/<γ6’ˆ:ƒΧΨ׌ZδMτ-φΊ+mȏυJΥ[₯[Δ0ΤO»fε2GΫj9€eFρ‚uλΐ£ΥGŸ’ί§ταPk:Hν +μΐj^F―χμύƒ·J題‰{δS=ό‘hρβC7 }‘π'uœ=š›¦DΎGπiΊ™DK«’‚Τ‚΄Ό“ϋ©οΟ8Ά΄'‹ J>ΣΘsΝψ8·ž +—@YVŸα!€μQύπƒž0„0…OuKs nDέIbi_θ†ζ™­γΝ„UeeτσVΎ’6:²ΨIΦΖi¨ZpI@k¬Ώ2’,νβz6΅άε-ΌΔ7oβΝ+΄žo„χi+IΥΏέJέΟ܈oG‚-ΞΖ–ωΙtΤF‘²οm@vηΔ<αuΡ±"'%ΎωŠ'4ξαΨ―ί³zΡq†s{B­™ n/,ξ|°Q‡ŸDB‡”Λb½qΥ^³ΛΟ·­8₯Ώ’g}T–ΧΩΨ˜ο†‡€ttΎΞυ+ήΨ–°.ΌΦŽ|¦œς]°½FFu&±>€£KRτ}WkΞUΠDšL£Ζο&tA%ί‡n¨c³IΤΜ4ۗ鬙 z#cPœΑ7‰‹€]D@2Λ/Ζβz=ΎΊ­ΙυΎιD··αΤBul[y‘X‘½l@θQ?lc(εωhS +h&:~£ΨEdIΜΜD=jΔ™ψŽάΚΆA‘[Οs²dρB@ žΘ€°s‘θ­ κ>&g–u7;οέeβ֘‰ +Φρ“T‘»1 Νiΰ₯–²΅ν +ETΦQtΟ>Σ'ŽnpΤeEš€­έpΪ]£΅ε +)Ώ΄ΘA*³¨εG…Ξ„ΗͺΧcT*T΄n’ύY|Y=@o?E@\9³Έ²ΝB*¨± ΞmbΐVςs›TΠ˜y6•#Μ\―+± 3+ζEώb3ΚE*”$θ,ξp8£†¬βΉM‹ΐ#*e³τDvfαžνξ ψQο©1΅œzβ)#dΣpύƒ*Ά΅SΚΖΞ–‰ 7?-Κ)oΛΕΟͺ%u_wZΒLf‚σWθ#qgͺCsΪš1{;«Σwέ4׊Ξ5)―‡`ΦΠCNπΑύ=<$5]χ¨@坋΅τMοv$Ξh“]€ΦZuBη}ινcΦpYλ° £žΑο‘’΅ΌΥΨ ΙΞ+Ϋόdφ)Θ _;”θ ŸZρ< yzE/ί=yY*j\—³~F£ΘΥPmH‹‰МΧ`ͺδ‰œίٞΐ;¨³oΦ Δ!7ϋ™KΦιeq]ίό<ΑέΚƒ*§LΜ'r†ϋY?ή<‰ΌJ "oΜJ<Κ±Ξp,Gjωtλzΐ;8vΟ¬/0’―»&3–`Όξ»š97ωd?ƒgΫϊxΟWoaLYΊ +Η{k ½ ΏIΖυ€ςP(βu)nvtΘΉ* ή»Όyjζ"§[t±τυwΌœ›²E1χΝ .]½Ο»ρ°pΉž‘_ +Ν€,™£ΰ ->§ZJΈ8D–qw˜‘δΊyŸe ήΟ&½>sΐpžŒSΘ_ώ’άΞρƒˆ%ž²²θyTω χΣ>₯xΈκB†jOV{ΊΫr5Έ vU…M›κH²έ L³fθ›λ`q6f”ΊΊΘ˜šΒAΘuVθςZχJΠp€ΨΜ½Ε6δμ[θξG5ΑCιΎ,υ¬Ω`θΥΓΗA)Ω*ϊήO t”]λWΔ`‘Ψ‚y's'ΕΜ‚Ž@WδžŽ>‡]νΐKDΘ^‘Δ‡l ΐ¬Ή4ΕB«fηŽτw‡ž₯QWe΄κMz\έ ^–0L†ζ§’Λ‚1„Ι·οaJ|Oό˜\ΣήtK2η‘ψt₯Ί‰ΨύYͺ~(3έ+Δ}βύ…Ι}β°ίΙΡΒΰj‡ύ±Ί”‘-5­ιQj΄ΞΗΦκΣιτX[T/cΚ?=Ή/v6aw©?—=ώόΕm:*΅uS0PΉv±nˆΘvŠΡ¬\zΗΰδaΙmƝfLΉs=γ’o.I+@1+]$yσpι‹ρ‰z…σΰΕCΝwσk΄87ΗΨr°Ϋ±οw&Γ€ψ ι=·`9οŒfVμyΧΆM›TXα3•JZΥΡ¬2ΥΚς–ZΩ‹q~K«WλvΥ{ΒζFΟLq΅ΞΗ2 Ξ<£’TΉuΑb%%Β‰ξΝF»υΙ[ά>]#=φΥOΞrko₯oρSYί"Ώ¨xΕĐ3Ϊ4D3PΑ:εt8΅»A#tZ£¨Ι/ΚOΥ;9#|@€οXS¨Ιz±[Φy·¦€!σT^ε9Ά«ψa oW#cεHhlΡ»–υLΑe3ο¬ώ sΘTε~όX“°΄’5ΈΪ.]WYΘ‘ J<;&­/-}σώΙ0¨ 8ΉΎ{ZΗ°――υ=ΫQœ#υ©ΆΈ†³ +?•1hτˆΪyΈιΖωχ< κ#ΙaZ>N(Σ5sΓ.0ÁΏ[~Ε³ΩφwιΠ]&%‚ΩΩΰω‘ƒVΊ¦”>^ƒJŸl˝κgΫΣΧ"ψ!‰JzQX…Jμέ5Νμp΄1rZœπTΔIY0άΐ΅MΆŸœΜΌbΦCB1Ξ ›FIi’βε5œG8+?#ΥDΟ™άpE Ξ‰0iώœCž7Q΅ΣβΦ~ΤΥΘΖ‘‘(ο *β$Γ?D΁γΊ¨SJμ:¦Ή~cνγ|²Nw;πύΘ›”k|σgƒo―?Φι¨Γ©}§ΛΩήυ31ω±fΫ­θ„φ +RHQ‡φ1Ε5-4Α‘ιe.–Γ)tΠ°‘LΰtρŸΊQ― Ύ€>°šλNτ3³ΊvθΐeΊπ?τcx[3Ρΐt +'?ξKά1έ+kΕtdΥΎZήΑ/αŸx%FΦv±€λO4­0‚ά ^χS@\lW^o¦LϊΑ0hTYgcn„π0WΔOmςΆ’βκŽ!†'s«―Ύ΄w¨¬x Ω*.γf»8₯ΏhθλtΞΊ„θz·δίΡζ 0­έΥ·&?£ +™ΜνQΑ(Ρυ.Mjsγάbc?Ζ“ή~¦‹ΝΗ¬XΗΧfδ€αŒ½Ύ‚ζJ§e[N*ζΈ—ψΔΖX€?w0bϋξβ†jσξ  yu@¬£s‡ΓΈ7οΒg?΄  ­/νΞΕKιhιGLˆ±‡x*Gϊ+Oί ΊΕM'κΡύκrΘϋ‘ωγ°ΐ‘ƒΜ6B­i‹ΨΎ(6€GεΈ1y8₯Ώs²λώfΪ£“D΅₯'όhf‘‘ηΕ6’Λ!V¨˜tsJ½2ηδaDΈ*!’Άˆκήξ!ό+Ϊυ +… ^SUf³‘qΑ@ Šάω O+€ΰgw!ΫXΌfω9ZHKτ’Zž‚οΡXZyA©—|E9ΰΦΌΓ,šύΛΛ7+άC:ΧΜjF›ο^tκΈ€tχQUεVtμώ@ώ*Š2πςΎσœραdΗrD|ŒŸ\Ώ/Uzρώ†Αι…KoXσδZM.’ Β‰iάpςu4Ϋ­ Ηο]Κ.Ύμ=IΕ%‹Τψ¬΄Ο£r^tj ’¦ψlβ­ΟŠ­ο¦ΤΤ:jOnί†iμΙ; +rŠΣ„ΜN^₯}Ψ{\4F@“έΰ©<>ψ£9·¦w΄r‘Y„§Kά°˜£b–1ΐΨtαHšwža«:7kt§r}›¨Bίrυ‹όΪqeαbά:ˆΕ»RΤάςΣhϊY•*dHkIΧUx&Mν]FaN|•5α“oͺ'ΟW–#3RN†υή„σ»Φ4mΰvc₯1τF€lί–€ξ6 ©xφς8Ύμj%Ÿ~τŠΐM§ρk­ΩRD”@(s¬žHJ–ϋbz{›y„\Yi¬ρ['Œη•!qgD1$XΆν˜ΰ7ΜξλV©Cγώά¦χ„.U Ÿ­ œΎLυ2Eϋ]xyΗΩΊΐέ/aΉ™Nι‡₯Η€ιΘ’˜rSΫ@yyΊρ[^g€”οΖIΰ΄Ώ0ΠI霁ɠn­D‘Ι΅ˆΚθkΔΝU mR cIΤ?ς€Φ0bΛyxΊ»RΓ9Z~«gNBUe3Θ$Q>„tόAkΔZ§ fRveƒ“!H9Ν(qjς²SͺφΝ­z ƒš?k‘Α5]q63=+žΠμ<@v45iI·JλΝ+]›‡΅Λp==\––¬I,βŠ3θ†ΒͺƒŽ‰FAdpζΕR{c?@ζΖ™ΞxνΧψZ4ZΊqΜE£¦}ω}Œύ[’*vŒWίΠ Έ]ΟmΕ%Z( Τ·ŸO½ΎDεΛ|ωΨo γޘqB„HKη0<ω“6 <πΕgœzOP|Ά²η3~²CHΉz½)ξΘh#KrD•πVoτ'ωOn K.Š  οΙ€=λch fγγΑ(Ϋΰ΅μ&,­›(Χtͺpqώ~υμcΏ]δ½ hZ•μŸ Ε¬θ‰πFœλ°6ά[¨ΣχvλΔ§¬Άj}#oς·JG›Κ”Qœ(uͺΏq΅'₯Υ·«Ÿά²<~Gtώv!Œ| _ύaQ F=υΘΓDgΥώZΊ]ωι$stλυΏ6¬ί°)ͺ“–Δb\ώpύJ+xGλΞ2³iͺΕ•W―etΖ :rρΜ΄"ΔςjqΘβΰMφ2Πη^χu;υfJ‚E–9MαGΛϊy‰\ΞTRˆiXmΖί°?™Dπ–ͺ.…MH*ώ#eςwκ˜4CMˆ jŽo<Ά†’«έοƞϊοdΟ[΅Ξ‘V3sgς½œ +‰U_­J’λσ]=ΆΒ€FC$Qς‰{Iϊ—C8ŘΉVЇO '«3ήj‘{ώe  +±|Τγ0œά·1ΗήՎ¦ QλšAzhΛo`φ.<΄ύœλU$zƒK\«4‚ηŒJ'‚q€Q…mϋΠoy]E«κ ω'b–g˜ŸιΨzKΝ¬§¬#M΅…†β qωβ(LDθτξͺΠ<ΗυTR—“$΅ŒG…%κiο~‹m¦dY}zͺτΛNξ ‡ήfŠWΜ’£μƒ²Ϊ#$[DE—m JψΜ g”θATΏ3Ύ>x—Γ°τυVNS±ΝA)Z+ю\nΆ|©{ΔΝγ ά„\v~ΚΝ ϊ¨Ώb«OŒMbΛη5ΐO…‡unOŠ@ƒιόψ”BΊ`uΜύ9Uρ$+r[€—"n ²©qٌ_SπJώΧζ°Š’ ΡΨΆmΫΆνΫΆmΫΆmΫΆmΫι·zxΏ‘&΅ :φΣΊ”χd ΤHε3Ηt5Ιz3?Ζ£\σδθχMΔλHq‘ά/Γ ½A,U§΄΅χΗz­ UΣΆpŸρZ«ΈP:WΙ‡ςf!›TR»Z +B³puσ鈰{‘θ^™χ, ―»1}²4ς–|ζ¨Ιfιjβ`PΘ €“ήsڎχŽbίΕr’"ͺuΕΓ=π9χ§Ν,ώ_Ί—«4πAχΈŒZ„λχμ¨ίj7χWy&!σZw=~ΐœΛHΡ*g]˜ΦκcŠ6‰9˜(+–ŠBMΉΒ¬j†ΐH:Ίω‰ΫΓΛYH§‡“uε/6Ημ7ν…a―nΈ†!΅!^ p‡ζyδu5ςνΞ΅…kί9H[…½ωx;`Tr pΡφςZteS³&ΛΜGόJe–‹’άBΒΈTG€΄“ΈςΦθd<*gH†χrv}Skeμδ䋬ΕΔΌŠ=„θQ™(­ λξί‰] Cj§νΕχuRσzhϋ[7Κb­­›ΈλΫΏ’¦˜Υzβ%θ]YΗ7Ώδ¬ύ:WΓύ@F+k,Έ,λ$ˁΒvWD™7ο`WΘsyΗ£{΅Β©Ύ6$9+­蔆ΨΝΩ0ΔΌΏ‘ωΎΆ^3x9½W#H)Ϊ<ΞΈ%–!ϋζœ •°°›ΆϊΎOθ‹γs#―?ENV³žqaθ°ΐΧ- ί”αχ@*₯š3•Φ΅d{ƒzμXˆ Ή‹vu-2¨LVXq΅J.{δϊ9•Ÿ^1hΓ½ sΦq;ξέν ΟO +)ΙGŒ\•Ι{―Έ- β5Φ!]e¬_hjΓόS†:XTuλ2…βˆ»@Iλ+σ%«ήΐφiΎ[ y+―MJ$8¨*ΔΫλ 0΅ φΆKΙή QΌΝ~ˆΉά§/Ό•œu―bΣ.eΰGΒΠVΈ†WυθŽ’ΘΙXPσ–ιZ»€N°Œg,gLŸ98KττON$ιΡ τ$Ÿ?Z"Y4‹Ν%w8t$Κ?84=;δdπ;«ΤV’|*W]ξ&W2πκ~’#bγΨϋƒ¨Ξm`©Ό―ΎΗG6nζΗ±υοαΠcC`«€ΈφοΔ2iφ|έδ< L’MN,Ub‘c8pμ‡ΟŸΪΓM=ύί!Θ·z•β>sˏ`ξ9ζͺeK±-\ΐΑ•—7§Ϋ°Ρ³,Πz±‘ςα|Ήϋ=6y71Ν‘H™οYς£‚ιΆA›π̐;’š;^δWŸlE~.¬ηΌΪOΜi«·έςΜ 1ψuτNΏSΩ‹ “#†ιͺΐλ_p•άΓ_WΘzNώf$‘ΠAŠ g(Σ&œΔˆτoJͺ€α7žn@ sB²ζ“ƒΙˆB©°»SE‡½z6]Ÿ«Λ8ΣPΰ ΅pωCQŒ’φΈNqLO‹τqs+nήo₯―X&Qώˆf²jΏ ωK’‚;½·…¨λЎς(ίw˜»%aΓθMIΊυyΦ7J{LU₯@•,huΓ~! Q^7'Ž… ΐ€}©cx΄2ˆ+νωw‘U;64Α†P-Gα‰WΓRΞ%‚BW‚ŒΆͺ=)9ώˆ}XOπs|IΥ³Š,gΦ%Τ$¦Ξ FH:j;I€r£‹œ“ρ-‡Με`ξuΆίŽ’ϋΫΔViΞ½~ζ«D4EGͺ°q#³SΦί΄ Zκό_h“Κ|τZWύͺ­’Ž2Λ ŽB€±ϋ’κς;γΩdyΑH]Š:›‡ΓY—|VςΝ#κΠR6† ’v3ΌΤx˜ςq?G\ΜΉŠ?gΨs6|E§JΏŠ³£΄ ηlίύm-τΌPΔ.;M_„gςρΨqDνXο³ϊρ·h–πoy<ώ`4a‘8ϊΞ¦Η^wΌL‚έr’κ=μ OΨMοώ-ΈoヲZΜ-«NΧ^ΐΓ{-S)Ε§ZW”«ϊ|«s‘P€ώΤ‰O†™,Σxœ*‘όι›G5υωάI₯{Uθ:¦B¬€εN?šΝlςΠP' †•ΉC{Κpς)2yigJ9Ρε‡μ}trΙαν?ό λι»Q +ϋl8―²*Αϊξθ?V¬γΘζΏqWΩεΛξΣ‹ύ%Bp:ͺ-D³Q؍ jαw[H³†t½–\ξ‚Π1…UΒκ³Τ£cɍEΘQΑ:JœΌωΗΞ:«žΖOš²τ•Ύ΄‰&7Ÿe΅«^΄"QU‚hΕ hβΤ…F¦«—άΟzȈ•ΜΣ§ ?»°ΊŸΨYυˆΈ’uώΛƒκθiGZ›c΄%uΪφωγ—"γnP§x<„ΐ Ζ·"΄)Œε=mΏ_sΨHθ”>sB‘―€—] Ο7{J—Θfͺ;•€–«@&"γœ„YΕ΅sxGBΆ½[ΉΩEC*SFYΠ&C4b +–«X ΗX«ΑξΔ«g&O@m(ΨOΖΘ?χΤ< ΒUμ‹™΅€gROξΧEˆθ( §Μ\ڏ¨χ’|~’s|'4/g}ΥΆoΩ°¦ͺ'σ­ι,1bέξζ†ΐΐ5Žσ•Ζ•&=2_βLlσ›cηιW΄½ΈD½ŒλίM)‘/ΈϋΑ’€‘RfUQ΄9OΆη)?ž±Sο’΅†Η‹;}†Ηœa<–Κ„,Ω•ΉΓ0c%ΰ‚5Tίd†ι² $p€A oδΞad‘y- +ΠΧ Π&ψΗΊ—Q£kέψτ,=f^¬bμΣδΡ_+ €jΕE―Ροή[δΑ αΔψΈ!y"Δžλυ0΅ξž°ΜψpAјv JΌBπH1˜λ—§|At·ω„v@ΚW@|έxΐc¬ΗΜΉ‘»ο “,ΝM8δvͺ¦O°Ilj γΪξς'$2FΧ‰Ιί$ρ`KΔ[Zκ˜―·ΟΡOΜ' +ͺΜΝ,‘‚Ν*τ±Œ€W—QjρzP+ψ*‹­xΔέš)VkΗqsχΕχδŸ/Έ95τ]Σiέ 5τ=s4ςAί<Ήςώ―X΄ΔvΠΚωž›{υq€ΗMc€ί ΦΛ +PVpg ΩaΕΜ)°’©oΰ Yyύt„‘±3_π.Κ£|M9τ.hήa˜KΨ―YqΞo7§NXQHQKšgν}%βΈΡΊ Ϋ%-¦@ϋ?͚Όζ€~'nΆ(Θ]m ‘P°š¨€Dψjuvk™'©ΜsχyϋΥΆΧ Sβ6˜λ42v—Πm―ΐΕγa)½ρ˜.Uχ "ΣSτAjT)'U0xr‡jŠZ…s{PμαpfŽ^c™Ϊ™wσMzœ&― +(g[–}ͺ4γrτ.Δώ,s"2'§žΫ^γχθ@4=jMfΓ¦ ˆΑιρMζ ΣΝx(>€ž ϋΚkΉέžTtHpQ¬Ίx]—θ·)€ΜΊδϊέέk?κν“Y;GFχZSšZ•ΜϋŠPΡ‘xΎτ1>2)H΅VΈŠQγ[HΆηJ“5Χzw:WΎύ’θΑ?#zŽs}175k_x’Ή|Xw0‚‹ςΙ™9l$Μ^%-ƒΏΛΌΕu%„)[½hΏjN]Saͺ6 ΒU»ΏšWšε؟(§Yϋ4ΣΜ1aικY?.Ζ°sΌνοϊG{RΜω•Tob³μΌ.x³zΡR3Ρ<ΧΆΆ ΰσXK ξ«&mΩ¦Έz­hb)4cڜξ™)9ιν’^‘½5 XG†w˜δΐ·‘‰2v.A-ε!a­=Ϊπώ\·ΆCXŽxv}ΣΤ?~h2ΐmošΑΎt– ΞΈΐΟΑhΒοwή“"Q“–8Žκ–ςzK{³λ Hψπdϊ)L½tΠ€‚ό«{Ξ{ϊ3€Ι‘<«Όδ"†aziΰ.O£Δ₯†Ξ¨v7γr¬{³Φ>ς΄†ω΄pd0W΄˜ƒ S Aϋ~Ϊ3{+Ήgh^Θ匑z|υ –κ₯©_znG+ΌώυΡ‚ο~=ΟτΩ¬€RΡχ‹ ¬U0Ί_ίΛΉΝcZώΌΒȟ₯²2oA5Φ…ΗΗΘΌ­WœK²bσ³KυπίΜuΊ·x¨EdO§μ漘T_”8<χ^όYFΊXΡΥ―b7SW=¬Ν—H&ώh–WOMUχ„r4+ΰšT€ψΞψ6©Ξiψͺ~πK:j.†(a0T›k—ΏJ%α–K₯I@α%-mG2Ό#ΑΥε”PζQYNΙ½]ιΏΜΒ―%‰RC1|¦&FŒϋΒζ‹Θ{ˆ]Š:?HΘr>q/€"%Οfͺ +΅5ΠE +>”,&PΊ'hΣ€υVR1­ Ψ φώ©Ω•‚FΊ]œ^—ώ=άΤ—Ε ‚ΙQ‰³ή6ξkλ―₯9¨’εO}~ E·π΄TδzeΙ½Α0rf,Τύz,ΊZBΦξξQψžthδέqN7Δa§LH…H »›Θ拏°‘z‘7›mϋΉί›½5ΌέΞΓΙ£~–ΰgΝ«>‘ζ1…ώέwˆu¦‹V™π±*A8:4 eΊ4i­#Hσ(€ήΰΈ³ŸΫφBlΘφq©‘ΗO΅˜š Ωo)šžn”] ε&eLλ–Lζnχ λΩrTg|‚œJά•;BΓPƒV[•Β$ο~ςQU©c‰Κ,Ά²ΆqBZ˜Β˘ +rŒFDΦQ3ΰΝJφ mD §Ϊ©ζς0½W³Ήέt’&»^0}Ζl[Ω–SGΏύkI΄Ι%ω”ώŠΘDΐψzθπΉX< vύϊ€γξ^τΏ€έΰύšG'ϋ_;ŽmX}Φ&ύ:"ΉώΉ΅^#CEΌQ΅bom/ρύF.Ξ›•ΪΉ’Γ¦u˜?  ΌΫ†α₯=ό›£’ρE§εοu7=j‘£aq_8 +vZ80J μΌIKυΊάyΧΞ^“ΎhY"ΓV›–e±›‘¬-jB¦ %`.d|wΜRŒ:ƒ»*ΪΣ₯Έ(ρ4{±‹θψί¬š&=σ@›Π’\ၑ~Ϋ©]›O2BŠͺ&LΣm<3¦V|ο4‚kξ­XΣπf>­…¨ΆΐΩHλΣ/h•μιξ*Ά5,μΛsŸZuyŠ‘‡Μ;₯‘Xςα΄“nύΈHK[Sgu2Ψy‘ιx€ŒΉΆ5ύΞρsM:j76π?x+ΐ3I`:'οΒγ«€BƒΌ€’Š'υL–>‡h&οέ5#I>˜j6+’ΘΆζTY IΊšIή|„lΖ;άnt‚#€Ύ +ϊ3–|¦ 7ΫڈaVŸϋzd²ϋαl•@Y( 9ΈύAhJƒˆσ•Qϊ5ξ΄τrr@JΑΘ·¦2L/²WΜlωΖ’ψς‘(7ξ~₯ίθΌ;“ά7JδΏμ3BE(.YU§LLš4φwΉΪ‡δ3z7pzύΙΕ#W¬cΚ!< 8ψO%Οp ll₯Ϋα5ͺtiέώ`ό +ˆF@λ .…θςΟνυΏ}ΉΖ37Ο&ΣlsĊ†ΜβdϋzΎu[Ό±<ĐLlƒϊ|ΐςΏCkpΡE,ωͺΪ—°N!Œ5θΊsΨΓiτ}βHιf—F 9l¬?νΐjΥS<^σ]ζ% ΚΝΰΟΰ<[?:GςšUo˜ϋ’})[_Δ‘1㠌\δ–~ž„‘ίΥ-Άθ:φY σY W ]‹Ψ€}FaΛπ,ž80œ:•o ΉΛ”z/ΣΙΠ’GΉq;-τω"„‘;P:e5θΆ–eΨ©―6¬λ/‚ލUίχŽep3,Ύ8ρ~ί΄Zjq‘^jam₯,ONMˏJΧφJN~ήTτήά”hωžcΠMLΚ–²ΰ]Rs§yΝΩΪΩA{i“ιop‡χ!]Iߜ@Žcμ„¬ ‡Ϋ„m})dΒ§+rΫτ?σƒ;Km‰γΌυkŒ φ 18wΟLUοΓ―ρ”—–¨Φ]ΚΝ¬v©‰υ£`0 gͺ¨Φp°$λΘ†άFŒ&Ίώϋœ, ―€Ž–γp™½£ΟV…°p α{Γά,_—R¦k2λη₯clΖUl2΅ϊ€ͺr»ΆgΈ·ΘN’Μ$/ΗE|r±CΔΫ°Ό©z¨1τΔΤ’šh? g‚π΅o°<<(/hP—?Φ»jeiΔ)§€Φ«†S1M₯±σθ)ΌέΉΔυ“)΄£!_²Δ>UM ›‘ΎΜΆt=œδΩ$' q±Π ‰γ]Α όγ ͺΓφ„v3ει5΄ί+MfDnXο…δρ{69t،{gˆα@­5ˆ{Θ½—@vV}hώ™†΅Z-T‰„β.YυμΜ( )A¨¦iβό{NxωŒ4θ„ρι™AHlvΑ²_ΕλΎ&‹ΗΤ¬ψ56fγί8 +lϋάΑKwWP‡ώ4Iq^4'ZΑQŽΌ•³Υlˆή"γσeHGό­žAψ€}‘_n$!ΒyΪώ³ΐ_87\`qžσ6 9Χ=@ηC3²B€ςΤvKq +―ƒ‰Ι­_™ΤQƒIe3· +Δ°- τχ%”eN΄oseΡΟmo¬θŠfM΄₯{=»ο\΅jO/ϋW= +RJ©ώdRxq@βX΄!όώ,™yIš+ϋ:¦ξίς>mfž½rŽwH°V:4ey~a™EύŽ)ɝh nμόœ!A”,Άγcg0OTad>ΫmΚ‹]₯ης &=₯6VhD³„šλ][’†³§Π©0φ›ΞnύΕ?UΡάΌ 55Τ;άιζ»§Pj)w8 vUεAΆ§”Ε5ŒŸZΎM:†[«hΥ[Άی +0†δiΊP ­©’©δΠΰΨw?vKYt`?CΔ’ϊ¦!Ξψ8e΅Ο“UΜo™E]ό¦Ωnͺ&‘xEο iwژ1©?5Ή./6ΡH +RuŽΗ]’Ξι€eκZ͚π·‚Κ΅Ί6;,Tρ‰2ΐ’χ\”s=xY’ϋiγρΨ]3„CGέΤdΌπ_γ&lpŽK ͺ―@Έ¬zOfC­"{I2Ή;\ϊbΌ (ΜUήπpΫgiV φΘ‘Z>’Δ=¦Τ:K4G‡Β·ί%Ÿ$€ Wˆ“rW$ν8ίbIŠΉ ο#gΝ€Ο»2˜Ε3`|ΖAΧφΧλ™ξω9Ž­ΰ‹―]Lςk½V!Αf9;β¨@n',Ώw›WΥΔνVTPΛπ;4y_ IίηV ¬de˜Z«ŽG]_μV +κE³η·YVφ»½)U˜˜σ1έφ:Dˆί˜sΰΎΟ…ΰgΪlΪθUΚ`Τ–γ%ηoŒ6(wκ֟‘ΤεΏ`ΗAβ=RΆyπ—Lž ΗΨM5μSzΫζάωL{œ6ΞτΝΕsYάΚ7]y_”εΌ‚[ΦmMX—w;Aαqύͺ©φyθ.sAmΝόΚn܊ων΅*oΈϊηη₯.«™έ€ωFimP|:SϋmΰΧΩo“&KΏ6Έˆu'!1¨«9λ;kΙDͺŒgŒ0ލ±hKD‘OΒ„!>ΦΚ ŠΊ3Ό―χσοrΞ³Τf;Ζ#^OΑ«ϊ)4HEž£QΓD˜k€„š/nͺޚ…ωνœΑ΅CεΡ/›†# ]1O›2ΌεLί1нO‰oR4*ς¬BΓο!’<<υο”’e’<ΚΞν}‰ˆZP œ±žWΛ’ύ@©UŠ>¨Kgΰgo ³‘›Iτ`#Ο½V¨Γ]­•GuR:#Ύ³ΞyΞK?MfAB\Rξ΄ Ά!”κIUzΖ’>ιΛ‡ΞݍR‘?<&jΔ€Λ@ν­Ωs«ΰŒ΄2ΕοD· ψκ η‚“˜›Λœi“€Τžyχf±*OlOÐ[€=σ‡ -χ•Žό+―­ΊΤl·; ¦Š37r<Ά8uΌ"rijXΖr0'•λμLτ§τρ-aλŽΠ&BxցsoNμ~lΦYC”ε’ϊW˜5ν²ϊχΌ½O(ix›ΟΧ.–Κ L•Œ‘σ¦$§L)XnΝkŽa^<Λ„Βάζ³πjΡVcJGΒ`³yS@sηΪ€ΑT._Υ±B,2Ύ‘΄œ%>PŽž¨VΦNUv\+4˜pž—^½ΐv= xr?=φ{ξ;| ) τΚλ0ΙIz±aΨΚΕjX"(2•’Ιƒ°Pΐφ§Ή xlιQSŠε­yrφ"‡^|Λ¦§ή%ιΧLZ’mYƒΫ… ϊŠ.p^SvBΦαΌŽRχΡΒβŠu)XSΦHΏxέ¨ω1–¦š\»j{εC΅ˆ(νΉE»Rρj-ŽQUbz_x„½ΚC…œ)½ώHΧ±ŒΨD #FΣψΚΓχ&{lMz–d₯<4޳qf0£Α™Mώ„έ;HύšνΗγΕq[’πΗΦκ ΰAΤΧω jΈE Σ-Cγ…`ANey¬Ϊΐ¦—£…bΑ +Pδ’‹Μ Χ ΙA SIΨBΤ’³FΧp%4ΪUΝ©„9™«Χ‹cfCK32ΤS`­‡»Ÿ·α{β§0R±„½ρ‚ΌIQ2•?WŽ:–j’Dϊo™'ΦΑN7CrFr'tΊ^ŽΞ›»-PwΣW)Q»€U†±3ϊ r±όΠ&χν^q%Hθt*’B4έe’oΎˆ`ε|οCnΑT?žζk|τIα‘:’“ꆨ*ώV½Ν߁ δ³σ½΄ΨοBΟψ ‚Wsγ‚^FΏ¦42zΉŸ6 ‚‚ν–½ΆοβΡvi†ώΦ ©‚`‘ΰEφ³Gνξ‰Eœ>‘_ˍQ*nΏ(Ϋ~Σ`ͺw:ΖΔQΤ e}₯PqpNόd½~βΏB‡ƒ°_ΎΗ²8Εr^Bϊ,kE2|O…Gψ '²§žϋΰa&xφ–Γ8»,Q,υ|n<†§ΉΚ˜ysϊŒϊ“reυ’ς‡8₯~Ά­ƒ½F‰50ςxP)gιfΒ/GQΥΦ,πyπΑ››lŽτVwš¦‘A,νλN +AΜν<)d”LνχΨ~6λΉYΊν(Œ/Α™6Ν¨""·²’λό '›4^ήΕω\7_OΰrYv3‚=H?—f?¨C\Ν£ςIΆ¬Υ]eAι,‹]0` $Ϊ7ΚF΅;™υ~A¬W ¦ ‚σΠ>ͺ §4—Š f@ݐ2§Tx}}κŸωφIP'8κTύςQΥ9©K]dH*©\ΦKRn=U#e“θ„™Š°E:·Pιχ[‹Ș˜z•Ϋϋ䢘]ύ‘W#εΔωwώ`o}οΝeZg wΎwιbTύ,E$OΖΔΈF\Ψşqξί暸qb΄ Υ Ν–δώnΟƒ­”c}G”Ε ~sŠt7¦qˆXϋOH)]Pόw061δΔ‡όΊΦφXFUΏ ΏY H’)κΌP]FDqg.Θυ€οΠ¨_λd«Ν¨θΖΚΫ‘ίΖ†k΄Qˆ‰έ^Οά=³²Ξ ΞVCNyGΐLΔpkMΒ‚Υ±ΧΑ‡Fš όj–f•ά9&Βs°_Β}‚ͺ·iI~3}0.3r7_nΥjίv½ρA r|xvY@Ν#ΆοΪLψ.Ρ _Ο&:9”Ζ,D Ων4νΰπ¨X·s9^΅7—ΉΟr2y ΆcΑ Sθ$ž~…ΕΣ€ΤGΐΙ`ͺӞˆZθ―eη₯dˆYηOEγΛM»c‘Γ‡υeε žΩ/κι˜MΚ#H IEϋ<ΞΆ?έ€`θΥlζΌ&Β –ΰ’„ιΙΑn[“GΡJœ$ ύL€kΠGq‡eƒΎa›ΘΚc<]Τx1Έ~¬d‘ύ¬VΦ€…κ9QΝΑτ^b/[β+μpΖoMbμ*θ!Τ Λ9Š{š_θHĈ#fηPΓΏœ‹Z½τR{UƒΏ%σζG:ΓΰΟΖXցˆO™„„NTxβΉ"M d{Fžζδ0œ¦ΗW&ηZŒΜξGά°vŒP³™ςό˜v0‘^±½-’!±˜ +}3K?x”Π{HΕύ”eGi ωN©`Z(%υyΐΙks}ŸήΠ곩4q œΩψΜ)‘²6Ρ0AM(<™©h¦(7:[G=ƒ’ΔΉΑά=Ι[rςΉϋF*΄κw€£}Z†ϊƒΣcΎέ‰«‘Aα£$ςs鳐ψγ”eP³ορͺŠ‘ΐτ$¦[΄Kέ_κΫεώyΒ΅…‘ήϊ}‘Lι€+ν°OXB>S–±^κ—yh{ε¬ΰΦ•X{«Μ}nΔlζPΡΓ¨m©Ά1ίŠmΗ³OάΑ5Ά₯””¦ζ/#M*=νA·<ΆΜx¬Q·M Ω†h'O)Ήετ―lεπΔͺXεΈ )ϊδ,l1x:τHŒjR±«ƒ •δhzk!jΒ|„η~ž˜h§M°ΡAςX―՚Μ|τ#^θ„ϊ’={b/MΒiO7hΣΖ5Πϊρc`Qε™Μ jo;7pΚQ―­Ήβ‘Zξ†bΫKδ£³Xιl@A]λU" ϋγpzωκ―₯@~,Smί—|ΰD8Ά¬;ΙaM=ΦΚΧΦμ9ωφUΒΊΎQτ}ήΰΟΨχ­Ÿz²ώ Ρ6F&Xψ₯ξΎKϊ7› gψ0^ηθ‹bΙΘ,ε`΅/QkΤvη\‰ΡΠB%›ά§y&Kχ).ςέ;|cχEίΨσΧΞnΕ¬ΣwK:ζψ"c4ΖX²ΚnJY܁۽ +:U’Q©Κύ¬:Ί wžρΒ―Q,¦οϊJ0.¦i Ι.[bno ΤΓ²&D*‘0JΊAΈ’'yš+Ύ +γ^ΞΚTtZκT/‹©ΰPΤPšZ4”«ŒΧ*σΒΗ£ŸmL@νJG3\644πFΞ©ΫkNgνΧ΄σωΒΜ7·„“ύMT¦ϋζs@’§’χ~Β€qΒŒ±lΕρ΅3Z04”pΎΗs€L2Wnߏ0j.#ρ”v MKԐ}žήr=υ·1s TΉ$†Ου€ Ÿέ*]+.³%i_FnρN’DΎ6.›ΈŸί‰=7i F€fδwΌzσNύΩHέΒ +Qύu›Σ.α―9ώAξjΣΤ3AšΠ€ΝExμ>‹›q=šb¨–₯ΒίM³Q~‘ν©>—5œύ6!H…TŒ>φLU(|ΈL/W€KM+ήΓ΅@²­h§¨c’νž.Wh[πG‡e³‘p\£=Η`–ΑΥ榻ιΞ €άT·•d”"Q5c‘¦gxt±sΩ%ρ^Z +»l“MμeύΑ»YW/ξΣΔσ•cUHœθTύ΄γPψlΗ~ςs 0©Γ4°»Q(‹ΝΧΞkŒ!‘؍½Ίώ©7`’Λ΅‚ΌQρΞΆ„ψξ±θΣpΒχ)ŽλΑDzyŸwΘ…AM 7šΏ’š ΞΙ_4έ‹`«ω½ εά4ͺω’±ΕbΗ)©ςaL λ'ΩΘ;ιkϋkΫί.ϊp0YgΐγΒu«eHρυUΡ‹·λ·TΜŽΦ’7z’ \%·R=όvΔ ―†—‚c”Π­@ίμ―±¨+d +Coϋ$t{rJA-s•YBφˆPΎ6T{’φνžχhΩπυΥΒ[ω%,ΛωϋfΩ&Π…eJcΝLΚd{†MƐό€”,kγ<˜6½φΓ]>{ΞvAdφΟωΖπDφq £…StβχC€Θs­„ƒ₯€šM˜Υ.XΫΣμΙΒθv4σΊΛf«›©:•‘Gg>7[3ΔDlΠή>A#B£s}Γ7ˆΞϊ’,‰qυμ$t§›f |›lYF\JφhΡ~6Ί[ °ώ‹Ε½CΖ,πΘΉ4πω°]ρΡHΡΥΤώ,χλίδpp³σŸ“ωϋΟXBX_ώΔ gUΨ:Ά,KqΆηh +=³_ §7F#ϋΊΠ[/wIγDχΛ–5ς¦ƒ­01iΰ%σι\ΓΩ΄ώ ”χcϊiΔΡΞ{YδύΙ‰vYƒe|¨σaz ν'υͺ{mαΖ¬0n£¦|H˜X—^°¨<σ»ΝJ7œΎ§ Ντ|…­ΞΣφUΙ6±jΕGޘ7ϊΌšͺu₯ΑF³ž³Ϋ*n3‘rNΪsΐXax—ΚΌ₯A'Μ  η²INΤ•ΫΜη“IE!"ΔV₯΅χϋW~ΛžΙ'φ0·ψFfbW#π0”ΐ—―cΐ ΈZΌΦPP£Θ³Τ='¬ψf‰7 +ˆlΎ½Y­λjY"―₯$ΥuYyσΩ^\6zύ²¦4_ ΆMLθžο3f½X­ƒΐJQ ‡MΞ `λ~pF-†«…Β—­¦°Ÿz ϊU|°ΫΫuf%!„VφΚx£\˜Ζh%@Δ‘±)T`Ν£Ύ₯}}οΩ}׏_φξΛ)«ZώωωΓσ2™κ²”μN¬θX½2ψ‚šφžτŒ)…ZŠΔu’~†Φέ«fE|μόŠDwΕά#ŠCv/?[ξ.ž ύbqΰ›ΌηΡ[ν”œNht΅³RΌΝΌ'™ΟHAύΉοZϘτΎLcƒ7¬π€{ΕVώ’Δ½Ά‡Θ<Ε³V>γ&ƒ"<Σ―ξ6ςΨ{‡ΰί~Κ3ώαp¨ͺΔ7>;YM΄"*MƒΧψ’Υιm·π/ )τΐЉͺ$Ίq{ΘᚾQΑΐφŽ’#­W0Ω—©c$©Δ˜Ύ4;LwέJ2V€οΞ'Β™@ΠΤKΫΖxŽ=ΑˆΪ‚ΤNλxX ‹Ότ4>DΏE–uπ?·1Ϋ[½ύΘ€j€lšM[7θ|ΜG厑"P 6δ6(Ψ[άι7_Υ·-¨ΓcΈ–‚ZV]#Q žd +ŠΣ‘³iŽωϊ’~R6’ 'eΡ½A‰ΞͺΪlSŒ‡YνΤJΖδ₯JMjkmλ”δŒΤ]L¬Ζϋ‘$I¦]˜L^(hyθ¨ε½`€ άχfOΚί°£ωN‘Εw³kϊ>ύPnή*ty Ό^4:¨g Iκ:*x*Δzj>Xsρ$€jΊl, +2ΩN·Ω³x&ΖUF‡τωQΘX^ξZP:fΕxΗS!V0Ϊ~jν]VΖaΞTRΰση`e-Žw°υύχŒ@― 'M~¨Εϋƒ)(x­„§TΉ9 ;&žj&ςΒ H”Ίiš!TΊ,:~„FΠ0*‚χ|ΊqΧΐ²~δŸ O2)'pτ~ΊΔyRP‰zaq7R`GΔcΨkl™’*ΏχΧ@ότσ9fŠύ(ψ8hωΣ*ŠY~+ΖΦ±…<˜ΡτΨX¦½ΉΰύΖ1{|ΨπΉύŒΪΠ[άΔ«xYΊ=:σ8M5"D+εQΜ„‡N‘\lxUĜ‚G“C­e8.«1‰ς K +30«³Ζ§>LۜG‹¬Λξ­w) 0‘½μκ‰s +9˜’#η Άb›.c]0‘Mn~”•œ‰Ψ%‚ Λ ˜΅·F<[Šψ™a΄.‘3+Eα­ά2‰u>ͺ–ͺΪi¨PU›"Λή΅{t冉9­Δm—"dχͺ“κ2ώp3~Εά{$΄Kξc‡‡€Ξ«ΙΌ"―¨"cΗ{y­\nομ0pθ½DXn΄©tΊ˜S†[ΧδΠ½ΝΒ; λc₯Ψ7Ώ,|¬ιΉqx£E‚Xn™²>€WΥΝWJΖΦΥΎ3%Zo‰ΒιΣdΗ¬Λ.ΧPψ’K›ξ»‘o³.fp4-»θ +Ύπl{«Ώ`‰'‘u·©ΒεYύυ₯ͺΡΌlEΌΊξEHω¨ŒŒςΙcΝ/ +ΧKΘψΛi±ώ£,—ŽuΧ‚ ξ8SZ<€τα«6Σΰ~6€‘sN·-Ή5Y!²ΨΞ£Ήύ_zΊΓτΘ\‘κB›yύrsοbdμ g™Qσξ­οrW žrιΑ²ΞŸfώόFtΕ• ΕΟ‘™₯ο#Ϋτ α‚ΙEfΨ’-b @8ƒ°ΦωM/fVD¦΅ωkρ+6_k½ @J¦αδœ6"Q» 1J–;ŸŸή2Υ'Υχ¬ι‡ϊ@Ζ›ζ"τ4Ζΐvκsƒo–ͺVμΧΧΰŒxœO6€rFΊυ γA{α―Ξ+.―Ϊ@£a!4nΐ…δέU¨Iw—8šΐy™₯r4ŁqxήΟy·ΨrG±κ(λ\kϊR‹Γ:Πgώ ϋοͺΪ6,Ρ‘ε%V*€sWE…ΑŠinε ³ΈΉ½ΜŠ JΚh―1tέη°’tH8wύΚ=šŒgLjBΫ#η'”3υΓέ¨Ι†Οΰ‘–S‰;Ζ‰λ(όΫδχf!/­ŠΊeAίπφ?-[IΓ†pσσdŠ!臑»Ž)³¦¨γεϟ²`#Yϋ††αcΠ82Oœέ~ƒUžTN-’λτ‹ Ÿϊ#aώ"Y6“‡Νρ/ΧΗμάkσ”΅’'iΆDE$θ‚)(@ΏΊvN2±^Rψx˜ίγxMέ·­†”οπΐC/rI b.ςΥΧβ= ~eΠs|QΫX9εpΑrWLVz’zB”Λe9C/œΖraΉλMoˆε\Ύ­ΟΠΦ—l·G+bheMiQ۟ƒ,‘3ΣαΣ­‘‘1IRegͺ«—‘ŽηςzhYš‡,δLάΜh!υFMkώό΅ί‘xώΤΛά<ό>ήԊDΗRψ«Lr1EʁΌu›O’ώ΅NψΌ%˜7τUύ†ΕPύœΖ£??Υκ=^‚V5Δ–”VŸ«ςG~Φ ˆΞmS€qΣΚ£Ioλ}iMΈδGτGΡT xΙ@ά΅ΌŠ~άή·ΐ΅«Rsώ»’»ϋ‘r•ED_ + % *ΥυΘ†4ΓiΌμ·³Ε€Ύ©Γ,ΛWπΙ­&Κc™uμζW‡ςlΞYP/ΫΒ}Ίβ6–ΏAς +}ΝήzΠΆτT”"œƒIvK’»²‘]ά/7;ͺ<ρ$'υ2βW<.{ΕCνžw’ŠΟbύUš“.χς3:*jιΘV’ΟU:~‡φN1A·ο +# <’οΝ›Η;,ϊ˜P&z²W#»Ώ³~CtrRήD641#HΦϋΣΆΚJΠƒ!UoδΫΡ―λPejT·G?:''“xΰˆΥ,Ύ»₯aΘ‘ŸŸςVXΦσ|Π +ΣΔH¬―4ηέn}›*gh$Κ:oH6X›&ΰͺ’%E 8|l‘Κ‡ς8œΘ˜•ξˆ%‡υή€8βŒΜ« Μ}ˆ(œΡνζA} aε{e‹³ύ¦•Ύζ_ΡΝ}ͺ”ŸςaHO5Ω@N£Hc)qΞSΔ›M'―όqŠΉiuΨ·{ΏΧ©7šO7FCΘΨΔ–φZCΏΈ¨°zr.θέγ£M3ΟaH αDΰ>άφ{ΞΏ¦lΨ=΄HΩ*C‘^ανΦ2Ψ². αγΕδ~3 fΩEΣ½ –κΖΪFΏˆΏZ™·/ΩV¨G;’ Ε3ˆγΔ/R;ώD©₯u&Μ~’Η ν eY―˜αόi¨0Φ*VΟ‹Γ[F˜B2―+Ήΰ0 ψσ†K~‡^λ΄/ߚX―F:Εψ|ϊΝTmεyZ‹―8ZΔ}™bCœ“rΣά ΄œΡλyb\ρ&Η<ΏΌ p Œd#e +ΏJD.Ρ0λAGε2εεμvRΪmΏ7ηΐmΉ20 βFωWΉΝΡ˜$LR&‘ξz¬`ςPelΟίΒ‡ΧΝ ’n;ΚQ«']&{ψΈ΅xP"mυΊT±Βαλ40“όΖΰ‹š,Ο%£ό !tDλ䆍ε¨nω–DήΕ/ξΌ¦Tιι:§δ{ίΆρΓsπι ΰV\t08Ž8Ι“oΌ:sλk'Θ‹:>"ΑC– +“£l]όT‡Ώˆu>P–Yj}ω©€QDξηN”»$O/ aβOΡ›ΉωΥxΗ‰€ΜKφdό†K7L£ΐίTΘ~ύή]`iIΙΉΰ π—{fδβ£ξΰr'» 3ξ†λSAh1 +7Ώ +ꐟΏ™\…ΆŠΎyJΟ±ƒ£gΖVXΠ‹` =G~ΣξY!μ».‘œβ%ΏŸϋί„Λ{ΌœGh†YΎΑς52žD+Ώ§πΠU5T½3*RΤΐvwp·iP`9nώ-„V{¬ΒB!½IΚΘ 8ς5Υa“D-SΒ:Ϋ+€/kP„Β,|6ΙΎ +ZxRBΰ‘Δ&±Γ‘oXα9W†pΛ‚wσ@rςΉςWυη‰ͺm³Ο”Ζ<Ληqν ξπ^F•^ rΩRv+0Ε51«'χ'U’7nΔΧ§ͺ41>-¬μ«6ϊ‹0;PxE{…ρ(IΕjD*W‚m⨁˜―Yέq4ΩύwfŒυr¬ωΊ5$κ*{ιeUΩpΎΝ%糏– ΠΙ½xKΒ«†/ΡyT?†|Ύ6X 1‘Qwsϊj:‘‹wM›?"‚Ϋ7EΔΈJ)>—šΠxq,Ž™H @Ύn\wBΒ’ZΉš ‘£₯<„ΓG7δUΈ·‰‚(_ϊ§I Œη5YΗ)$7•μδ˜$Gώ„­ΤR@Œή Yš―ͺSήΪhUΧϋž{α[γEšvŸ†όϊšΣΧ|ΉOU>ϊ§A1‡%{Βo`—¬‚CYοk“%Χr«SΙ oΦ‡φ(&ΨιΛ4½ —Ό*Πd)uy‘φ‚‰£CυnoWΖsp2 ·cΣJΩo©\~#ήΚ ­Ϋ7JEq™ΐΖαƒΥLŸςy"GxξΖXfΕ §7 ©’Αα₯2ΞruXΖ2΅: œξψ|œ Hsη'\ή +λ ΫΑA]ηe.ΘΜ[†δ"Π½k5ΐW~kΠΜͺΧηςπ7Qθ―LϊΑΦΰ1=›6€^΅Qζ Ώ¨³χ₯bcJ4όϊOΆiG7AςŽΉΜο« β<4'9’¦KΥΏΡrœΒ­=„ δϋλΈΎšχY*b95Κk) eΰοƒ·I*”z–yε7R’ΔΓ³šΊγ~ؐ;΅}©yαsΕѷŏe ŒΰŽΩ¬·{™I”ΰ¬ιΉβP(_etΑƒω²i¨O0λ¬2‘ρ±―[<Ψ•n1λβiωJ²퐆Χ^ϊΏ5e}$Ξ±ηkVK©65μ{3‹ή7υ2ZG8Β;}°½‹Œ mLσΤy|T¬1₯0qΉPά*pύu—LμͺŒVξ!”vΫ<~1ˆ””’«ωΒθ`ΧeKZ­«θ3Ήθ8;žρ;2Ωέ` A@†_Λΐ*μm^Η£*—i«W-ΓΫΘ Š0ι6œ€(’6&XaΜiΛ TΨdέόRςE›Νo-¬˜ΌνΏ}š‡@˜£ΠMY]9ϋu―U-ƒΎ$0₯`Ψ77™TΘ«Ϋ₯„Ό§<ΤΤ ‰M–ζΩήΫΝvx‘_Ζ=IΡΰ\8XΛGύVΓ5οΘuEνfχή#Έ’ň؀―΄Γeϋ6²·™τHW» +ΕΎμρ7ΙS4Ε>Όt³?Ζψ΅r6«ˆ¨ί©vs'XXΉ%iOύ¨΄5₯m~ϝΊοm θφI¬KΐζcŸ«oΰΛν^—dη»[?nAΡύ@_Hκ’-ˆ—  ιζ·τΰ¦V~jς^Ωn ‚WcτώшΙ+lΉPSjΩW;} +Θ€AΊ68ρ ͺ{G§ΏOŽ­eZ΄­KΦ‹Qό™‡Ξrξ0> +bRφΦ‰fήkpο}Ÿ&Rkxφ Lΰ ζτά`^£@υε~`Ϋm¬ΠeΡ5!$vBˆFw; μ»…np‚IΏGν%άz°^¨š6ΙΝΣkŠΏ‰%Š_š)ΔΈ…Ϋ—΄±ξεΝZ;°Π&ΚSφΌ •} +$QIΓ‰©S—ι| +m¬!#q₯n?UΤO:­³ψŒ|Auο―†;ŸυΰiΨΌY@xmΕ6ΪU+‘G5h”“©ΥOeC―s^ό‘ȟžλ 'λΟα/ύWώM·kΎ—ν‘£•φ9· ©·0MŠ}½‡Κδ>~’ΖψYr—u‹³Nάπ»h ΛψiΛZ\ΰ&ΎR8Ξ…λξΓ½‹(ςΪΧΣVΜ' ά&«δΕuaH"ΘBΠηL“˜TH€iNρ -Ωξt.Ωj«’ΡρZM.Š–γΩ„S/,ν4=ΗQυ0YΊοHfs0CξšͺO8έS•‘#gτC9”„₯OŽύΓ³θΝrCφoώŽΪ‹43ΈJΡ}κ6z¬έS—΅'Ÿ¬A¬ώS½~Λ μš\±ωΆ‚δ½ΞΒΣΫ•δΈΓ)Ζd™3=»έMšMkΏώηy»†°:;IΡΤΫ9N χυΰo?’{¬V[;M`6ΪCt"E.γμeπΫPν•PDΏK:±έ’v@Œ~Hψžή›RιiΚ§°ώ+© K³žΦ!rΆCo›Dœ&ςτ°Η„=\4 *ς•ϊ ’άΐ©Ψ+‹§ΰΜΨΊΏό»ΙlZ­™??„7™ΰ€2Κ0ζξ…>Μͺ«›ήY»’=PŸreτv―ΤTΌΠfνe χ†!ιδϊηνo$π¦·*V½ŽϊYό’ͺϊΊ@z«’‡­Μ·ž”Q|«k; 3~μčŸΪΑ[%Ζθ―Υ;.ΉχcPΥύ…A‚¨ΰDΟ1Ή«¬€s%λNΆEηΖtm‘αVΟπ*!lXΉkjĝƒ •N!e“i—γ:(‹)΅Ά8ωIυJœψΜΘ‹"Ω ΌΧBΟ©8‰”»œ68T{…Όΐδΐ:Δ-CAΙq°Ϋ)άυΰΔυΆJς–$κ• dh–«EIγƒΣΛCNA9?ι>όiB¬’!KNƒ!FΫ3™W›€%cΓ±…­‹•`Έμάυ2‡Ί‡,‘Σ5BjΊ£€M¨ͺα°¨Ή#Ϊ²€ο«`μσ¦πP³β^προω§L\ί~MiθΖIˆeχpPΓKήԍύΞρœe+›―€μρbYވόDέGY2ΈUΟ"ΞΦ–½α%ΐΧKG‚+Δ LŽ^H7@aΏp}¬fQ`9€κTΟ%~ψΗ€sθ{€oisN|Λ5ΏG¦ΟRΤI?a‰… ©ŠΑ|]Έ5·‚—.„ΣυCmawΕΡ WE:ΊΏ‘+m>Κ{œo Π'6αIΧΩόπŠw=Nγޜ/σ¨έ‘ΛH5°Ž›lμYκ.Π Λ­ξ¨VΕ2λ©ήŸC[&φΪΉu£ΛΗVqa0›ΓξNCΰNZΉž‚X.‘‹€Ζ²uβŸ,—₯†‡α|` $dΒ>YυͺυΛωτV­Ρ]QχΟRKΏ+τ¬»ΉY‰ϋEkR|ΪυKZdю Ÿ MΨΐy{₯#SκU/`ƒΈ8[.°—ΥζΡ‡χϋΪ»ˆ)ς†P©Ν£―βeνƒi0ŒoήΠ!FυΚrψή³Pͺ_Δϋς’υvγu³ ΩV1+%€’•TXsχ`εΑ#'auΙV²¦M±s#O» Œ“,,8-B‹dΓωPd|³X—Oj«βl§ +ΨY7φhWf°GΩ[½Άsr’£< Ζ£HόGιΦ΅” +žΔ%iWpμ:Vp½_"Σ;Δ`Θ §œΙ|>Eƒμ|οY΄dΆWL–a^ +¬ψKΖ!Ω Ο>]49Σ +I;2*±ζοΐ(ΉSݞ±vA*²ξΑA·8­Hμ5±„·Ά°Fo/Ίy+vT¦#κn eχ€ +-ƒ'v€•πΣo|ώœšPŽ@ή5xs”ξœ-ε²ΰψ«ΏV ΈUˆϊžœ˜ΓmŒ¬_Ζtί’{) ΌέΉ(ιϊAΚlFΧΝCΊ€Χ²Žt'f=M“ς¦„$›§#‰Υ!ΦyNτΎΕ/Nω€(˜²’ϋ?²Ξ©W₯ ³ΫΆmΫΆmΫx·mΫΆmΫΆmΫΆζ›«IΞό€Ύ©€ΣUyͺΧ’Œτ»ξK‘ΣδϋϊEQ š?ǜδ@ίVαE¦ΛΞ`ƒν‘ C‘w5©(¨ƒ½£M¬›._„rqŽ}!CΟB‘‘!ΧΧ©|ΎsΉΣόŸυ@‰Χόmbσ΅ΤΣ^ΉXΥπžyBvCΙΦ‰Φΰ2[~Μ‘ΆεΠ <χX‰Βk.`ζΙ…›p„§υα-uοyγαH ²6Yz’”ƒˆη‚KιE@ΜN.’ΐ\W-„2LTΆΜΉΐ$₯f;;^·Τ°4¨±§Τ7λ_;-Ώι¦ͺxဍ-maύΓ§Ό¬—Υ΅οβN έΓτ€έDœ{B_Β―mΚ‘ΎgnΒΨηΦbn£j,ΰKV,΅%P™'ΪΛ T% Β€76Θ K˜ΰσ9Τ°e€a»Ί˜£a;ΑΫ9};&spLδqυΑWΆ’f‘²΅υ5ξθμ`–₯hhV–°!‚‡=ΟIŒWό9Ε¦Y‰λQΪ«)eΑλτΤ±aυ,c-1yλ«½8ιaΕ>«ͺ“L£Vσ'υˆyΌΗߎ }f…Ram_Υ5֚ξΩ%CΔΏoRωY[Ή?sωϊΝΗ“ϋx˜Έφς]ώ―γCκ±ΉύBσ<Μ;yΠL±R―οT»Aϋ‡]0Ίˆm#λ컞 ϊ†ίΨlWPy|ΣχώΗVω¨¦ρ„°; ·3ΐBΛ³ζŽΐ±1—W܌ΧJ Α²«pΖU˜ΚF΄•ΙΞ›M• Gj7Β`α†%ΰwΉ€ΌΎi,ΆΒ”ΦܟAεΨ]—"¦’PK άϋ—>“τΗn08Ÿ@7Φ’(XUνΖ~Ιήη*Νχƒ,~ 7ΒυΰnπΞΐB?zτ—,hΝ°ΚFF]σφͺEH±μ ϊ[ε,ΠN „ιH  +6=βL™z! S„X•@ΎΓΫΉ΄υBΌχ²ιΧΝ./AΆ‘&(΅ΰαΏ`ό’βώ:h f£„ί•φaB’)ΠΰΣhΌNΛρtrB +ΈΡΆN8iiŝ…GΜϋ0•JθΝοŽ7Hb2fE-@ exr·dUdΦNp α@ρhH§[ά&ΉΙ,—8zz³™~ζ$9y9!dF–ύGΑtΖ/yrž32BΚ0T4xvQ…φ·gχΔNKλΙςΚK‚&ζ}ΤknΨ¬v©δVˆFΦOδœ<¨ΚN 2 1P…―Π/»§§\sWεIiλΉ‘ΗD;#4ŸΔ«m°πVVι/-τΒ΅ τ{b%qƒη("ɟ¨»š‘cϋφ‚L­±Ι›χoεSχ' Νjζžϊ^2’Iί Π²;TΤΫS'gž¦šM]˜³ηΠ\p:zΊ‚YσT.φuχΔ —.u09ŒεΩ’ϋͺT#Eht― <΅7Φ&d?ΪΊΗλ΄9lΚV9ΆLš$d:~bΆΥ©X7δƒςG΄s +8Œ€ψφ4t7‘UK0`CεΜ΅₯d0+λ›ΪσŽυχUtVYK}܈pε†ΉΪίψΉr^•θ›8ƒΧΨχ΄³pf\S₯ΐ‡ŸγφŸ¬, +.υ$5‘(_Ξk³Κ™Ύε9ŸΕDgΦ2βΊοQ!Γ)~=žTše’ΦώΑΜ7_ηyu·CΚ.%h²Œ•ΩŒdKΰβχpδQ}s]d aD€ύ#ͺ$ΧΜόμX7χgΩ2r― +fϋΤΆΔ…œυIIw5Ύ7Ζοxα»Ώ­4X·ΌI'¬†* χΕΜφ’μ8˜;@‘+Ή7НmτΉkHσϊΕ…Έœζώž`@\‰(mα..’_@ΟΉχeάO9Ik ΝRδΡͺ₯3qW‚&ϊδΕΗb¨Ξ +¨%4‘YƒΝ2―ΨΈ¬λš}Χ΅Π!Η:[4°Sώ`W=QwπΊώ?œ^Θ] ¨ΎϊxH€έUKžνžž.ΘηϊqβƒŸ€8"N[°ΝΈIτŠ ‘Š'άͺtSΒαΩwΎUΫͺς9TzπΝοφϋ#g¦ŸR€&iζŠμOϋέTΙ;ͺXδϊ`hŸΜΥ\Ζ䝧bWxλLjΐbq(ήσι}J„Ψξ}P<ξŜαΙrΒ9΅‰;ʘNs2Sύ‚|-φ‰ΉPζΆΚGA_ϋQ™KΨ‚Λ%¦κζέα 6ΐRxΩ΄xI’  V±_4,Χ}I2ΏΚΣ Υ`™'oλ{ΎΛ–.”– n!?JbΤϊ»3Δ~Μ?K¦¦θsÜl{‘  OΧ?γ“Υ͍|¬½³}²(*ι΅‘q:YΞ8u§Α€nθΜ!$c?ŒYωξ›C¦z]I›ώlDΓΌ?&ΔzΑύM%³wΖΐv΅ͺ›ša¨Vχ/p†VW/ΆΦvο\έ4Ωυθ=–έЁΊ7ί•…v-¨Hϊ rΕθF˜«)¨9±yxxό‡ΝEΟN"77|­‘RζΉh ~|Χx»βyŠ6ήD`τHN'9b- w–ˆΏT½ΫF}_ΓΨΒ>„žε€ΞαšαΜ7Rβxύ+Ξ,«$εXt™Δ?HxπƒIΥ5ώΘk9½€έΖκHOΚ‰AfVݏr²Ϊω„GX5Ά. a›\₯MξG†“ΤUΉύ‘Œ·\Ÿ”μ {cεuiΔE«όŸ*Hf’ωc_Dh(α« +ζ_Ȝ8άΈ ο’χx³Ρ°…ι ΘΕ'=˜uG―GfdΡ1(₯|GωΑys7 .τ4„VΎ/.ο{ρ2χA%Bξ,>6K…―ρ³–‚TB!TΝ#ισb³|€ΪθƒΠΝκι5Vύq„šΣƒ9€‡Όε-|ΪܘtΩφΈΠˆe4Qƒ"¬τγ7½ήιogcj³—όΝ-k +Hϋ"DAΖ-wF +xןzgD°τόdε―<©³ΐs—†Mά5„v8ΰ‡ΠŸΓlΤ‡γVΆΘΙΣΜQh‘ι.d~‘΄—Δ˜ϊ¬l6dρ«Œ«lA}—Ν»’*κύ‚pάcˆ‰χ]Pί)3ψ4QA—‰Ω†-Α7ξPK"ε€ /Η +…}Ψρf²„›¨A; ½½αιιε¨CχžθŽ=Φ.ΐ³Ήͺjΰ4;k¦HpT Ξ‘d<ΆzΛνwB‚΄M5½¨m½nƒzρΝrDA―x0ωœCΖc’[JH#`΅­Πύ”ƒ–Λ—ddΈ =σS³Άomh2w$ζ4οRjŸ―Δ”Žκcό Η7:ψςa·+R¦.Ώ *›%«[&DD]ό7άPυΧΚRͺEβφœ$j8 +{b{νΑ ―G!ŸΆZηϋγkψ½ εƒRΔe§Z‰5Γb™ŸΔJ”;0MVΥΏΐCακ ύ!£}§]9]I+ύ2c­βδlU"·Af±ώήu~“=c‚οbΥEμέ2ΪΧ­>'ε~·6­‘Sψ»?Mm d½l…@[φ-R•@αΏψr€΅ι\‹­,’@ύΕ)-|υ`>?8Ψ ήμlE΄Ρ Έ¦•χQ₯†C‘6? ž i2".φ$If₯!²¦@εulF2w,R'²mD«Τΰ½£!;HζpmίΖ|hέΎž(06e +"r!τ‹PNσ%1)Αͺ%U$­¬(°nΘw1„‘QΈ[’‘»Χ¬―±Β΄tΙ2‘­Υό¦rέBΐΆV―všώM ™:iΘ‘žc10,Œ‹!ōσΝb…Φ»έP-έωo{`x#Ψΰ„€»ΐA΄M@¨‹]Ί4§ƒ bσM  ±qUΊs]€%sœ|φι#Š,ωΓ£°ΩΊ₯aω ZšΐWςΖ=To{ωπK6#SΩβZn|―Wχ ˆ‹)~Ώ&MLUZύικFΆ)(C3"$Σ9Ξ°Ιρ­ xΉΘ₯½ωQEΑΨ[ „άς¨AώKΨ@Ή|Ή$ιNσ²ΩNO–(μƒΦ7Ο έ¦k6@˜2πκΨΦψΐ“Qε;zt<…= λ/֍ΆIτ00³₯₯ΆWhrΜ Ή Aλι +BήΚΤƒρ9dTbβΡ4y_ƒ»³’Ο I­˜'Hξ°έυ0 }iτ†w–2’«O™„2NuΝβ/:ά₯3K/βφΓCΟ”Χδύ»nbzΚ-Ր#¨ν‰`[qšš3ϋ.ƒ~ΨW„Ιψ> +‚ΊϊQέ/d+ν5ΟωY_P‘ΥόŒZνƒ#HΩ‚ήθ ¬…B™¦Σ@Α‹p9p²`8ϊώυΑhόcΝ3³Ώά“`β―HyΧKκC½‘ΐξΡξ_#ΑΟωSAτ£ pόβΠ1…[‡υβD₯?φ£Ψ]rԏVSύŠ%qΎ"•Χ’Αΐ„π +UBβVΊBέΆZ?Ϊρβ_ žpθ„’DwŸ²)A%{“Ψ^βΜ>α ΕΏG(¬€χ^‘‹RNχ4="žAξ-~[ή]˜,―uώKΌ―“ˆZΞLiEΕf’e$ψ¦‚3C–|Ÿ‘Ν6αvχσ~ c•ηtΩ‘β gl¨ˆΎΕΨΨPΕςaΣqΟ8€cΏ;}·"X™Χ\ α;œuΆJšΐ|xρΊrΕK³zΩHNDp‘Yμ΄ήΒΤt9δ}‚Ρ7•πεߟ7(ΔΗ–eδZ,ΚtρΕΓ-©ekΦyΙ™©/K΄ΈλxTŒΝƒ"ΧߌΙ~ŸΐpΛVΩσ‹ͺΛs&ΉjŸ7ͺyΰPί ΣΖ1oEo°’5y‰ίδY >BWμ€Λ&π|«ΤOŽε»πsŽ,uЬS' ΐI +¬„mΩ7#±T²LΤ‹m£3Υ₯‹œNkΫ(«bΛ‘¨zγcΩ,~֟Kˆ― Α+)7Ϊ΅Φ€4€ˆͺρ/sΩ&A-Τ\φψ]Ογ‡Έ^ =Σ° +“™¨ίυΜƒZΗ#ΠZL*'ήΤPμΡασωŸ“αΎΕ„Ξ΅|—Ρ_ΐr 6aŠθƒΙ¦VΕ>Aj§~ΞjΌτΰ‘ πpiς§δΑ¨ΟΑ¬6ΎM ΆM{¦Πθœmώ:΄εsΑΎƒF%g,EI¨¬šŽ29ƒfΦ±J,φι’IϋίΗρZȘ]ΊδoΐtŠf,―/œΰε+bΒ=E +qZο@‚έΝ°I·Œa+B4°‘xGžψ +@Οfν*Ξ +•{½ιvΦ[”lΜΉπψ |±Σ’F:F iƒζηQu‡ #κ{GδC.@,a΄d%Ω9oŒLάΩa‘Η$§M-s|‚Π`؊υήzli”ιΝG΅ψ±Wλ§ν’Ζρ?:؜ϋεΑ©Q½$ψΙ5‹—Ό ω)w‚S¬ΎEοQ5)'ΜΧΎ±mpέε@Όlϋ7)£₯Ι¬Ω―#…rΔkvŸARAΑe†Ά‰”sΎΏw‰¬]ενιρπ€0Ο«ρLεϋ#χ,β!*"[Ύ;R£jΠϋO9^NšϊΫhξψϋBMu$&ަMωΤΓ²ί]Ι™pΕ}7δ<©“P ΏaŠC8|³θ·_$Μς₯ƒIB5Ό§@ߟ°[ͺM>zσ`•J…Œ(λαζΫ‘”ΊͺwώξΤLZΠ<―μ&ζ∝*δύόv$¨εέ+a­f )No?kvtfΘ8 Ε΅‚€γ'–ΜX”; KcWΐׁ£ˆX:ΣζώPμ­§’@ΧZI‘χGm=βxœΓάr¦Ϊ90όϊύθ7eΤ'›*©Ψ¦ΖΟλ8Ψ–$Œω!P•€ΙΛo€}s!ΨΌ%2c5δGxΈ`‹Ÿ)°ιfπ1©mͺΣ*;’―sg^nΩ 1’>ΫΨЍσ” 6,k°/C$»-†πρZ’οU_Ϋ3vΞ>Υ\Rό#^G>2–ΘΓL¨>Α½sε[.9<ŸA:€©aΏJΤγƒ–¬ (kψ‚Až…έ” q£’wu>%ΌjV­ƒ&`ΜνΤ›‰ΡλVΎ+:žͺa¦ͺΩ—N ε1‡ΦΙ&Τn1ΧΤΠOΰΛφ\vΛξT‡Ÿ7?oŸ**&WκeΝ3žώ4|½ΪΐΘ^ήηΖ—ΏGGϋΟ)ͺZ[l8€ΟSΉΞiY‰‡;]‚rNύώAκCŽ₯Χ©γ4T>MUσ€‰ ΓΤ-[/ME%sJ6LΗ–P]ΩΕυ7©uƒΝ-ߜWυςxΖSuω_¬πC7H="ΉΦ?v’3΄ͺ>ΰ k=ލzG€¨l”uζ"’rχM²lIF₯Ο]aƒ¨Ž3ΚΘ4Γ‘šΦ/ZΦΚQK,1f%γοM/ις8δέΤΎ™mxΠ¨2ηβ{Ε€pήM[(°S¦wθp₯δy1Ί +cέ·ι¬ +JmvςX€¦χQiAc\£…nΐzƒZ½Ήϋ|' ‰y"w«Ύ’ψΗ)Ώξψx'‘λΌͺαsA8`σ+s#R„,¬Οk„ϊ|9Λf/8ξ'™ψ[e‘¦ο@ωX=žwςlŸ±ΫΊ¨»E‘>6aΔS-{ΖA6βΪήJE»ΪώOΫμln+n‚Ϋv@±XrcΧΞ™gVMKœDRζ;p&ΟΚTδ΅³^)„ύ¦υ:c8;Π•κ +?ΪύRύ¨¬ΤΒβp‚›c,²NKM?ψ0|Π«—_(€e‚Ά¨2o‚ΕυΜΑά#Gp +~ΰΧQkp<]„E R¦Qͺςέ,rκ¬q4C΄g†ψž…If‹΄R0ΘσυΉx_λ‘\»ύά 6Ά‹$~Iω»³­|‰0 +^ηηFαF˜‡ΰΗΎe°–γόwlΟ9Φ|ΆΎΪυ΄Tιπr› ΗFm­‘ηΠτόΖ€˜ Έ·PF"]SΡ’>qd―ΏρfsEΘ°Pθ³―Β·έMΚϋΊ(4Ξ%δΛFΞΝϋhσJΒjκ]Ϋ[δUr–Σ/τ@ΚΘκ:~ήWΕλfV#ΊΗ₯ηb€ώMεΩTk6+C FρΚ]ŸΛhsΪ¦χω–Ν_eϋ2€Εο+…šVΩB[ΊΌEMξiΚB…†K εΐθp½Ί5=Ξέ§Ί Μ+Υ>χψŒ^•5ίϊΡPh³Ζ¨ƒ‰r˜G“ƒΙΩ†ωΘ54-ΓΩΛήs–”iΔήά]Ήκ“f§hl²6ηΈ?N.šΡ+2Ιγsš,έπ<…\ΈkLθLEt²'₯qX9l'ΤYm,˜χψΧ HpΨεψο·†Ψ%ŠφΛ¬yώ9ΙYNΠ%“₯Ά@ ˁ†βΑ-i=;ΛόΛ‘/.Y|3…! +yΧν+›r@@d¦ͺ₯hδύ4μO¦!ΓuΗσ3`B7Wθ@F·›]π9©Ύ~`‘)kΰΤ†t’(]Λω’ €­ωΐ`ž`T]#Ώ!$y ρS"*ηΟε<>Χ “1™—5Η•όβ„鍑ωƒoe`@ιU%GͺΘηΟžύ8Γ y§ύQΣ΅hv£ψK+ύ…θ§–!KRŒ•ΕžΫλΚϊΘL*|ƒυ‰ϋ°>brFŒŽΑƒE|Φ°žyQP}°<†˜duͺ`Oa“ζ|=\Γ₯VεΥ6LΈ³Κθψϊ‡ΰΈh©QΙ”š]ΨKžΌΪϊΡbΊ}ΰεΔέ*HqΤqΌd/2*ηω“Ω$9Ύ#λˆ§dW­π yΥYWλdgΗT­.f³KΑΈƒ\ΨξΡύ<.…ρ°žcœοR†η4Ό”Ye¨ΐ`ƒ8»΅„†—ŸΌU+q’#~r<ΥL΅xΖπB΄9L±φ†Gͺ³œQie9²·d ΎΕΖε–θ. ΒΞΔ»>Έ#qˍΆ―vΗΠv«*ΜB$}ΣΚS]0xω½’oNŸ\‰oτMφW,<"=Ζ{ λ{Π]U™^>^˜rΝ[{,jι‹*μΞα'prd{ΧPwV`ΦMoοδ^ηcϋc μ„QA–ό?Wmς”E:ΔΎOΊΉτ-ς- ,&ͺ†η2FΝΫύMŽ75΄“ώg-S^ ₯,aDΔjΝ^±|~‘ΉTϊ’š—qUXC6*°D}ΘI–<ƒη±U5o_$gέO ξ#9Έ,&wXQgϋvΖ™oΨ{b ”Kϊ΄U‡7¬’\ύέ^‰—xsu8=k‚Ζ‘ύ‡ΰ•NΨaŸrG,uNό1ΒνbΫ9ς΅£z»ΩΞ%Vμζ”"RxΈβ_c3ͺ}Η ˆ«Žπdύ©…&ώφΕϊβΏϋΡbΝ†cΙ/ά‹dwŠΓθοί{λsι ͺ4U~YΚHYύTιqœB ˆuhζλύH ξ‹"ρs―πΫpcΒZeκ1Ϋχ₯=cή;PAφ6•0β6„@»ζg;ΐb”‘„J»gŽ2“/‘DσdχA ώƒΛςυ£,@΄Έ΅λ$s«[Ϋ·pI—YuάR_‰ϊxagOψΣVεP4χ•‚Ώo?Ž=0%£ύrάq%#nŒ*jچ£–O&ΚΌκ»z{~Εΰ$ΨΩε¬`ΊeWRνYά[ +Β¨Ωo‰j»:ύKΙ`Ωά[)±kϊΒψ=Ώώ]¬γ½"BaΦ]cτ8{}mΨθ€ͺOΡσ@%Ίοr2Λ₯u”x§?Ζ―Aώ¨3V…dH\yΧ[аqτ©8νfƒί―7Vΰςp(€Ήd‘%‰ϋμ}³»P—L™[MšJw£bB(ι΄σ¨-ώ`ϋ.3‰)bχ~γZΜξη’  KΊͺη°ΪώW]WΎωΩ‰β΅6’ΈΔk°^΄:~βVήιΧ‡TIR…<—\·+βCQ"7B­—ˆ΄¨"Xθ mwξ~wΛΈςώ΄HzΧFOS‚ΦΣΑ`TˆT©FGΑ[¬Lq/ΣΓkX’ο½ Ό½˜9y™ΟΡpŽ ΜN*ΡΘ…¦»ŸiΞKR/ωπPlτ]c¬χςφ +’εήΞκaI)?hŸ#‹%~«x58ƒž°VG+μέ0PΆζŽE— Οκ·ώIΘ­Ν +‘{)†yVŸBŸι[Nί‘ έτ\ѝs‘5₯Ζ+θ'ak^ψšAv’Ί3po˜–zEOθCsŽ[]»=F„1―3^D½*i«t"­d₯v‰χj”‚NT8ς.[9“ΐ[pΛI|Α*[°ͺŽΠη54σ­φBhυν¦3θ†&œχ$Ϋ‘™ΑM°]ΝΑφk½RΔχcσ`Ϊh*6¦)ς­Ύ8²tm&ε5¬ψuE7JWέ_―FεbP vT Ω½φͺΌ-<(~±ϊω½ΰ\”’–Κ?‹PŸΠ—c΅{υΆ…άιʁGTτ’-22’Ρkit§ ΡN>qšZ.ιO¨šε5ϋ7ϊ ½)b^η”ε‘K½ώ‘β063£EΩ‘Omλ˜Ϋς%O½”4m\ί6p€Gδ·―ˆ«CAˆ %g«`ΰ,ζΜ@W–M–ΰ2-s.t2ς΅<όΛA'ξ"“Χ‡¦gz}ΤTX»_XMnπ₯]‹4Δ―[k‘Nξiό·§Γ*—σxœ›šfαΖυ‘·όmΈλς"'ΛzMmP’Ζ48ˆ©] L©Δ,¨έ”θΟb2Ωn`šθφ‘ΏG4žR<ƒΗ,E¨ŽVD–Νςc/²ΈμğK˜Y(ΧAήχ >Η κ·ž ―UψV#VΧΰCΌ(’Π―ZYgΰα,λΖp)Λ€}ύbΟν?ΔM\ γ~qκ*Hl,ƒs΄-GΫ‹΅r¬΅<ΆΞ&‚Ž4½μ¬‡`m₯Α\ͺ v§Έ “§xΜH&ΤώνG  §ο•QέHΠν*Έ„ΏΆLœΝ’^ς.Xωΰ€=mKIaͺq)yy,9ΗΩΖ=&ΣνNJΗΡ‡ΰ<₯>—&ζ:ϋEˆΖžtyΙk£nhIΠQ’„0Χf‡ W±VRFŒŒ$₯™‘•ηtζW‘T'Z“μΧσ²ͺ˜6ΐ₯0LΌ’'ήQœŸ:©΅ζδ†Ξ ϋVαHNΝ{/ο)Σ,Φή[ιD/7ΧߚLdάύfžΘCΝ―ΰο_Ηεe˜σ8όz¦8zVUψ2@@πΞ1ΏSΨhφi–ι>%[ζΫ€ηjψυp²fFοj fγZέp vlC;hs¨lέ3§•ϊΙ„Ί.Gϋ<εV Ύ«νS¦7nφ`‘Oχgα4.1f,R.˜ώ¬ΰνπ€Re†U5Ρ΅’΅χΓ~₯Λ3‘.jE1™0Ί§³σ Δ·@‘₯ĝZ5wg‘47&όϋ„ΣΒΨTΒ4Ω³ν6ωίXδH©NšΠΛ|w<―–s …ΝυžδΈ§( +ν„'Q›Ω/ΘΙΔι–—9J +IΨ …yΕY OΪ#TKs*ν₯3Ύu„δΔjGWKΎ>_~HxΤυ±/)%ύ n~ΟRZ}<ˆ ½(€~ŒΟ^6λ’±%ζό²κBtθΞΜ―Š€xu·&w<ςNi#’OR;΅ˆ³TJg'ΎόςΝΒΣlk6?fE% γV qΑt ΝΗ*l€§M„Žφ„p+):k;A_]Gi‹7?\Yς—hgTkw@v!¨}€“σm%/VSYDηŽΣbΜƒ%αΩΥuxa55'/­‚ΨTΐ½‰;΄K‘q–πG“ν”E-Ά—V’aΫ¨ζ‘‹ΤYΎΥr§er>βξlB&ƒb‹&e%ΎΆœΑ*0Σδ³`Τκk»ΏΣΪ(j3ΨZgeύε 62€7·f\†Ρι&Ž±Ξ€?„Υυύη1H¬nŸωΞ@ίή²ύMKΧ<<Yi΄dhΰ€n½½όˆQiκͺ£Dnέ2V«Υ&ΥWΜ›ώBί–Dx€ηDΧΕ„Μ¦V–ώœίκ·Rπ©ΡΚAdα`σγVƒΦo]Β|ΝόT1UN8=:: §\Z―ψΟΨj’Ρ™«m‡˜|~Μΰμ‘ƒzfΙY5£­θ€₯QB)Aτέ΄:n/ζε_ V8_b2;.‘ζάf„ςΌ©ϊ>u‡uUθ)³£IH€m[¦‘S ­IY"^gz΄Hϊfh±=–Žh-ςΝcRκw#+΅‘™NЧXB¨,nΫgQnΛ»φ9?ι>}!·βaBΘu?Z}Β`°Εz8PιξAΛγ0§[£ι)OLγC’4(6σάa‡‹B›AοσΎγΚ|„o=&{Ÿ‡šχΑŸHŚχuο Ω‹κ3šςΝH‰hA\7Έtα$ηO.Δψͺp†P ?Mμs«ι }˜Σ 696ξ0―ω\¨«€ηρ­bόνRΞ§δΫ ε‘vΠ·ΔϋfVσD'― χaή΅l>¨ΎJ额υwŠΥΙΧbOν;/ Φ%Ϋz N`Η Φ½CœΗ/hP˜LώYTΧ$ ‹Χml Ι*22}O5γa*p2ωοTOΞΥα³oΦΖ•Eΐκ\Τo)ψ|8ƒ\ouΝ%ZUύΔλœ‰Ξ+αΧ꟫I„ΨΥνCiΘ[MΓ5•0F‹(~€k‰ο{›9’ί‘Π#Šwxk<¨] γh1γ»WNΔ@ώM&ΰk2IΙD|ιŽ‰JdΌ"ΌpΗΈ 59AφΈgή³4―‘°ͺ<|pPΫi~[€mŸΊnŽ:‘­2‘8§.ΊQ2) 2_ΊΛχR©JΉ(Πτ\1΅«>ΐό,WΚόBŸK¬š/°*S"9£ΠŠΒRΐLυŸ€ΥаTΙΛΗϋ›­Ψw²€ +7ήTΰ͝…£HD˜{1.MγB:€0άΐ‚uM7€ΚΟ€Μ^]XW›ύ=ΘM΅A₯»¦ΆΛΡρ‰¦y‘…Oμ#A™ŒSBΘ™Ά)l!ω#k`"\]ώ4J'Έθ叇Γwo^:6όξ_dŒ(>3Ν΅$‹·Qτ€Š»SΪ€ΝΪSΥ xχη"_ΐ>sI$oMςSά%Θ8³¨©Α¨J΄ΫΧΠωTΣf™ϊau€ͺΣ†β8ΨηΛ†ΞΧο΄AΝ°<άήΧ#Q3?λ-!η KF>vδθa9™lTιQ±(Α”cΜG΅$γ`][ωˆcrγ†vΎΧx€MϋΠSCμ€Εs4g,`έα z(ΥGjm‚LlSsΦψto°Αϋ)·α.”°BgŠΥ",ΠτOgf«}v ˜·Χœ‘ΟϊWL +κ~ΌJ’°ϊρr΅πσΐ>@σ'¬fςΰΕa€:^4aq>ƒϋΞΊ6.KTͺIτcθέSςΊ4‡ξ˟Ĵe˜φ†%bήοi·ω¬/eΒ[8€ΑRϊH½χϊN 1’νΈλr―QdΩΫΖ΄«'¬ θA¨‘”Φb†eajΌυ†ύ'œ,$d|2Έ )_χoκcp₯md―ΰ_ώmUσ]ξ/ϊΈ,w§ ώβ‚'ζΛwώSŠP<ΚψC»‹₯τκεU΄£LλU°cΖ c²ά}VŒ‹:Œ΅ αœφγOƒ«.ωήΩ[±N,;Χ9b!Ÿ6Ί•Φr5ύΆΝΌπ±*΅ΖC°w…΄³ο‚L^—:$})ϋ’—˜°Μσιε-Α°TΫΨbυσ^'gΐYOΓ&€ v˜ι½―(5Ό>—ƒ«Ο9x§Œ„AΠ­ͺŒY‘ΦTMpdTUΞ™*œΆLJπ‘)‹8mΊUyψFgdπΞR?:q?Ζ§"έΛζŠUωp*ΝησˆΞvv­—‚7ΡΗςδDιίlΚπ«ΫΰΝ§)rr ‹ΈqWAΑφ‹Ÿ§Α^§ρύΔ2 +W|"ˆ˜6>j<ή§ΩGJΗ2Ο•Ω—ωΎz£"{1$³gΘ&Og­f&MΩσxΈεα +oGxε6-Ύ=4‰¨²yΒ±2Ga"™@•RR¦ͺ‘8¨Š:Ν±ρΒ/Υφ™kΣΧα +_Hν½vwtVο9τƒZe>n.#v’ΣrސJ» ' έ ­œ§œ –OΤ1U#‘Px>υΖoϋιΩDίɊI€ΓԞ Δ¬/οώutΰ#δ9œIεΨεΈ7Ό“½y‡„œ3₯Η3 σ#Ι/²tɜ$uΩƒ‚u9VφPAX₯ χ޲Gλ`>¦Ρ>qž6΄m9Οα"S–ψ8W ΠντΑ#eΠ%R§»₯«ύ#o™ϋπ<β‹Fύ7˜s¬ψή +{œcYΓ₯ΒγŽίxœmς6? S +ήeΦ +ω¦ΊΜΘΦ!S‡―a(™θΛbœΞ|˜3B”—FƒπΑ’”‘Ω φ•όPˆ]&eΰ|K’χaKƒŒPΩ΅τΖ$η@ϋU6Ρjϋƒ₯X‘Μ{WθΫpΰ3l‰‰|ΡΫ@ž’Ψk‚τη[^Φqr¬ς4 DΩވ2ωυœΦkμ% h›)Y_ΐ*C_γ\¬Yψ! *|pγuς&Λ,έγΞ#κuF‚%οε6~o@ϊ/’R€zμt¦\ύk‹¨"{œcvχϊ&ŽΰHš‘κP•ρ –Τ]ŒΪU{‡αeΣ.D)Ww2]…΄T +@ΉΛeδ‹\]ΩΡψ‡²ΟY^­;΅ρz2iφΥ‘ΰΩΨ&ž ?ŠIέ†Hδ< 8vδ«Ν +£“©LL£l ~"λU1NΠΐΚfT.Ό|‘ΰέη0Ž΅¨™j*ΦŸνΰ Ή,ΗΣF©0ΙA^ΥL5U*7`3δ?Fφ;2ύ5,Υο2uoη’hw]n%ΐœς‰² ΈΨdΚyc Ÿ©Ηtoζ`¦³ΘσвΛBŒί7kdYή +―Ύκ’9š^” +—„a2ˆœΉΞΚ‘yψ†”Y θ=Ι1ά#ύu ύͺ­Oα0Μ8NZ—|’φφ>–ΩΆUHΈiiΊ4Αop%,“0,A+mUα«έS_y(τh•V³.qcœ=$aŠ•Œt4έΎIιU¨½=KΆŒΏΊΟΧΕ6OeSγMΞ rσ·nΊΟΗ`k‡ƒ^“ Cδˆ7S=z‚ψΔmϊξc^±ύηπeν1ς™œˆB<¬'ΕώρU`\†ά‹ηSˆo€:lΎ ύΐσBρ6Πd?Ε;μ%Μ»Ε zΈ Υ›ΞχιαW›;Σ~K„`΄η­σ¦`U΄«U—:62Ολ‹Ύα¬ 9SΏΝh‡Ύ'L‰DK [X‚Ζ£yHV΄.[%Ά”oΉH;b鉳€ηcK9Ϊ/πƒ€<&N'8ΰbξίΟ[Ÿ„ν½³gλ9Πε?Δ³Ag¬ >;+ήΈ"3 ?•+₯R`ΏsdΧQwQΫρ:HG&™tšˆ­tq8ατ²8\ύρƒ±ζ5ξAmH,³™²v›ηvd\’b ξηήA2ΡΎ Χ μ–£‰Εbμ ©‰˜λψ)Pδ}I$–ƒοψU™σ1qίFΙ‰hΆ&τΩVͺ½αdφ· ½α^Ή©¦ Π_Ξϊ­ώ±d|n0:dkeΎ=Γx8€ήΙ ›K +b―ž‡ώknλbzˆπ’Oƒ'‚ι^ΗQΪq’Έ“7ƒ½ι±θ󬏨Yί°¦p8of‰UqmθaQΝwΉDΣ”x7Ή?ras#Β§m,xmN‚˜ΥcŽ…ΒzŸΡ)Εζ’uβ’Μ£Ό tΎIf£> ŒiΏŠK3iΨ²ΊΓS{+žfv|±Œphιbt ±§K&lo”t|ΦέζΡI»*η4[γΈΖ²iΕ"ψβ]©ΔpF-]α `AIRv·υ™N7,ϊ-Ϊά΅P΅3€§Tς[γ5TšλB`˜ zΘα!ύΪEϋΑvΔΞ·J}•Yiβ•ΕΩ=τ₯xαC¨N›Œ•?8›έ±«ξυωŠΥώGσΤ Λœ΄»ͺΞʌ΅BΫ εώN>Εͺ™Έ§ψΥUpn6"#34Ÿ"G·Β’ΣΏ›GΘŠΫλπΫδ±>eZ{0KΏQΕDBδxVˆx»} ˆ$₯©j#^J|#€Α™Νϋl­@‘Vv澜H‰ ‘ "ZfA'˜ΊtE—§ίΐWfέΦL|4ωaNΌυΦ=bƒ4gώή_€eΜ¨Γ©XRζ“ V +ŒΩHL5ϋβκG’–ΎΥ,ŠT₯*G“³φ―Uτ‡ΌM©ω>2TˆpψΞU!ΨΧxw"ή£ΘφD\Πr Β^Nd\¬φMkQ΅β$wT§ +ΊΝπ%6;‡αlύ[‡|}»V¬ι«m₯Wτ<Χ‰¬£`H*ό«w]‰ηΰu‡Ρ ™;Η‘~ ΗΰTυ”ϋ:/GP''ώ©t@iρQFη2(χLζΝ"R`α΅ρΔFhολ”9±»Wp"|”«€Ύ~+FDQI΄ΩΡε‚₯_Υ ΧDν0υΛΥΈžΌ ˊ†ε ςρΣفrΛΌ…’oΛψŸLη}U―ά]F£ΫΠό½©|q΄Ϊ’w" ςe…1 o bwΰγi»ΤV¬Κ¨Η\Y«ν—K#JcΤKK²C ψ™uJΗ[r―>W°*z‘χ:ώΏΩ‰ξΧΈΌVΆ’ΚΐP/‡Β?g›q .αςκ―XΦퟎ΅δ™;¬„A΄hYI‰ρi—mφ±›ΐθpˁ±7δSΡ)ζΒ‹%YΪ‘ιάΏηyρ^ilζΘ‰”k‘0θ½#_4 MπŠX©'ƒp>lCR2B=5z·TV‚ +ΓnςΪ—τ¬R9Tjπσ›eιξπ°φΚ3ΨiζBΙ―˜Œx;ίΒ ΐ Kμ₯—Ξί7^giΟcRδgŽο`}}2-e”Όbπομχˆ]ΰύίJRΔ&†Λ§mΗ·ΓV0ڝ„όk₯ι4 οj ‰»˜ΚτfΆΒη²έγΦ\Ο“Ndΰ‰π₯’.[„Ιn¨ώƒΤρΤO9Μ‚μuδcώ+Χ£Iͺ‚kEΧ“Z“!Twκί°ΎΠ=m‘ΩŠš1ίφ‚Пt»nB§WαtœψΈχςΣL‹“ψYάύqΉΦ/ΐΓ„ 7yI?μfKPpžS[bζΥ'KΕ +UzσŒ\ρ΄dσ' 1κ\—‚ƒΫΣΜΌ1$KφnmD-γ=ρ DΪm©Υ=©γ|δΧEG&\Έ“ΐν}'iRm|ΛN*$Ήΐ-Ζ8s,>Έb o*Ψba–dΞ»λ«Lς}¦'ͺΎ“Χ΅M&— [ΰ*₯”3ΎΝ7z­’l£5Ά–·CνούEΗδͺπΰΛsύ“d&²hό/°mΔι1d4ckk8(#Ή~V3‡N~vΩνy‡Dΰ›+.Ν›oΡκvSxŸ #+[œW{} +/B‘vΥ&—"ν#‰₯];7,"pg΅ΔlD’5[ׁ—ΪšγΑέLόLMŽQΟί½]…pΞ8hτͺξBΓQ%τ;"`iLΤ+ΙΆζx,ž$bωyDKθ’΄αϋ"|”žšR­‡,βΗ)ϊ€ΐ¬—§v²Ϋ™β›' ‹ϊ=¨ „νBΪNΙ°•+4½ž$υˆΏiά1xKΕΪ—ΦqΘ O΅yŠGp«"`έ&q>Εκ‰°&T +gο’‘/Χ““ΊιDY8ϊ8YGYPρξσl/ωY/(Gυν¦ŒοΣ£[œΥbyΖoΞl:Γ₯gvlΪιI‹£o­ž]π$ƒφqθεO cτΠ,ό¦BΝ}Nά4χςŠB€π—=— •S₯)΅;Μj‚HφηruΊiς'!(=Ε, S]ΑžK ?cχ*νΙUϝEσuƒ4Έ5Λu@ΉέAB*ΨϊΦŒκͺϋ|-2jI¬ΰi³~ŽΏH…D«}σ +³HHͺ“Θ;ˆ•”ταX‘ OSͺηΖA:ΐFm;·γT<5π_JxyΈ…Ϋ0,oﻟ¨£*Θ[!j]-?{ηΥzt¦m!ί„½Œ°-Ϋ’_1«ΰΪ|ξ„Šα?γbΑ8¨>³_@ΚΓJj”=Έ*xν φ› ŽTE‡?’'τΛ-Rlr‹#Γeˆ½”μr·ςΉΰP’`sqΏε -€"ά?Rσ—Ζζ%,γšρΩAΦK£ΈLŽυάFιΖ'VˆΚύ@`˜eΉχήΤυπ¬ΐ“’FS”š9aξΝϋ~δNWŽ–tMε%W1RρΏuϋum“ OτO¨fΥ₯r™Π‹Έέψ¨Κεy@ˆz‹­”Ÿfψ?-L˜ψE:”Α5B„~³Θ( +<Ο’Χ,ΫufH„ΎΛF`‡Ζ…³ΐƒ°D’£CΌ a.αsU‘±­ξSΆc^_βΒsοLœ ;H°φ`αΒ”­z(OΆ¦g9½[θ.w/bmοΰβr°²υάΝ°Χvz΅Mm"aӊ€#.εƒ\{9‚T:ύϊešφ8Ζε—m&ό₯—ψΊK4§\ςH!FQt”PΛT—ƒIΩΐ‘ a₯π.σŒΤ΅­L•auλ€± `7―νUΠ’κLMw«?Ÿ%·Οσ 3LQΜ$Ÿj/ΨΏ˜–§\Συρ™E°ya—RλCšhο© 0•.Δ–/x3°:$Ω8ގξ°4QŒ±1œ·Fτ²ΠRΛΥΦ]ˆ9ΡyςJ8Τΰόp=CBΏθY|^‰daL76η*3Ί.'@˜νά¨9ŽΪo0Π\ώΩ8£Λ/Ϋ”‡4%ΖNŒYΉD7a£!Ίά8§}ʎ9Β™λιΠ+aΎbh…μ!mΧlD†ή[TΖί5ˆJδ-{DξΨͺ£‰QΔtŒΝκW,ž}T™Ρλh2―{&ϊ;£Žά[4ώΔ³€Νύ‡ν§‚ε―BPryλD„ͺιΏYf.ό±³W7έD]y©ΖυaZ †7Ρv^QšΈΩ&x¬ˆPcH'RŒh·w*’Ξ₯ •ϋ&‹³pίX‘/ζmŸX… |§Ξζ‹ …›ξSŽΡ;κΝ;ΑbΓ $I8ΰ§Υ_ϊ£¦¦ΈxΕ ŽΘέΗ*Ω’Œόtϊ +ΖΌ)§φεη_o;Kx'ά4‘εϋ/RI°zΆŽuoOZfIΰ!뜢DQ–m[ΆmΫΆmΫΆνͺYΆmΫΆmΫΆΝ»O[χ ς-#[Ζ½“Ÿ Δ/zl'ΆΰΝ + κϊρΞ~ώ@9ϋŠkήΖ”Nm&cΪϊ’ZždtΐpM”RzŒ—―ƊΧΓDμ–%{8<ΖβοYœΪ]?ρ‡|œΞο•™PτJ²bKιί„ͺ[χNΦΐk?“½5ΰ―Απ¦φΒQΙοo¨6@ΎβϊΘ³ΗKΫλ~;xΤm'Ϊ‘ΧΆ·’31)9‘‡©μΝ䀩‰ŽΩθTΏ@&zhΗΈΏυ™SŸ―d WF‡i(L²ΞŒΒ’‡Έ(Δ8&¬1[ͺ•-¦ΠΑβuλx7ΰό–t?!]ι{=L˜ˆ Φ=ςΉ£ŠΐΪ- Ο€ +£ΏQ}βUΠ?χβSy ϊε“f‘LήχX–ή«“²ΊdA,K³:nσ&™Οκ-GψηŽΚ ιXNNΔ‘͘0|F›T“{r†Χ΅H!c,sΤA/IUw™ŽΧΊ¬‘βšnd[aΖΤjŒ΅f εwER$3p;JK~¬ΏV‰νΏ|Χτώ LθΨΫΐ +<ΜΠ NefΪ’+Ύ/sΕ$mΙκIέ―dΆ|5nmσΜ‡ž‹Δ3œB†ͺΥ7ΏˆIn’Ψ€+9“³Žλj­γΓwχεΜ|.N,iάΫ Dλ`?–Γ΅ΒD<Ή|@qΜ”š―½ŠΣ—ΡkΞ‡λΎŸ’ >Ήι±’Sλ$ύΒ +Ά![‡¦Μ΄νHff­ϊ7ΡlΈEjdέ8e¬BωηΉγ?F₯ΪΤο#™,ΦκΜ,5“0p€†½χ–ε?1™'απ-Ί;IΞoψž†Έ“'YΝΪ\0χMϊ +ͺΏή9ϊ¬ΙθΙNuN”ψQψaθrΏ TfΕUψ)˜όήŠ"¬οJΚaθОoΔΙΉkœ_<ΑCρδ―Πλ•²|QHΧ2- ]٘Gπ\} έυ"‘γR ΊUR5³A…Σšλ0—Τhu’ύυι―,]Ϊ/)ΒG„ŸΖ[εΑ€wƒ’‰Ξ]˜ΨIžm<=QZΈŒ#ΈšW5λCΗ/ΜΙΝŏ0ΞQΥ/j«Ύ΅Ύw©Έ³Ε€!ΰ‘ΎλΑ:AYsͺs_£‚₯[€c½?¨(ΙΊ=h‚ΫlΥ›«ΖΕΠ ‘λE΄jτΘα–8'Ywl†ωKΗu%^¨VΈ²^°©…WΎω%hAΘωR½|&€ΔΠ™ΥιlgZεsΘΖ"εE`ͺπΉ3ζ‘ΧΞέEιΚ&™‘ΐ‘§³9vUΟ_q',O~œ`ΐWA珈ΡœUΦˈ“Θ¦­ε€εd―*šsΈ3ϊZxΞ±’?±S;ŸšPϊ½τiW‹Œ7uΧ\λαBAΜβ·o€šΧχlΝΪ*έ“—t΄θ1ξƒΕBόAƒ­£€ΡεΪΖ‚ς)rΗ$ΌΚξ!“¦Τ1΄pΨ=Z4,ΝGD'Q§@&₯±Ej`Wΰέh άvp―°!iox‡a"ΊCeΉ0tΏΚχ>φQ\‹ +Qe$tdSŽφJNΣ:0θόCΕ Ϊθ Αγ ΌlΚ/ψ=ΐα";JΏEψf˜Y°3Φ£ž„γ€Vύ¨Δb;Mϋ·Έ»&yξΘ‰ƒ&±Χƒv]p&'%£ώ όpώ€Ό ΞΞMš­8Sp½ά2 1j­ΒαVdΛh³ρ{dŠ.φ’]kKπ’a¬oίξMΪ“q‡žˆAhŽ!ζ€χΙζΘυ·Π[3Τf,³ΖΞ‰=O’ΜΦ΄PdΉni@Hr½$|υκΕLhΰƒLζ’h΅›½§±ƒm{U‹–ί3Ω^SΥφo|Δ! ;¨Ο +jˆ9ΐύƒ0”ΐ“:³j–ΪWνΠKΖΎο,Hdρ`Ίw,p+TL;«oΡ‹u»Ρ2ž„3φͺ’ξi] D›ψwK5 Άχ–ώ±wΠάp«R,Οηbλωγmύ‘˜ =¬Ή“ΒDμE»8“dR OΫf›έ@Y*œσ F)εΖι ώ§§sο+β¬{(V9²’»xαΥj½|Aƒ―κU£Ν`dκ£»ΛΜΘΔ« J–•ΥΖ’iqΫ ‘ )|+k, ΅Γ¨± έV‘jγΒϋd[±΅‘²τχ‰Išρ2iΜW΅ΑG»ŸF¨D{Š;ΏHό— -ͺ^Έ8!|λβΑ¬ ¨SΊn§Ω‚žt·_wmUσŸ‡EΣwAώo1φ}Ω‡xw6-•‹οƒEsFU%λ©©œΑyTΔίͺ +[Ϋ€hVΗ†N«Η&£WE{ .}Zφ:;ž&JŒ ίβRΘm’ρ4vΏΕτjkMΑ ˆ99E=ψ‡όœZƒ·ΔfF~ΝΕγdŽΔYΨΗqoH«έ-σ₯Τγ&JΩ?|Ζ}Γχ6•1 ͺhH½xφŠΥdΗYƒ7–nΪq«°u‰Τj: W_Σ–Αψ©¬ΔΕFσφρθYqq“Ωep²‘-Eš:Žh·…ΒƈU©{¬¨‹‰›καEjfIΜι…e°fεMηε€δώ)ω[h9 =δˆΦΥΙΠo…Uψ‹ iη•w#eS φμ艀F…«ύ€Œ―ˆ£š…ψžt$ώΥ”o»ϋ‰S—ή½ύRL•­8oX\_ΘΡ@³\υ’‹ 1’σ¬Ϋ‰TbτrΊ&pΧξšΡc<¨ρ|NPn0ΒΉοšΛΟ3fαAƒΘGQ*μ'³cΊ<π€!ERΎ†b^±SΆή‘~0θεΈ6ηψ˜ΛΎŸ•–Z”thη‘qL9»₯ωaƁͺΛΉ„L)BΟ«Δ­΄žΎΛ΅μ/sŞKμIGsΨJJαΩ +€cJ'pœD8ιTJڎζQ‘οβϋ =v.&‡ΪΎχ3Χ3nΥθY[ tΙβεTΥ9ι2D{ +κ^+Rβ4€†πΙΪ€Ωn=eΩhΔ­ M¬‘FΟh’'‚ζH΄τ9ofrCψF$.vΙVjš²\RδΏπgYΫεΝΈ#ίuώ&πΧζ~cv•ΓŠ§Ϊ χ0Ωήy6 [bZ ¦ΨCœ=e9[†ή’“™MgΟG§ S˜΄,΄|¦εψp·Χ₯.‘œ«'cψρε‡οcP.-° @‚Έμ­:#(’€!…@/S‰oϊ4η—νa(ΛΤi•βξΔ1ΐρPπ=ϊjNΫ}Β«ηEX.bWτ·~²“Ε]¬iωdΝQ?9 +,Y7N7@—ΈΣCΥ5$‡ε +aE^δ-—ηΰ~“D(GQ|?χP7Σ˜zC>€+š₯έ?ξ ΤθS†SUJρξξθ­32©u8ρQ¬NγΥζΉμ€μκΧ q:_0±¨J B‰δ[ͺ‘¬2―Γƒn‡PΛ©`aJ8γ{NΦΘB˜’Fƒϊ ΐ΅Μj6|›/§;g–ΠΗ9Μe9$—± +) kލ1ΈLazˆηΈ½WƒTzτ?ΖJFπa"n—ψ€f꜑x>ddHšQCί‚ήͺTCγ}Œh]…Iύ”μMΞ”/n)ηΒˁjyŒNΦE₯=UΜLτ\,nhi_| pΊiMΗVkv~2EŽΝΪog¬ΊΠ-Β©ͺ{¦΄Κςc‘!W΅S8`œT‰‘˜ ‰&Κ’ƒtC¬…ι«ψY•^ή­-yΓ[φβ7ϊƒ¦ ”Γš•t€S8’²lν “ςιaΌρ\k;ο'‰³|Ή]&[rρ/* 9ξ’Aiθͺlηb9΅g>0‰Σ[ωΣ–=Ξ^Ϊ΅»!²_pEAό—4r˜p½|IB^ξΔΜ_΅w°κβڍ"[Vwh\Πo’ψύ36M. Υ`‡_eϋC똭M>gL`πϋΜ6δo}OM™ςξσdν0œϋ›ŽΨτ°Bz«3„TOŸΫύ‰¦)oΧ8λέ¬\…PJruZ6ζΗHρπi+uγφύcγψyύf;‰ϋΑƒΙaιΈ ξώ―βŒ‹Πυ¬#/.“~ΨŠ:–+o€Α•J(ΚΚ ¬έŽ“'P`[ΔJι?νΝf*Ϊχ/5dΆC‰yœ–Ξš0θ‹½ž2γ‡NC9Τ8±€―!ΤΧ΄t0ΓΜ±-ΙπXέΪΏΛΜ{x ΰΐΆ€Nj`W<[λ(Δ·GΔ¦‹€δtΧ‚rcZκ>Η5EςM²α‹¨ΥΫYω# +ΝΛk|^Γ2>Ο,B +{Ή³γŒ[fκ>ΙS•ϊmDD†:±εŒαο_Ξ¦¦KdΖΑ[oΣv"ί!œΟzW*,g;PuRΚ‚,Θ“ξ!ΈΗ5²)r&Uϊλ°Lc`l|Ρ+'ƒΧ~š‘ί³‡H8ήΖΠδΣΤzΞz­]-dˆpζξ€ >ΉN.pΕ‹`PΠie"ΓINͺ¦•l`φ¨ ͺ7ΑbšΝVYlίΔΠcΨl©%½gnD­α“tήΜRΞW<ί‹>σΈGbE.ΧυH'§…Κ¦*[9C^$΅Λ₯K?‹TλU}ΆGG2$ΤZ€ρΒΈΦ>§ŠΩ»9›5₯ o[=yΊ$F +‰σδ!Ρ±ιT;±aN ^S"vXA ΤVΧo>+˜zWΔ$φφίw|LΥΤΟ;™&y‘Κoίʎ³=αν^8ΐoŒŒŽd†‚ΐΓoΏ³ju’Ν}pόέ²k• +±/Δ+3~+†Š§˜™ ŒF—½’ςΝ9H’ΞŽ8†œόͺ;ԜaŽͺROΔ!ͺZ˜Τ- `h > ϊ«lΜCέ΄,ή«DŒ<π΅χ_|’?ζτΓ%ƒη€}Ύ6"φH,dΣυpB=κΗd.>;ŸšΓq.Σ L-œφ„ §γ3Ι…$ͺ}·±φj¨?nΪ)ψ8ή8;y2do¦jEΈ4Λ€q{ +œz—L‹ό»Λυ1υ*yΨ›†£<Μ~dρ*=G^Ν“’nKVeύ˜R­#ͺγ±4ΰ[QΛΦQsnƒQŸP¦€Ό Eρ†n ™‚ί”(ΉvD§­βν­*πkc8€Ι•“f#ώ]H +»(“Ρ’νΚ ϊ¦ψd±ώwφΜI‚c‘›bςMP²Ο`#?¨"εΚxΟG‡ιΰπλ­@9Θ{Ω4ρŒ›P\6,ιI^ka”‹,SΈΐm<£΄©Β1ν7Ω₯rVŒOξl^­]Ψ5ύΥΌ/-n’wE±R=οJŒΦ£Δ9kx;ρΉ*ΩSͺ Έ^Y΅jδS‚΅K••―{&MȐ|ΥΎ Ϊ Ζγ8π“yf“/‘|Pm, VΟE³; ­Ϋφ! +N.ϊλΎuNμ=#dφɜπΌ‡ξ°³z"ό„Ί½!QΤλ +³θkŒƒ² Γ?ήHκ1U°Ω )ΜΤ1ZΉ ₯P€'J‹pPχώοE °#α΅αόΨϊΒ"+0Ύ +RŽe A„}EKΊψύβΓδ±―Ξβ{Š6€ΓsfZZfνcCB–· γ 2aΑΓΪhΔFvu­ωgσ.UΞέzχ€_ΔHά Λ >ΪΎͺΌL➰φp‡ιόiέZi#‰ξ°¬LΫά"Μ]Ή†ΫQE¦ΌΏ„(t6ϋݐV³)•]Γ2²±½Μ"¨9+΅γ—ΜZ²ίΑΥ½‚i™V~Ž‘Σ.;9ՊF΅•ΓμDι+εSΖ=ε<5)JyιΤž6λKGŒ7˜ZO>+SPΏ]!0νΕ€ύΘEͺς(Χ„hdΥοŒ_;‚υΚ΄ΈOGκιǜC»φ½v©]␂+Wˆ±‰«ΒφkΤΩ@–°ͺ[Ηξ“f˜€CΜΗΧρfΚDΧυκ”4"ό„Ž ξžX|©’F]’½‰ŒVŸꏇσ΄^IcσΆMB‹{p†±šή*IˆΈΨ1]ΎRб©±p~"37WΚ=©ΟnΎΚφίbΫ=F+CήD]gIθ偾Ε₯'ZΩIέΊ„2ψ?­α=γδΛ>jDwMΚz€ω1‘8"LK:ΩΏσMσΎgoαφzBL‚=€νR²χBŸ=τ7₯Dμ‡`.²Μ[HRbIpΗj˜#Ή;rας£±j·‡gΐVSRy`tχσΥ .ΠCΎ°¨Ίν“n Ο½δοDΑΖψI˜ο#xLWΞ€o}9_Q—ϋ{EuτۏCΧΑάΠΛΔ’n΄ψxos³λ— 1¬α xrλΎV]э~-α t°RM§OB^”fΊ…ς%QΪέa¨΄υ’ΰ΄“;ΛDπΎ_8›+lOtΫ‹£‹_W΄›Ζ@Ÿ.ΈuλZη-‰κ& )€d…›,QŸ(θ{8γ΄‘εόCΞV2ό9š2Ω{Δg‘B‘hŒx R[*`οφ,IΥΠ#z—toΊΣσ±ˆ,Άρ†swœ˜ +ξ`LSΤκƒœvϊ‡yXέa&K‚pKΙε3 +hHHΑ3LΎΤ’yνr£8ϊ0ΏSH$0ι‹'¬™ΆΙ•uuPkZPQDγ2ύϊύZžψ\‘ 'Nτm3O]οRz‘Ηv>@„.ύΧ±EwΔ9GPD鏸μn7†* +λχζΝpσΏ aV…c#Ώ§WV3½β…|(<ΌχmW7@νdSN7$ηͺε!Re*L"ψίι‘ ΕΦ +άUICα‘VŠ’ύώΐ&’_φ*%ΧTΕνΩ+>SΘθ<Γδ ηEH…#l¬:Ί΅OU?aΕφD6Z€aT†>—»& 1ardΑΛ +ε§φιXδ1Ά-a˜*Φ θ!β™AΡΌι›"zΝ@B@…Θήύ©΄\%sχzvcb φlά“›%/_αœ_½ρ~L@θ„w£ΉώR½3‘ΊΎΩo³ †•ΆΏ>y.Oω€ˆ4u†!hΗƒψΫ CΗά©¬©έΛ¦\‘€BU;nξ¦Ή[ƒ]‘?E(ΜώJf„Υƒeˆ‹’_fbύTŽ_­}»¬IΩν'€©LT[Ί˜Τrψί/#uΉ—]ω:;ΰ:εu©Ός&?pΪa΄ΪΠ¦@Ο’΅E +”£…˜ά½€ " k|VI†=>άaΜΌ±ψοάyξΆΛήxv“Ϊϋ ψ‘Άcξγ²Δr¬ΔΠ™ΐ9–xΡsΡzβ;‘Ιπu 1Ÿ_›(TE8ΜΈ.`ξR”mN)‘yΝ!•1„`6N'Ηn~ψοτK+H¬ μˆL˜±·ƒˆ–ήΪχ™¦τ0I.KΐΒΞ‚+V₯©ŒŽΧ„;μ€<δ%N ϊσ#£ˆ°U8ύjΉ¨O1ΤVb.8QΏ€n4˜η„Tξ§dώΌFeΠ΅/ύκp4»t!Xbˆ¨vŠκ!ƒΥ•_PΟPmκ PΩΧ{†“xmΩ‰8$б¦]#pφ@Ε +ΙΫΨ°tώaψΪΥτKβ‹ψπ†υ΄ΟμWxδ”Oΰ§o.5Ή}ΖkΦ1ςK·BsκS)c4ύβ͈{T™°“y7₯o}ϋ ΞΫ]Α¦Œ+IΊ§ξ)‰£―3π–ŒρΌi)ΗŒΗή+>ΝΘΪiG-~Έzρ5’«πRHΩ"c#mjΤ0poζ8₯ωη‚€(ΆL͞‘Ϊς.j…NΰΝΪ]K½Iα(K؞μΤΝLΡTW‡fχJΑ²™ϊ ΣΟ– +§OŽJ?Ml8ʌŸf*,ΒNͺΦΡΆT§Hε²μδ$ ŸPt²s‰«μT&ςA`;NŒΕkE}pχϋu Ξε7RοΓ“"ΉTΦύ……¬MΨ‘άkδ/ί…ο„Μ]’AΊ,€)]( $'DώSbύ­u2^‚‘O t%nƒ0zΫ1‹ΪσΝ3Ϋ₯>Φ‘΅Ηϊ5ύκ‡ΪmzΑS„κFt‰GKΪu“Ρl‡$=β¨w•ΞΜ?¦Ε4.gί ϋχtΰFY+#›²•œ y@©~›Γθ#δ£6Δ»±‚!ξΠ"`VΧB&ΠE?œ›ήšhSεmΉσ?ΚdΤqψωτ΄­Ωθ]²ςˆ#ΏBΒg9ς ‘N*bΤ χswρmRγ“ƒ^{KsA"L]¦ΧixΖΰρςμ›ωoΪ†:‡‰ŒqΡ֚4cΩύmpΣ +rRn?Φ-ΏωŸί„ŸΟl6Šσ©ͺa Ήγ,oeM¨ύiAο@? |ω·χφQŠΝRdq`κHRύ‘σK܎cεM™\y΅ΌOœΩͺœϊλ¬ί%y/yw‚"ˆYΘκjηΛgΦά##IΚ8 Κƒ―u–a. zi*ΡH]2Πυ€<Ιml¨hjXƒZΕΜΙϊ¦l—σ1!‘ €Υ€rx(εΕϋ˜l’n[ΗLtΟSΚΆδ!z,€!&–°ΕA–Ή‹―$<3qk4@jϊ΄ψ?­ΧO‹Π»Ζςq+Fα–ϊ !tC\޲:›Η8Ν;E+w/ΗΔŘί™?d^αύΞΓ€Θ¦Ξf€0ρο&)]ς|Jkψg•9\©Η΄lρŸq Ϋ:κp&Έ„b°OθΘQ.mRμΌƒ&£˜CυφοΪ&ό»œΛ‡ύ°ΈΖžiΡΝ !γi@ §N +”‚Γ‡θ«™Σf…ιΟ-ζYM6,"j«G„Ψζ…³^/λτRΌdX¬Dπ`eΘ’ι%Λώ’}d³αΦy1ΫBηΔ6 ‹;ΚaR=>Ύ!6Β(ΤΤZwΫ-ξ¬cOδ#pM«@Χ>̍ΧΕc‹Φ»CH³e$@¦r]$ΐβε—›#(θγ-'πΎΘ*ΫΚ]?’²ŠΝώw)π0•l>©«χ·dŒςΣk 1dcm; sΑ~δ8&“Ό+‹(‹Qψ!ΤyΩά‰œ₯2²v­ΉΙ8…:›Κγ¬Άΰ+zO'ieΠΨΑTdΪyκ₯ΟWδ=§qbTy[ΦΩ Ÿ€ Φˆρρ\tΓΓ¦"†ΐz-  τ©Κ+†3ίo ¦~βή.#’2λ²~Σ…dΡη{Ψ…1/Α4O™Nζύ(‘ζ–σ7 +ΐͺ<ζγ$|<™θm$„³žQ‡W©έ '1ύΉ\š:€†έ4Oξ³ΖoŠΡE2ψEZά….¦υo(yhHΰψ£a=»fΠBS†$…ΥN&'ωΊC…β8€p~m΄ΗW€ο&:z5ΓH|±0θ_3Τ|ώ¦‡ΛWϊΦΏP"Ί5u~}}Œ.υΘΡς+ξάς½gΏ’f–Λ6Έ₯Ϊϊ€ΈF,#­` β \ΙΟ¦Ί{Ήΰpεή£Ε=b’―_Ιz=Ω§ԍɲBΞΣοΆ βΕRVkό|Λ Νπ7*Α1Ή; 5ϓā*˜Φ‘¦_M―9ΌΓΞ*΅•$ίΚ_Gρυλ.]`ϊ<Ί|€&†—Bϋ©^CWεέ¬$©¨xϋΎRdˆ»D5A(O± sΐ½άπ συ΄ΗΈƒ;ΗZ˜‚WhE—ΒθΨ΅†xK€1ΪoŒW%ž·VΦ:œYΓJ:h||=?£*-€ξ›ͺΟnη2ζœ“Φ9₯u}‘,΅Yώ?Αa4 ΰ0DΪ»R6­.ΤX, +#χ†Tε‘6ΩΖΤ±30MR 4 κύ”ε +_S Ά[(w»@Φ._W>ΉΘΪΓmsΦi™³0‚Q€δ‚Ό™π²“uqΫσC”Ζl1χ.ι‹zq5Kkœˆ±θ»ωΛ~~»D'GCίIKߎq†iσeWだ|Kl±Δ|S–―ό;px“λFtw:³ηUΈq ’MbKψšAΠͺ­ž0¬gPΆ,Φ›{–άͺ'*‘–†κšθ ½Ž›˜₯ΠήΟ˜LΛj₯πΥΎέ‘ƒο@ΰw-#;'iςΊ«Π8ͺ“ςS.Ρ­ύΒΐκ{ήj/Π]ργΞΜ·%Ψ +ڟϋIͺ LSΨ‘­„εP%Ρ¨†pΝ|wύ7ώψ²|Z|Ύ‘XfΫϋ©γΕΊΎaΆiͺΫ™ήέ5Ι-κCψ +š\Ο«θ{„!ρ¦D2‰zώšŒw"N”7{Kόα‡#αξΰ3ς¬Ωό(<θΏμή~ΙϊΩ:©–‹ΐDΚ‘η:7±Ϊ"A<α=eΗλO1ασΝpCʞ‘€+)ΆΔf[° d•N&«Υxa7΄Šρλ[#΅ά+δ66Ϋ.Jψm1Ώ?ͺ|εΔAΫ—ρeϊΜί¬(O†ˆ¬y΄«t"U’½vΌου΅΅ΙΤυŸΑhΘ#pSΟ;&˜E­ŒIΪΘθ»{ΟΙk±wΉ£Ο%g€qUu‚†οxΣ[ͺ%™τ.½†Τ†‰9˜™}ZxήΧιq`―tπnŠ'‘26*Jη;[Zε0Ζ^–Ί&b«XΥ#—jΟ{ώ₯Ζ-¨ΘΔQθΰοΌΆJ‹”žΡ ›`ΩxόŒZΜ$εΏHI +•¦ŽΗ$){#‰„7ί‰=­›"e^UHΛΏbΗ&§@3&­NΰXΚΌΞί|Όί{ άhZσCYγ‘§ηΨ£ΎiΏ2½βfͺ’zΙά„°vŽ0ΦΞ‚ˆ"³NϋΦx§H§8ΟPό΄ΩtRέ’΅¨ƒΦα­?Vt¦μ4ξϋΓq#| *ρ© Γ–‰€}ͺψŒδšΨ“Oh@NHcwϊœΩ +&‡Η/ρΦVi1‘%EDπ>IΑΆCBΥΥΔvΎQΞΨcјK`>φΪY§ΚˆΆϊšϊ_ωFR ‘χ28Ώγ²E‰ζΫ:ΐή[š~mθMΝU–T½zυ•Ύ^7“«ΏW 0ύB<0Dw^‘˜A•Ύ"ΧάC=S…Χ«»σ9ΖΈhšΤp;R™ήgčdwπ‹Τ‡i9|`(‘Ξ}€ςjΥ1“W±ΜNJp§m=ŠQ6‡.3νi„ά“μo@E‚Ε›’ž9ΑM―B†ήi)D'h™ sa€`6ΩΥ᜸ίfηœσEΥm‘ΓY:Žνͺι§’Χ­DͺQjρSωhλ|?δ°lQν.ΈP$vΓθΚ'_8βί¦Μ›ƒ₯τί^Μ‘ώξ\αν±Φ +κAΎ?΅|<†©hηΒ0„ΩkΪ[ΰofΚψΧ¨uZk'θͺxΞΜΖi½JŽpΙΉsΗa“φΣ»Γ,ν^ +έ„ΐ4μN-ŒS‰‡ihοn– δΧ4ΚTϊ”l7°ι;ΐRλ>Iδ&e‡‘ςUF2ίΰP™tfz³AsmLά’OΏYˆυ}P"ζsV\Ιλ½Aκ +Σ™΄ώ/Β–`nj;vΒ@ίrΛk΄ΜΝΔΆδω»ΧΈq'ΚΊβ)ζy•ˆΞKΟNOήζ‰Οr`‘4l.]ω]Ÿͺ}ΰ.νL>Σ±ΐ«šKžfSW¬ζ_&₯žd:t7QEν¨]JπIVf©QΜ™F2CKΩ!ΰΝ>ΣζΟ©©I–²]7ž€ ΗχΞ΄γ³O'‚X‚ζΰ₯nΏωνΨg΅ιέKnhsfšΩ§πM7\L±3!―ΈŒ―c%,y_ 6)K +ζKΧЏ{ΖλΆO‘(=ύΓNGm¦αhΡeΜΊηΏw‹ρc‘R”φ$n•jH–Τ¨Ϊ ǟ’ΓΤPŽ`Μπ‹g_σ#Τ :E oάȍΤΡσΐρDœ«υ™ς¦€8σΟ+ύ9ΤΒΡΈhΧ ΐ‰c5I₯BSIE?X‚ΣW{7™ΔΦΠ +γ:†ήΆ¨­m(«6έτΎ£’3X GωŸΘA‰Α@έ FŸΡ™E—γΞ<Sϊσή0η»κžQt‹›¦Ρ{ΈGKk•››M(E-Ε²wmcζμΔdœͺŒbk‹,‹Τΐ`Κrσ²"‡kš'€ƒSŸWφηΒ#1ΰi³•MV&Yά[‰—N„eD癝ΐεprkb63 δhj”PkΒUλm_Ρο„Με‡`ΰR₯Αέ4–j46zTΙPRΆυΜτΆAŸ‚‰½[£&»xϋIΈ Υδ+}ύ’Oτl£Xή < §ΌΤφ‘ο\‹ΏΒhγa¨““&ξβU1 $Ujύγψρ"_žμΤς―`K¦ΌΘΝ—π΄[‹ževœτ3‹ζ[ͺ­yZΜvΐ±~)Rπ@- Ί-Η·€kA'©Ω;ζ‘Ϊ»u F¬|U™!±A‹>‚ΕοJZV»g.ω›y’_Έ%<‘ν/ιYΤ(‡CφτPΛa€$£­ϊ%΅Μέΰ3–έ6fΐX˜ ³—ϊ"ίΰm‘ΔC±SOxŸjrž¬fεe0¬­Φδ3˜ |&γ|šωρŸ|ƒδ+2jω) “ΪΙΑ)Ζμ~΅―4jυ…κ—uj Gϋξσ!hq57.θŝ(ŸυΡόΥ[W%ΨIηo1&fyW{ϋΡμ„HŒF‰£δπ»ΡNόΒnΉFΎrΣušZ°Κ,Eg“G9δεηΐΜ«χυζŠ&ΐ<ϊ Ν{Eζ΅zζλμύ±lĞ(ωtΟ’uΨ" q5jξ|ΩΏž τ ‡‘ΰU^υbΣρtRZ£Žπ<΄<Ό™fpg₯zvN©πŸ δδχœH°?"Vƒ|Ϊ_@„­ΫΛΉS δtΧ'ΘrυŸ]!φ O1r\ρΔ`O#7φ€œ\IUΚj:ώ[ KSxό/€D—Qt +;±@TγΈ4ΉKzI‘£›kJLΘ‘'ϊ\Ξ‡ΐ šφTΐN§c$hœΡwϋ]p”³Θ’GΌ*+‘Υ²­|yݞh„Ί―ε¦nŠX΅ΕŠRӞξ(kΥ”lͺˆο1$nώρ&U‡ωͺ;ΰ9€ T2㔇Μί”FL¦?T,@’ +rΧ"£Η*<­Φ$+)ΌoV―*gŒV0zΜΜΘ²IλcΌΟ“wh‘ηχ +rυuωω-zͺ0­> 5ΖzΟΌ†>~― ώ1„›Ξ +`k΅Λmδ–uύ#Ψ? ωs%Ά ΡY‹RΠ ΕτwξχΓΈ^<Γ”™L8ga,Ωm“…ΜLΛyε%C[Ÿ“ΥΌ^|&Αu Εu)‡|£ΨC$ΚUD š΄“mYώ+"·χ΅N fΌύ°΄†wορΕΗA~ύ„Qψ™žέ,x‚χή*ΈaEs γo‘2QHsΦςΝ#  L΄δ1λ( ͺχ Y ΕζΑ>d>_P !αΒp叀£Η:œ ‚•Ȑ€\GςxVƒQ#?7φQ?iΙ–ϊΘΚ Ξa/·ΈάνΞ!?_…qΡ;·£· “ΑKσUNμι\^ΝΚv8Κ)e@Ηe§ΰ”Ώα@α4³xΙw0ΎW Δz]KώU榬Dτ5ξ†…3Ϋ2J,(ΟpΝ=χτβz9­dI`”Usx;#•©FIKqEŠΎDΓhǟ7E±$–ΎŸ=v[μB^Jf\Φf$©·qΜΌX'ͺ²rόσͺΠΘl„’#ΌχlΪρύ[²Ι¨Έœ(ΦΗ'U>Ζxξφ,δ…άΌ₯²ΗΓπωΝΊ&"“kϋ8nXjDw α?Ff½ΌΗ A#~o―Ο*§δΊm„^ MdaŠβ°²ηΥT³ΦΌIΠ _$œ§=o_ΛZ8χ άp’0θν1mŒθ¨ν!5)Ριa>=P«± {Ϋπ©g¦½ +7/Aύ +F«θ5$ρΠΆW’•ΒρέQέD‘9£Έ˜H~^ŠyΟY(>8Φ@―š7F’—m„+Ψ•ά@»<ύr[ά^ήΦCr„π -οζx6€ύ³ΌχEΒε0―–ͺΙL +!½ͺ…~Wdž[ΈiΧ=qύd’#N£π8α”ͺf7Ω0[{όωV©0i4x©O!ΰ¦8tF:`Rkƒž‡w3˜kτ$oέ"‡]¬}OoΈξ»̊ΒΗgΣ>ήeCΧδ3²©δ‡~Ε,%¬·εαHςΧ(c»AA2­­ŸŒ!‹4ΧΤ5ς7Ζj΄β@ΎΧfj-RΗjφλQΪšhr"ЍtθθpͺV027Β™.f‰PΔ‹…γ[; ³XεL!ΰΗ€αE±—£Q’ΫJrɟ3E»{ς}–£^mnVxt€26ε·Q‰δύθΨƒΊY;R—9 Ÿυ±τξLφφρPγ‹UΎL™ŸœŒ-ZΠF-°Vσ€Εa%eν¬Œ^¦@­‚Xfq0α„7‡•@):UW™A _Bψ― ΰ‹¦…>†ϋ™™Κ3ŎaC’—[?xά#t&κ{ΜΛM€K©ŸγΡΊnψAΏ—ιρnΜΑ-Ξ8ΡφˆΊŽ6ΌΪΖΦͺx.ΌŸ7B(qήt#…>|±fvœΕδΡ¨ΆΈΏŸ­_ΐŽζƒ¬ίAΊU6ό$νmϋS!HbΑΗανΨΡ1ΰi’XEι…–ƒœAvuΦ €N£/v4™i‚zy)όΩyͺgj΅–-#™R βxI3‹΄T^eo>ž“^ςͺ€,‘{pg]αΠέΊ‰4]흑ΖzžΛSΖFώ…‚•h«’ͺ_FΔυXηθfΗ#xsŸww{d±ΞǚύΒ@[υaŸ³ςφWΓk $ ₯oς˜Λό³­ ΈΎ„†%\O­†L1Ζ]j +x^pηϊI@F|©cVWJxέί¬ϋΐe‹ΔΟ²a€>ΥQ'»ΚqΚcΑωKT:}Μ!νŽq”ΨρH2 C†Δ ΉGλw*XξψPΞέx`[•wo”† 5dgRρ=S$@Ÿπ=@Κ8«3-ˆ„ϊ€μΤώ«Br΅PAί „ŸM_±Ϊ€D9ΝˆfΤΩ€¦SŽώF―jΜΚτΦcY$smέ€CΗμ +½M}π™ΌτΊŠ +.`ψωx]λ^τ"·α#•\=\Šυ(),Ta­νYŠ9υ?~Ϊ…a§τΩax{#)?4ΞΧ2ΓΊSόwσAYδΩ&ΰΞ8žόWv‘2ƒ„ŽMr·ϋ<ν]ZΣD›™[θiHΦ¦wύ(‰έgμq…ηπy-ΝyΥΦsςL ]Zθ+Œ­©Α(—"·B"Ι³ΧΎ°–ΰ$ ί7snϋhΙ7RO»:8Ÿ%±0ƒ—λ|šΫώΫλ”m«6‡Σ$X `ξŸΑιKAυο6dS½ΈO»Ο3RςM,QόL!Ζ-άΎ€ƒu/Ύυy™™z΄ŸŠ9²“Y&΅šΠόι[R°;¨¦ΰzΫq·\ε#¬ΜΤΧ£ŒΛΨα΅l„qp7k"”όOž™VW–TΟ_ΐ‘Σ™Vm–ζμN!RQΨ\„S*½{5ϋIς6$’ΓκŽλ½Cυυ{tΠ΅‰BΘΡ“ϋφ!j€Eh|?R‡υ@@Ηο%ˆkΩ„ε€Μ»FτάY+4?Άfq=Šfjy–­¨‹ΕέΧ5Α2]νXDΉ ΩΜΈψΙfmy>€½wΖe€ΓήΙV^ΗƒQiΰΔ_E’φz*οΑg@uP«#'ά‡X„“ΏVzߊ―Om=KT'McΛ3 ϊE>νwŒ M(Μ2«>ψ^ςˆvμŒUρŠ‘ή ͺVY΅@l½‡ŠΛ²ζ\ƒ±λ0Z‚`‚u5*2σPX:žwˆφ¨¦Vfi`R=Φ΄–ρ‹Š©Ξ_Ξ•¬ "ZƒΐxSm2‡^pZ@€ƒ''U+!Έraοτ™x%•Ϊκ›(kτΎj»ΓhΣ±>φΉˆ³DΚA“^ύšfΦΌŒ,ώ…ύΫ&kΪ0pR»ΌΕ‘5FIΙŽ•8’›”A>΅ΜJ΅Œ^•r4Έ9ά“ΰ]ˆ$θΞVΓί8ˆs…,Zύ«'Ϋ`½δ폸Να₯Εέt—ΖZι}”?²ϊ\ΰ‡―*3δnMιxŽbe™lTΐΥΨ-]iξt+= +žόεχvΗ ΨΪjΙ,ρο* xΠ5ΰΣQj€”¬3˜σžΑΗΛN~σϊ΄š {™¬Χ.wRe{[?Ω}HGΚ~5r#ΐuΚ,ͺ‚’,‰’ηŒ ~¬ϋΰϋΫΫ€¬Ρ2hΜ|ΰœ»Ό•xΞ*dΡ³cΕΟ?ζ<SΠKωΛαLΩ–£"ΕήΨ …>, q~$‡–·&– P‘ς–ƒfφ –θ‹Ϊlbyτ4·$ +L·Ά`©―₯ΰ5šΝθŠuΒUφsnΖͺ3_’¦¦Ό‘Ι0άα(ΆΘΌBV%ΐΤ„1Β‰ΐ&Μ ‘’Ήλϋ<βf8"νYi”±NφHž †ΉrίGD"NWΛ!—ρ¦p›αΛ’RAVUί™gCW­q žX©Β\Ό€ΰ΄£ΰe5φz­ευγ!¦rΊΦΎ-υš 8 2XŒΟ]Χo―Ϊ‹ί ΏΏVϋz€ͺ‘°΅“MΙκƒ[”Τπ$ ¨ŠεΝc#«9#[έΆ»lO² υ fU&29Άηu»2,Seο¦—Ξό‚dΑryj ³―Όφ[Ϋj}!ΌP½mΪψнˆXiΉHpντhuyŒΛF™xDΊΎ5·rΩωw~<Ά– 9#܈a=ΫZΥρ½8’#"ιφ$ίyχΐ₯emΏKšλΙϊvdα‘³Ύσ»ώIyarγVF!œ·§€` ΣjΟ&dΉO.m«‚ ŠWdn%υtfτ·#vμε\ΧLsΞ'‡=M¨IdXπ―Φ¨ζ&Č fδNζH3SΓάΠΜΫ^—χ'ΆΗ ιquNM’(@wΫΆmΫΆmΫΆm۞3ΆmΫΆmηΛCRΙύ ύ½«w­L-&ŠFχΚ +Ι4LΣ€Κ–Έν€}ΛιΗ$,f~hZ †Ν¦δZΌΘ5γa’O€μ7] £Εp›’«Š*΅ xs#MƒMΒή5§:|§Ξ9@‚ttfΊ£€M¨ͺαΈ¨΅#Ϊ&}σραŽe}€΄λΌQpŠέ\§ΦvŸD;sΫ±ΰUκΛμNEΤM eθ|ώΖVΖ·\΅-΅Μ>Bh°wv?€"ή/ζΏ#ΑΣ#·2ΎhπhΑKυ6Ω γί΅dsΉa©cΗS,ΗΰGόΦ;7ŠΚŠR‚ΈήΙιΆe:ΑPδvΆΰΕU7‰{έrXΈ4P„³9χc£ΟŽœcbnͺ.C΅κAζη5εEŸ¦L`Ώ ?! +?,TΊμλΜ/E±V‹Υ=^Ζ„“)UΆψ&?€ΐΓΎδΜF#άστ4ΟΙάΉ„¨¦WΧw2\’ρ[ΚΈ΄ZΙbδί$DS_ι&^Y­M_qΞd)wGςl7ώ½ήd‹6οx ;΅h<Ž +XœՍ ͺ Μmˆ'}Θ²¦“LNΉΓ-†υώΡώΕΟ¦ΰ%^[Ύ|œΉ/ώmzœΫJJŠΖαAΘΒ–%Kγ˜ΙΉ`srβ‹Ίcΰΐ1gsύ1|ξΘ΅"d ςz ½Χ¬'7«δlWƒ!G”Φ€ Ν™Ά­άΎYΟυcΰ28G‰O[Η/p…žΪυήΒ^Vμκ•'i4"BΟndpπλμ΅ZεŸ(z4,Žίσ;Iή€5αSVκν‹CΖ9G΄ό0X(ξ΅(_±2Yνλ1.v3RΥJ&[: U53|ΦlS\ MŽΣ€l˜ΝSCl"1 ςσ‚¦χ»v6t’ϊ{‰‰e`¨|.C~Α͝U…ΡΨ‹cΡPZ)W+u\ΰV>"$(:• +ͺLξΊ·L{WΦ°Ρ–žzof5wg½Μέω›UΎÚβ©ΎL(Ϋ‰£­ +^!M₯_CΐΓ•qN”ZƌNj– LΧ.ΜόX‰όup‘€y¨ιΖ`maΤsΦμΗs2=:1z ΒkG0>δΡέ ͺ^ύN5M|χF6Ž^©ύξ ίΛvΥdczswƒΙ†ΰδΤΊΕ ιšYΌtϋΐΛ֟ƒ[£«ΣZΰ,Α-\œϋΔkά>-τ]ρΣ…Θ–U‘*&γ…'ΌK e’v ©MŠ£kΔά―ΝωL ι1TΟΖj:lDΙ¨Τ’Π^23ΰΠ~MBηϊ^œΉug.]ξ©?Ύ¨%w΅ν­”?όRž6ž>³„%ωΜzƒ ƒτXCΆ+uueeΉ†Π6gŸηΡpΙm%BoσδΔswru“ycY"œί +χŸ)`lΏΛ'7Gάο;Α@KβγmUcίΤΘεE© ‘$…D<βΨL[0G…υU‹Vm.~nfΡ5 +;κ,@1Ep „λφMtj[~Ώƒ:$Α-i1O±Λ”ύΡ2ΑκKqP03΅JQσώ,ξš:`ό­9Uͺ·ουΖgΎhΘPΪ½f «κκlyΕ}cΫΠΙ§μoςυg—`R©ΉvgβΗ”ͺΰγ ω5Μ;€«νWρ‰žͺ9:"ˆεςΠn„%yωηgΖ-Μ»Œ R]cΒWc ,HΔΨh\ͺ3›€―oΎˆO_,ϊ<6#DRD•+jϋtΔu|ψΟS?|\™ΟYΨbΙx²X=ξ@5τς€Ζ ΄;ΨΟ±4F"}ΑΪ&|6γ₯F”‰„+φΜ«Ύ{ΔεξΉφβmŒS7ψCdsL‡Jλ7¨ƒάήQ½ηI$MΎgΚή)™kΦnŽ2‰Œ³η €φϊ‹”Κ!™”F8k9ζ-ΰ2—§ψFύίρόLΏaρX~jŒΐ'r–KΊ’ν² ……`B±ΦΝh \,”p`ŸΏΩΡVπLκΦ&–π‰Lή3EŽ― +έfΟtZ7$Υ½ΘΖ’r‚θΊ"ΊλΓUΌjZxζΧ9©·Grxh€έγm¬Π>˜wΆ#ρrŒ8X됕pΊ{›΅αδ†ΛΪ\~ΜkΣτ¬|¨Š‹u ϊ&™<€Ω[»Œ™¬jιήcώt•ΈΧΑ‘Φ`Š?zΕΜαΪP_%΅!Μu£ω†³Η~FL;EαuͺένxWCφΣΊ8«€;)s4|£½,Ηός©y Ωξή8γW*μΛλŒλjj•˜žΧ¬n|½ƒYSH_A^8Ψω„YςΑ׏ι¦ά³!:ψ£ύ—EΞD–x$]’Oά +?¦Τΰkκ4"q?ό2{XΩυJŠ €u~Ξ R7IxŠΒͺ`jƒCyž>ψ‹©K%>eΙ@•lʏ’H>7mZr$3Ί’6ϋϊΏKψ`οŠ+ΠAΟΠ5„ύρqunΌ2δ„τ«vJ€b‘ι_uςτdAUώFϊ˜}SŠκοήh sZ _puFY4ΚεΟ‘$σωŠT]U†8γCΊϊ~Α%Ώΰ™Έ„‘¦χβ|6O΄‘ηψύ'k‹Β…Kc=IM$Κ7‰³‚έΞ;Q@ΉΓ&’V³Ήν‚"Α"z‘Ώ:Cš&VY)yΣ>,δn ‘{OjgRνeΨη»_ ‚Ηχ Ώ!¦ #²δ»}€#Œ {SŒ₯-ΕX@o9ΉΧν³³:yζΖG6πχ[D«4Έrx|x`ί HτΘo Ό­–=ΤΙ<2jwFI­yš«γ:A”Œ–¬°ύ<έΞΰνx°I!vζ/±\ω½πσ€s‘9#A08U<ωΣ'p’X―3b?$₯Y|j8F―*œ z2§ibYX:πKήΊLβtΰ_ΠΩ-pΤ}„«=—kόδάψ­ώ«^ηΛς;ΝώⱐƒςkγJ’#/Ώ£u‹jCTλodgΨ°α •qΌΎμ0Bΰš<ψžΈ\ΤΗkeςΓ|«RΉAfο+=—ŽαH ΅Υ\»uξΓΔ/6½ΙNΈU°ƒaZ“jaTΝ8όΈFγώΘY—Ω½»Q=€6 υ;KζΰΎ τ'Υ‘ήJ;ΊΟ ,Φy#£*£€Ή ― ΘΩ"q–©grδuMκh!hfcHS½ ZψAsνΏGcυrπ”hUε―?!K[QΫdΌߍυΐˆάγ˜μ₯'μΈ°+»όΰΏMΔtσƒΊΗ­&ΫΓϊνπγβLςΎcο²π°XHΏ‘ TΟήΜ«il€ύ"»‘6{)ZCt‰H–*έγE}ά‰ρs{ύ³`€λΙy;bίK}ψ>u‹”ž©B(ƒz`@ςkΗLώIκ*Zγ4χΡ!]zαί›:]¨9ΰ-#žŒWΐpFΔοΙ„΅P—Χ-ˆΆΜ‘4;ψ_°?<}?Ω#’Cš!2 sΒΡϊπ¬ώνcν3Fβ\OΩόŽaB.Œ%:G<Šz*σ˜V~^,'³©y-σWόDΧqCΐα>Υ7΅•ŸΤ₯%Ζδ³€ fΪЏo4tx’aƒ'Δ­Ι; _»NlΑ©{ 9πΕ“VΗGγ,‘¦[κτ,8bB+·@ΥLΣQθo$,ϋTψΧtεα•XF aɘ !Φεϋ©Ψ[ύm€YXSέ‚΄Ύ6.οΔeΞΆ’#-ΒRα\Κ3ܞ%H?F66šπrQ‡OΤBr»Τ]ŒZ†BΌδ`GΣ§°χύσ˜υ–{†:(Ν +μξy†βyΩkβΛkδ»© +|„&ζΫ\ώυΗ†2pL/œ1TΈ¨vb쬎ϋ§Π:QgkW}\€φ“ΖkΌ>ΝnΨcύH―o~σύf*β°ώϊ ]䠍§Ρ™ΫΔΓ#βκκ‹,Ÿ 4›ιΥπή7‹$OQŸ9D— xKZRCύP–kήoΰγ.F­Πͺpγ`S3χ¬κ―Ÿψ΅œb’œ£φxfΝkαμyN‘P#έτP`1N]ϋ'9ft=—ΟB`‹D9#€MΙό¨q-·Ώ_UyεΘ䇓;*Gζό‹™1 xΓOΨΰ±άΆΩΎΌΖZƒΝΉqέ'Δ~SŠπνTHTΡJk‹·8J†KύdZy ΰαήbθwy΅)Τ,ζ+9vm_Rύ«*šΐΣY rH›ηΕβkΫ™έΨ€€Zz,V΄0₯ƒ˜\̍Κι΅\l}rυetέr9τ:ΕΕ5Œ9Χ{σφόˆ³αˆcfΙkNχE5N¬k€@žξ‚Ξvαηθε™65E ΘρK<~  †jΛ;cP€ξωub.zdΈΙ$7N§{ Ž89.χΚΒ€:³ΉΊUύΥcrg―τ‹,σόžμcԞš΄Α―'4&λ Šτx\G‹Iεnβ€Zex-"ψ²‚TŠΑMEN¨±R:ΈΡiΣ‘‡ 8aζ₯Υƒ‘Ύ0Όάει18SA±š ws WpκΎ<Ε ‰Ι=Nπ©‰γ›n#9¬2‘‹ €χω uάšw–T^Π?‰Cέχρ„΄aΥ‘!τάύ˜ιΘ γ ½vLΡ½η™a‡_ Ђާ‘Ÿ’””Œb’Φßȳm΅ά|κ+ 'ΊQθάU‚‘*α 9›EΔΈTώΚ|b +ž˜&&ršσU bφ•Tφκ "J~oΫ8Š’ŸΓ>es9w /^ξ+©< Ε•€.]%γ•'λ—[ўWΩΧXΊΐ•Ϊ ψnΫάE`QcΌ“\ΘBϋβή©¨•©4~εU]’Ρ‚Η6*G MρΊΓΧ!Η'PΞMΏahκΖxœ` /ͺΊΌO¨δŒ@ϋXaQ‘;όW9έx*©P\Φ\"‡υ ₯υθb‚‘Μ•Ό{ΘεaΧ;>/Λlg›ΧfLΦ@l[Ω +ŒγΚͺM€<04ΘζLG!εψ‚H@<ρΞ΅Bί!„Σ»?Λ aέ8βŽΑάό=*ӎι·~Ά|—ς΄U/3*˜Κ8=`h‚Θj°ΥΟž’Ω1’Š}ςΥJz¬0\5‡AUvg>ςΡOΗ|iw]zk4½__GΕ*ΡlΚPρNΛΉΦΫcΪηΪΡ.LιŠ₯Ά*σD›ΐa…ͺ„±8 ;dVρ8ΥέγhλΉ8‘ΓΚi΄ΟT O²jD³;~Ήmνό{[χx6ηm`9Γ*Η–©γf܌b  Β7ΚΏφ 2yg7™ΊšŸψEYΠ,ζΗΎCΛΜ7ΧОi“θΔζV«ό>z‘_€-–ψηρۈ`‘?[“M™Ε€Tθ| + D’ Kφ1žΗP/―.φœgnωκ_ΉrE œ–ΟD1=φ¬“P H£mO‘ζώϊΈζΏwΠŒL-ŽΑšFύAƒ_“—6 ςT“Υη¬@3auΏ Ά>@ΙEG CΉmό1XΪΒΧ¨YV¬ž {…|ΰ€Ϊ9]›)’ς_OpΒ†χ;1β žΔΥ άπžfX’ +”­}ΐMρ&˜’ΩVΐe#jζ*(‘/Άzm%£Ξ«€θUΊtό θΑv@>ς¨Ν$Νp·J‘εŠ‘q{2}€ΠΡΐ\€(†-ΐϋτ˜_Ÿ|ΣΓΈ/ώώ^³οΟΖΔm* gφQ^ζ ŒηΫG:Έ5‚9Σsl½…Φ@ι0ž0t'dθHΖ¬ΖU^ω·Φ(©ω*ΜέρΨΗ ‹kπ»„YV>x:λΊΙβΌΩLΜώIQ‡1μPLσfω•DΰAοˆΓΗUκ\ζMu›¦μΓGŽw»΅Y₯š7Eτ#.›Ρ¦ͺ[υy;€VΤ|ή +m‰)[«”>ώV„ Τ/Žr‚­Y†ΧνΆί–1 ΌΒ­Sfo6Z͎Q=,£Œr#O»η(}φ{§@.…3ΏηD¬,Υv§}r7r3tšΩΈ{ ΘΌSmγšΑΖ!ώAθPΕ6OΣΓαΦ™€¬ΉgΊιΣ$ϊq‹P.–MƒgχwJrΨπό/ ±1qž'AΦΕ„ŒΪϋyˆ"ϋaŸ\Β.ΰ'ζwΊw3&‚)Ž€Ρn†η(φά¦oΘͺ†ρ=³ξ".,Υ‘#ΰ}φΣyαϋzΉOF;}‡†Cb§ŽβŒc~Dύ:4Σψ_|ΞL@ΒΠάΑ@pS“ΤhSuΜؐ›MRΕƒ/§·πΙ»qqzΐ©b$‹J~s†zy _κ t€ ύΪΕeΜ S± ½ϊΊšΎlxλν;}Τ—xDΐu}ZΌ!a„l@˜.‰S*ΣΆŽ‹ύο]sΦ^˜ŸRv2GφΠΫΣ‰+«ωΛΌΈ·LyΠ2tί©ΐbꂨ)ΝgΟ X ώi1>₯²MQ¨,ΡTX² [Γ²Tψ1v½#α²t›d₯‚Π…,H˜€ +θΛΆ?̇½§ΟN'¨>p~ή rYϊΑ†Ρq]΅ή5f(€TΉ­Έ6π-΄²ΟΡΩo]2†ŽΟR 7.ͺθιgyΧšŽŽ…ϋ=pη—•sωΫ0F‹6τι“@<( z~ΰOΏίξ±Ο’{­†ϋFGIοnSγAӟhsέ’₯,$ζΏΘJzΰFמQΘφϟc.5»­±S {‰uΠτ½™γ§OKS>ηu£cκ:Η‰DΪ )r£K«Χντ½;έ_”σ9„ς‹σy„hΠ† Γΐwβ1‘x7η +8}Ε©ˆ#BΤ'Ξi™XύFΉrˆO4|6Χ27ŸeΔN;`&ι·-βΕd0ντΈ€(.Y§ώӘ…™h^η2¦¬΅ψΈΫkXf%χAε·jΖ„;—ώ§}ΰ΄+G νρΩωΔ(NnŸ€j2βNΣfš€»˜Œo―9d5αiπξs€(―mΜτ§¦έŒˆa­œDΪ}bnωτzyDΙd­Ζή?τ¦G󁒭e†Ηζ$ΐbΪ΄uΆ œήB«OG‘#%ύμτύ)c©ΧŸ€uc²μΫΞυBΘ™_nŠž€ύΥΔIζ¬1ΐa₯γYYη« P+ω*ω>™Ÿj΄@pšβΞ8Aθ5/}Ζ―R#“ bθWλH+Ϊ:Ÿo΄€?J]ƒVΤΣ“/PaίLe!΅‘φάζ―nœω'rшχ¨γ"Ιί6Λ ƒ^‘ncΆv'ςε_νΕe X ϋ„κe ­ξί§_ό©ΎGλ/Υ΄ƒ¦ήΉe!d-K+ zΪ%τ˜nS rZ1[ΐŠΠΰ-Hfΰ€–Z›gΈφEκ‘σFρyJv΄ΡβΞ|&j―„ίA₯ϋδJa=xΥ²ΖwˆέT―ŽœiΡΪ΅Πƒd­(->ˆk§4±YJΎυV΅”Ÿna+―ΤͺP$0£C,ςΣόθΦ‰χiΫ[-Δ8„.IΫ~ΩύΕ`M^ƒ¨(fΐ,& ‡KnΕυΛt[ͺΐ%βq»L υͺͺM"δh'e°ΎˆcJηόi)y‚°Άp,Α†Γηηhp}ΐΜ…1Σξ?…ΡΖ&T>i™NWͺ‚¬’w{OUv4MŽΪώbX-]RWž”ΥͺA­VΠpC ]P£{TZ–&ιλ$χκ—Oi©Η!…ΣΔMά ώΞ³»ΑbNRΉtuEאn(7Δd*Ro’!­!χ9>T¦Ρ¦2ς‚ΐΖ#ααώ1Cή| .ͺ±~Κάΐ΅ sΰΠ +ό“Π8i^MΪQΓΗ½Β)J—ΛΠ~ρφ[jH%ν7±„εΜ€ Ο’Ζš;˜”Ιρ +ŸŒ%ωsϋΎtζJ·οΥ¦nαέπŒ +>\¦š‚ό‰²ή»eTdr΅βg&^ό‹ΣΎ!¬ ]7?aυΚ!Œ#ΨEΕξ>j ‡APέXHI›qέ*'{Ώ^Œͺ–Φ[[TΣ&§έMŽΥ³NιA20Ρυ!Κ^1M&‘όμ%ν"Ξ³‘*ΖrŠΆ}xW—ŒΗΉ]±k9QΟΎaή‹¦u#D7… Τ$\ί©aΞ vΨ» °ΕJύCμU²γ8>+ #ϊ…Ξί]—°ΣN +Ή£ˆΦw’ζm~ΓΐW ‚j6Ώ³zNv«Erώ5ΡΛR‘U—Ρ[ΌWˆUŒόv9:λ›ϊo+8Ξ³Κ)ya°δ½­3Ρτή=gjvyςπ‡Έ:¦³^Yσ •,>»’’Ν»Ξχvλ?Hή%K‹υfLχψ,΅gϊ9"ˆF?–^Υ5ΈŸ@`QπΜVΠφ˜1ˆΑΰgη‚ΏώŸ‹΅Œ£;|›b“ρ~.~½ΐͺ*SΣ5³[OŠZ.‚CSηβPG·.Ϋ aoLA\M•δΎΈ“όΥ"œ±'μtΏ†φΜΔ€[<£_ ΓυfνeζυaΌ‰&ŸοZιΑ-@·–ͺ/v` vΊyΖwβp…λ¬’yΤͺ.ΏΡ~ΰ΅-ŸaΡF~ ΰžγ»€₯œ½=¬OίΏηk!’ν@,°•=bέσ4„©Nη—°Μ₯qbrΔ­΅l“²Χ)J™Αό¬c/ +‘v©6#ͺ σΒVΐ„‹Ν>Th³FSκΤjIέΠnά’ΠΟ`wDςx bμLSέΘyΡJNαŠώ2PŽΣΟ»ΙGέLΣ™Χr0ΏΒyuψwEΞ›($švSofŒwGαc› hε¨W°Β“VνΕ”ƒφgΪ‰ ή`>© £²Ψi>hiZK‘²(³³>±A½0˜ώΑnB&ΓζHDšξ!C»η]Ÿ`ί°ι(QBNy,5ΌΒΠά–d“ςG‘ΌRθ„„Ybˆ$ε`έψžG·LΟι§R!«€+Cβ‰p‡Ή2”³ήζ(’I;a9Ν‰ΔφR&}™ˆ@Ω½¬ΐ}Ει¦zZ;Dύ\ώ(uΌƒήΨ.¬!xτϋ}~ƒžΐT4E'ίε²UΝ•-c;Ό¦ΩIuΛ‹―FϋTΡιΫͺKRrΞνϋΨΟͺ‰{C=”OΓ9Λ-Φ‰Uc6WΫΆ0·I†Δa]FΦ*jžz·όU|q–9γό(|9Βnΰœζ=}rmDR¦˜Έz­3.(₯ξ1L@KΛOφœ"}'R‘p’,EβCΐœOτd7@'δVΘ=σ~5ιΊ2(S»xB0!d]½D±ςζέ`ZjUs?"ŒξΘΜ^šάGΉ7Œή„ ŽΎSΜ`· ΏΐΊ™αϊΫrτ"ΝοIJ‡—! ΝΞO),?ΘΚIήΫρwΛ/ψNrmDsςA>ΤΤΝž.…CjψΏοΓIΏ«Κ­½χΡqnUσŽΓ’ω^Ν c*> +iecF&‹Β‘+ΗΏZή,׎“325ƒŠμ±Ία=„νΰkΈΰm{Ει#κ.Y_ς\–νς“ΞΨϊΞΟ‘ε]lA/] :MηP74³<Ιε&•P¦)DˆΚͺ5 a`¨m)v$†CcЍ5kShωΘf —Ž χbkΒχ/©kXΥζ˜άOςΐ_|±Σ΅ͺ8/θώfhj=LΞφ9e*ΐrιG“bY+{pυŒ•¨ΠΞ;vtUQŠJHΥΒς(ᒲ‰χ0Σ"²½œήY/bReLK€5@uδ0&LΠσ +œ0;k’ΣD_Χ›ζζ$`NΈ_›…eοЍ„s↫8c{VΠ|_ηΥΜα`π{:ή c»ΔΒ1έ lΝ½γkT-[Ί§ΊmΡΫ±D‡;²΄z₯οδIsψJ Νj&ΈΉ΅ Πω²‡H€ζ©؎)‘#.)ˆ¦œγI‘Ωƒ +@|λΌKΞ‹&ϋ–-ΨV1Ω%_]π‡δp6˜Γf<‹›σMV‘%άUMˆω1υ~΅>ΈΞσMGoΑΏfφpόdΌ3„‘λΞf·>‘/$ωοβ;Θ}N„]μa€1Ž£1‘šςC“w"xYA)υJι₯™guM²σ Φ˜\₯>’[ƒ"ΩΗ1%{ σγ]5/Kv)d+„Šh™΅ŠG„,Z<‰›<;—ΘAN7½Ε°Οϋ`‡1»yh)±rε²fΝxnςnbZ ‘2ί³ώΔ „4x:gu Θ­΅ΊaA2]ΧΣ(zΙυ$»FjZφ/š—cΡ.…γ»{’.»nνϋ ?j^+V<Λ| &'UxpΥk<³³,Ρ[fW‚ol±–ˆ…5Ά'my“pLΑl*TΈ/ΊfψΚΥΓλΔΉι'΅ΙOΈEεNλαM–h{Œΐ5}οΏKΌmΑαΎ©/ϋ`.‘\cθ΅(τH…%ΌH_‹Ρ,r1[QuΧJπ8”Θ–₯—<α›3NcžXyŽΠΝ]EbZH‹Γ2?ϊC»ΝnRˆk bίgT,έlφΠφΚl»DΝΨΚςάfœΖϋΛΗCλ>[qX¬1GŠχ~· ηο±γw2ƒκTΉͺΠΑ4Ο ξ|€gFΔ>h]ΪΔνΜΖ•νQΉθgM CΡδf-Ίς‡—€ΊJ”I0q3i1n'Vj-΄{/¬0nk™S>– ¦Ξ~Ίό)‘˜λ!£Χ k…°³9ζβ5Ε}[¬QΐÚYΪΊ²Ϊ“tH‘[Ξά¨ΑΎϋΆχi]VZ‹zΆ½ +e\ξSιžΡEα`|'ρδΆ©<e=—AΘežΟ΄?‚ΌŒΓϊM’viRΥυrΤΒΣ9ί/$?Ž›QΌ΅‚{8Ψhμύ₯p`Α}ΝEΏ±dhΉ…0‹•ξθξH™;5ΚG–ΨˆxH‚θ“™zl7 ₯8€ $°Ψ]4EΠνsΟ5ΓΛŁո،όΏUκύƒlJω$Ζ‹–ΉΖK$ΞωΌκ•5”ύΞΕ•—Έ@χŽžο­vx]ΊHOωΕMξΨV鱟^qΡ›v,―\υRš±μΡ8akΞtκΓΚ_Δ:_(«l΅Ύ‚4h"/Αίbv Κ’n„Ο$‘FGκŒοuΈ₯Y£eRŽDDΎΠθ­«…ιc4υΦ£Nέ ΆΪΪ&&.o”&TΧΈΪί +Έ½tσ#‹ΙxT°»Y$ρ¦σΰLΎ' γΧΔ(νΘέ KoΪθbu>Ά-γδ›O4ψ#λR ή +όνΦ Ζλ +θω§>ξ τ«›)fu¬R‚,„ ‘Άd5—YœΝTΓ”=Β¬–φšΑR΅Ό˜}ύ‘w#εΔωwΑ`o$©\£ŽΦν†š«τ~φΪ‹-λ4Ϋά ΚΜ-)θ‰ψtPC²w\(ZΎ«Ž Ά76*(’iŠ +­¨C7ΛN΄|ςΌ‰πΔ ¬Φ yΙᇧ«@Ή•Δ2:;-?kiΩbιθ‚Βgߎ¨ ›5΄ί½VΜwΑτΰ·‘Ω`²””l_Bφ(βΉ ΩΣ `ffψ[ہΰ40Sώχ½vGΣtCQ£v°nίΓ[ +}ϊΙΚΓu¨°;Ζ§KhPήμέn{-IΦρ˜ε¨‚Ϊ -3΄΄œ‚Ÿ+>ε…{;œΊΥlG˜‰+ZŽυe\\}¨4œκž&τψ5f#]Γ5saΧe<ιχS ξr鏆Yή}“Β6α>έ¦0ώ’d_2žIι +FΞ"“•z’U&DNv™D^~†£pRK2Ρ%OQw±+…ΊkEίS ςξϊ›Υ"YIε!–›)uΆCΎΕQ€ +yž~οίzE·Αo'€Γg€²Εb§ΙΙχΑ₯υK»ςψ-ϊωζIθs6Nfύ4 ΦRKφ­,κXυ¨χξb»Έ{§|m1άΰ3XkΗ' <˜ΐκξĐΕ hζdΌψΓg„sμ Ζ:”α’ά0 Ύ‚’(‹Ξk %ο–εXQSt**άίb½GΩη,Ύ·ΙzΟ θm_–f†*cF\―^,֎š1/Χ}6ΜΗoάχcc 8hv3[sμ&žHΊ Ω+fp/§ΝΈ—ρ|ɏε$XώJ’‘uω8τ{u’ΐΑ%Εμ*­ƒ!ΊF’x5ou +vžrSβSΩ1ρΒ#š“ΗΕ€jΒ(Ϋς£σ:2˜Υb½TؐΧ~γ FκΊΥxEg<‘ΖPRή” nν•X!Ϋ–Zΐ<Ό9ΡOUΥ€8IΓO.lŸΩΐKUF Ϋhn }ž+ΉΙoZΈCΕ—*ώUυ}τjJΟ8s`7ξ›Δ^ΠΗ―eώc•a‡|†εε‚qqΔΙαŽ`…BΚΫά θ”°Rΰωl])“gVъZ^όΤοθQ΅ΧMšDΪΦΞ­TuποΨΝU υ¨(νύ‘˜ξψΌ(Q•ΰ  +- zΒcέϊ¨Œ€(gSšP¦[™ΑίΦ"bqoΡΠ€%K‹έΙΑήg\ Iΐδ{·ŸΤ!ξγυA—kώ!ƒ!7od†=|¬¦&Šš\μž +y+τχd6d,Lu,ά+‡DӁSψ(&οκFχηWŒθ‹8ΎΫNγ»χАσD ·‚F§ιŠe_β0z leg‰ͺ~n €ρFΰϊ&Έ‘+Εi‹n’VΔθh<¦σκaΦ€€ΈrNΉzψzΛ]Z%œΈΰ;_ΩƒΡώc}/VΣ•C#oΛwTZ CϊJήMΩYg^ANό \ƒj‘ΚΎξqFp»@Ÿ{ŽΨEΆGΥΣΐηw»F‹ή‰gςœkb4KΟ:uτvŒkk!¨Ο·™ΡΔ ”J‰πψ:>qν“98lμ@š_‚“hΐ¦$Œ. α–*όŸΓQΊpεΊXΕoœ¬#ρ¦Ζpη4»Ϊ¦P1ώu’F„WΦε[±ζE+Ο™ί%ΠΡ™υ«U―7γHbP 6^ΚΎ8θζU[ Q$’ƒ J]2ΙοΠω«τƒΜΕ§ψ]φΦS.μͺ²@όˆ°-•]mq―YίνmΠVξ²Φ©_'₯˜DdαΒ»x$Π›άρC >(fx³ΗΙOθΧb(s]ΙoHΐϋtό +’Ώ“±έ »p8’H:³ˆΗ²Ϊš*šJ.ύŽqχc·”ecοšΉ#Ω|Ο#DόOο΄χ_|fΏN5ΧuqQγΊΦι₯ZNεp^(yPU›ω`‡Έόp9G?}Ψλ‰1ι©χšΧ΄½΅cΚΒλ¨ΜΏGύrΠΙιΆœt3W8μ£}1οι£ ιšπτ“³οA;M? kA 1ϊŠΩNe‰D«―•mU멟-ΛΏb{o·ΈΚηO}Λf΅Ξu³\Γ»8!Ž WΠΕ₯ρ½ιAζ4HpjsLιε‚FPΎ-αR'hΌΑfܝέ1‘Ϋ{ˆ+,g.±Πδl\;;‰Ά*ω„*Β|sΖa<i£j;χr}vlυΟM+ύ€ΒίX$ΎRŸΤ-D${Β½€ηNή\%Λ@Ί0πνd‘$*a=…q`4„t` _Ε)³|“dŸ­32―7Ν»‘)Ž€zΝζΪ}‘„eω„ ˜@ɝ8M*cς™ύeOκυo&Β”Έ5δνΓΦG ³€ŸΦfωωώΚ{EΕϋν[}»6yHΑnδ©Γχρ΅ͺΓqƒλΦA½[dn‡ŒσA=F@χ›£ΤΌΆςCšθrMuΕϋI―‚ζδΚν 5͚¬Φj γ½”ύR€Ρωδ²§Eqmˆ †– υ”9ΏΦE­I½ΖްΔΘ4²λ?*μεšwΒ”>όΔΝGf”ΓzPΚ―V-τ½~ѐ݌΅ΎŽΉΤ‹/Η=­ι•$P€™œΉ»oΝyNa8"YβZ'η―Ό‡‰φνz’%―λ°lJO“ΰElŒv¬8₯,γ].`Εδη2š)„ό­Έ•iωΪΛΗ}―ΒE@Ώf„Έϊž­sέ ŠΫλÜt­όεƒ…O|D*šΧYμΈ }xdΥ`*Țc-™›₯ggζΑ©4…H–·ώ!ϋ‘―θζΫζΦ`Ig¦›μίγϊHqοbnΓ"σ#CψΗƒ¨H(CΓΞ +¨”epJ”•ُϋ\U‘λΙʊ7izϊΔ_ίΟCK’φΣ€VM‘ˆ_˜N¨ΞUHα~h9Ό‘?5p—‘ΰ(ι‘οᰎLζ³ΫEJ9Ž3‘=j‚Φrί‘Ψ³=κoΘΉΘΔOk“T"φ€¨8TXeφ“ ‡ξγ/Ή„Œ°γ¬œ ‹YDOcm™)#γα(Nώ̟X³a„Η–Ξ”ΧΪήτeΘχz~ΓYš1}ˆ³³NŽξ>Ϋό+!˜ζyρUyφ;ˆ-—_7‡δ1BGιμ%τS³ζ#–/(©jε μΚ]Κε9ΡζŸ“a¬­"bwΖ94Ζs…mͺ±Ξ›ή‘ΗgJ …‘G(I ‘wύψτˆΉ έμ”Mσi!{mϊ²k#Z8›$,ΙΚ½ koΊSe¦m,οp%ΤAχ”nΛΈhΣN΄Eγo™ΐ…‘T±ξ$¬Bo +0CΧ‰>—καAB œCA °’ΥzŸ!Ώ¬OVXΗ†½‘²‘D―ΑΒϋχ ς[³ο’D9Jbcΰέ5nSΘz’γρ†΄1Ϋ"ΘnMF8Η^φr€fG²8?ιπ0\°­Κ: +Ψr‘φ7» azkӌ̧₯KΔΗ όΑΝεπ[ΖΒMΓ€ƒhΘοΊΩώ ”Ÿ²ͺ¦’εI± gηš-M†lΗΘ)fA@Gτ’ψέnai=•Ω8a1†…NΨlj_ pω]q{CΕƒ£Ι-[«}χΥέ-ψ‘Βs„$MΑβΎ9s¨ *ΪͺBpδ¬XJδuΈ O»vF`ίJκΏΫν…Φί"ΎΊwwn·%˜:Ωz·LZcL±VN†ύ‚ΐ¬ξ`{§ q{K+m`jΞπΫtΊ8pm€–σ6φ—LΡ2AγΎQ#f₯ΏqK§RΌΫΰΨ\ύbs«'άj>#1KϋοTσΣΝΫͺMθyp«­ν£Ά₯1j+Ÿ|Pt‹‚οκω¬δŠPx`ύqƒΣ5j:ϊw`ψ2ή‚·…·9€ί­γJ[:σ‡σ>•ςΧlߟl™Ρˆώ [~A–ŸS#.Eœϋνά{έzpsζβ-ιϊ{k•~0Χbί―ζ—½X0m$‰c° Mν§R9%<Ω4Σ΄UΫirͺ&hFΩΐη Kζπχ;‘«7ςΌΰκΝp΄₯4QG‘ΩΗ8ΩB΅ζΰŽ„ς‡ΝŒ˜ €Ξ)²Μ΅xzŒY€<gZˆΟΙ5 ZίΑςs§‘έ’  ŸsΔNό"'8zœ‡6ύΊˍRqœJ|Sš»ΈWh’C†OτS&bPŠΠ©κ%l#¬Ζ/‚H \V‘Μααό=p8ςεσ“)ςgΦ •<’eŸ*Sj;83βσΈnύ€šη+ηάWϋΉ.ςQ±ξ~Ώ–hΥnIB ΏŒVT[ζ3 rs‹B‡D'y»’?π»[r8τύέHιί&α₯ί'•­dsΨθψP&¦FZ(ίrE­IμN8hu&ρ±Gu6ͺ©™VτΝ†{Ψ§Ί]ήE^-υyυT·aτ›«ΑυΏ–C·ΌUω$RZ@cΖznδ1ΡωQlΡW—­ήfξ°ΒJ9Ώa +¬N‹$‹ηδχ‡EOΗτΔ.Ύ,ΈQ@[•ή‘ςY V7jΟρ~ +ˆ=ΐ#͍ΩΦΫ ρ”³‰„ΥεΙ#‰Š’ψhέX„ς„»Έ$πH/ΥœύVLg}š1Ζ[cY„£‚άjΑ‡6‰ŸίΪΧαa}εžοεBWβpl$]½I-ΐ†‡C`’’CΝ²BΆ΅R'Α”ۏΦ8ύά²ΔmΜ~±#xΧσdύ9β”Α§τM΄ά•‚f•‰’Žεχη%쿎†γ³\@Ώ™rΒβν<†Ϊ˜5lκz ©Ÿ²Ω ΣFΏδ vͺζˆώΒ*`ƒΨζ +*Η΄(€|W­­W +μ©d₯執mΚžϊεzΨ& 0ΛpΊΘ1ž6Ÿ#’ΠψY +ϋ²"Lδ£»uFRΐ‰w―υ·"ΌIςBή—ήp©SΗ'ˆ’xp†-Mοs=€›α`rΕΊΖP±*Z¬ &ipγΟΣ)―vJy]ΥU­ΚcZ=ΌJ”“ΑΚr%*Ηνό7₯W±δ,Σ‘ο©V/œ cŸ?d=ΟΪoξb~|ΤS€«HBž―£yυV‹ι8ιd‡ξdsŸ”ζΰ/…=|Lws€Ώ…ιΘΆΠr£^Ό£τ½W±Ž‹j;ŸαΩΤ!hή6Ώ‹ψWuΟΛwg¬³o”]YM1‹R—4±6iŸ™WΞ9 ‚yΰΤδω8"΄5h ‘§ΔŒUjζV°Εz(r΄ΜCPbς᎜@/pŠ]L?iTΏAEh|E9 IϋyƒΫ0ˆZ.ΣΠ0q±‡ί"fΆΈI΄θP02³Fn†i‡·Γ?—*`— Φ)”²-ω ΨDψΛΙ=έ ¦£šˆζiέ‘‡nWϊ-WύΒΚΚΰƒi‡c:uΖ˜^pX½Β'±χ5ϋٞŠ$€…}ήkA›~؜8\x©uΌ­ζ³³Lδ2¨‘Ζ‘ +ˆ]¬#“΄ΖεΣχSAB6%i©ΎzŒ¨ΣY€―cο;uz#©ΠL³YIƒ²­~Ό«ε‡ΙVκ9ͺ7Z€I8<Λ¬υ^yτšͺ°8V_D«« φ°κ6<^#!θFώ`―½'©ƒγΎ4£idΧAΪ½ E6M PΪ`'ύ‘]gsSΏ._sfνYΤ²Z•mΗΓς4Φ—Y…Œžδq·β~y;pρ μwk0}v Qχ›mζ€Œz—CΓι.π(7m“ϊexMi+oϋβs,Α“έΙwώL°ŒsgFY˜Y5DŽ”ώMς―AφZ†,dƒτyiϊλ{8―*ΎΟ4ƒΩŸόxvΨ²3ΏDΩ£ukOϋP˜όξω|XG άιύD|ξfΫνΊD8%]ΨοΐΒώ^μŽͺΚPΖAaΟ€―²„TΡΒσ7πΝB±°γšKD2Ύ(\γ₯Υα΅’šύ™Ψ8Ο²ζš “Δ8…›”IŽΏϋUΓΚΓΨι™½ρb«yK“†ΐΉˆ΅67"{½Ψ;—Ύ“Α₯φ2­>Ψ¨IRΏ•ωR’…eώ«Ιš5ΊνΆ2λώ ΙuOOΖ iQευ»“ήˆX^ΌC{*ΉbϊΦyƒb‰Šl—\s›΄Ρ|΄ίͺφΑY#™[|*@Ύ±½j ±κόΖ$uζnjΗ`—ξο4ΌθΦQΤh8|ι™³Θj8Β”›Δ5ŠWή―η0+_₯₯Mο'0·\žx€₯zMξ4§φςΔen΅†Υa΅…ΩW,βf‘έ1δ¦’™?ραMoδ―·ψiΏΟϋZF’ߐ­ΐvRΗΞ+2D:έ½Œ-Ν ο0M-#ύŽ%™&.›y@„θς₯οyΝ ©[(»ΐ±$CωžC!–Ož0‡ΆΥΆΡ-0―ψΧΎτσ‡B€r€^“Q1 s’xIƒ5ˆ,ΠƎoΔhmSΔ">;›ΌJΜ΅Œ†Rz†ƒχIΧγ_ +tr‡£ +Όm°ϊ(n˜U9u’]MVΌh3άνόεŒ9ϊR†j]έlJΎAΞ(ί™Δ©υΑLοxš +Ao +n'Z Uκ7‘₯1Mj@ν8=D9wψΆ;Žή¨©~ ΕΗ‘£ ‹X΄§IT“4xVhAι—‰j½qί—σ:ΝߘLt(’₯!vMdω‡Ώ7b₯iTΗ{χϋ >΅αΑ§™ώVͺaρΉ.t!›(Δ9ΰΓ+tύΰ[lˆ3ΪBηΰ3n΄±ƒ8,,Hšμ'1ρ$ލτ–αwdcΕ=xπ;―εvά‹άYyΜΓψS?ωYœŒX‰;+¬ˆ(~7σsΥ W·΅Όφ‰‘?θ'3dΑ„ζŠHϊΙ(γ€Ά7¨ό1G±λ~1>Ή˜’)Op’–ϋ–'jͺMXη„šΠaμ^κoΆΞ‡«N’jυ·Ώ 5FΌ +gX~Hԁ¦\{,C³œΗϊrΘIλꄆ:‚°C«BB!αθGγΕ‹;TRžξ˞ÏΤYAlJ@…Σ^΄mr'§ιμΟΝ¨{Qj¨Ξϋoδw=ΞQΥ‰θ‘΅- -E8JŝΌfΠξtΚρεϊΦ$ Y•ν:έg‘š/ΥΏ8κfži†φ9S6ir¨ύϊ±'*FžNΧ$oc[l7ΆL‘΅mŒLΊύ+-ρ@mΐwŒ υuŒ’Ϗ§G†Y’ο‡Ν†ŠR·ΰ#ήΧ%ς|IζΥΪ8YK‰8θ + ·Y„ZcΊ’Χ„μ‘9΅i₯πLΖ­ΦSαΡzυε2WNΚ^‹o{xΨ¨(R'ηCWρΟbύά8σ٘~L˜PS§Β‚Œ0/\žΌ$œ°‘α@ κ‹Σol˜ΐ7+€«CsΪΕƒlζKΚdE~ΐ£;Β<αP« .>΄£ιMx₯{^£%P°’6GŽb¬΄κF™Yχ„ο:ΜςŽβcˆN‘βC ’hχŒ ίΧΧ^Lρ¨,Φ3¦gΜOΛ5Gwζh©Ϋqη &Ϋ†t‹†4‘Gnp4' ±σ₯\E!—1†1vuω–©pHλ{Ζ6­t »h`Ό/©*§]¦τhfΗ$¦1%ώY€HξΆΓχL‘ΖΘτ†‘V‰‘`E5qΙg›³%O^‘Ηΐ6Χ$€Ώ λɘoΤ+f¦rΞΑ<ϊD³›qPΚ@,WU©Κ—9Φ–{G2ϊ;"|GJrqβΘιLcη`Z»Dβδ¦{™\Ί|QqWŠTΥ}υ ‘ΦvLkWό„=©NΜ +—"–bΎ·2ρ[.ν ΠωžzD\Ψύ”Ζ„ΣQρΞ5˜LG=5ρ—3ο²hUδοWΌNΎ—„-―η8Bο.ΡΞͺ}ŠΐΓ³Ύ0δ($CρXTIv-_ό(P‹¨ΙΝΘ°ͺCγˊp»]ίόΈ€jξΔ Ώθ8 Lμd€άΔBω κ”%œ,Gdύ•y‘Δ5ά&eχ“Iμ^θψΪ΅πΟΗ :ΎGή{ŒύυάιVdωΝκ{΅ =άƒ?Ζ.Ϋ·ΧBpφσfNKVΡ:Τv0†bˆ†j9›§ -m!”Λψ4` 4Φ·‘ΞD¬ΑΥψςύWyν+›υX—±˜!Oθκfp3»“NGΛpœ"]Ϋ¬]c? μΰάσ»Ώ@WFubPAŽ Z±v4„ΟΧβηK mσ^Niμ4―|Gn©4ͺƒgqU]»-'†be½ 0KOB貓%ab)F3Ν'―΅o_ˆU…EJα!4 ΪΤπηΣκQSΔι΅rί’ΧΓ4]M]=―ΠΰE΅ϊΠΠΌŽ]Yφ‘¨^κ~c‰™&D|³ΒKƒlš^ŒΌœ*H$c _CUά§™β:Z*ΙQ’ΈζκZ§p[{θJ§_7έC!¦n‘ΔΘ…πΊwf}n?ͺτ+ά?l#Ψ’§2χ΄&]L5‹S6πxγgγΑ6TY—ύ^&ΞQGΊΙ±j¦)4 ϊ–ΘRΥz‘Ο =ΨΊνxƒ΅lώhO€wύ_ϋiB–lΎU’3ςωΙF•Φj­-ύήXCΦD'τ9 ½/QK*騜1rQΖψδ"*”&˜lΉ™|ZΉGo]‹ˆ.υΑB¦4ΎΖωύ½YN›…· A21ϊΦJΣλΕΥ9-‰’(I΄mΫΆmΫΆmΫΆmΫΆmΫΖnۚ1oη+jUfΔΚ_7—¨© @ϋnk€-ƒ΄ ‘Έd+f>–ν* k& {o» ΐ]Ϊ +E•£[KšΒήΚΆΜ8€@YJ΄…ƒV]Ω‘΄ Ύ~^Ν΅¦j¨u»‡–HŠΦγ!Γ†ΦI~’€%£iB ‹«Ψε@Τw–ž‹GIBUœ;΄˜}”*G^g"›§/’f‘[ώ6YGδν±{‚Mόύhx˜φS½ I·KΈ½ΨΊfϊΏ=k*ίfp\p£2LrJκ¬ω°=ΠΔΓ0ad•²αg5¨zάωBΗRŒ3dNJ- οͺZžοΖ:SώΓ•Θt6Ώ\7³FλΓ,RΛΊ:Fέι ƒυ£]Δ‚Ž»aOΎτ$ DJdF}²biYod #vΦy,V.yΊΫΦ΄"’lkΫν(ΠxH8όYbωY΄I€blAε€ψ»&NЦT+‡]%KΐΉ>EΧ<ΥΚΨvzΙ3 +βχΤκ\ΞΈυΊρ-₯7έU.žs±”sš@uΓλ½ηzЈ LCCΉ7‘Άο ω3hs&‚Ÿ Χ¬ τ’ή9£!„Dά”΅GŠBj …MσŽUΟϋΧί"L[„n›αψχΡπ4£(g”.Kη'ω`­TρΌ±nόΏοΣohΧ%¬ζ΄$γQUK€σS΄\Δsͺƒ©ƒ$ε ‚5υ±—}‹c@nΓ +ΜΎα2i5pŽ(-$#*xή}ΰ6X\‡0eΑί­YcŠ'κΪL¦Uμ„#0r ώŸΒeWβ<ΖχΊHgˆLΔF­uMΡ5Εmμ{^/0z© 5MŽ†Λ {΅·ώΤ(Ώ4qϊ™{Η‘U!χ|δ[eΛxΙMKλŸδΞKωqιτBŽlJIdζ;.0λλπPο»Wδ.{:¨œ|‚3Ğ9R|Βδ%₯γζ°ώΜ…‘°uλ)Χ&ψ¨¬=’4”˜ΔΆ)†f₯"νδ}tΌr‹՚ΨV7NV*• 4?]tκ…yΐzD ¬Ο#/6ωί21-ΨJœ‚Upi{%Μ2lƒ™σΆ½ε?―οψ|έ½μ[kθY•μ6Β¬‡.‡›}ψέξ”ΕαφϊT=c'π#β؞6€vπΓ+k™}«ŽΒEˍžr1Ξ{Α·&ΥΈήηΒKb~Ώγh_λYDE=’fucaυι¦ά_²lΕςσ™‡ηŠΚSύέEΈ θn˜ …†ƒ&“Hώ±BΪsΊ*Ώφ†ͺˆψb ’F Άͺ•Ω€Δ@e%!qΣ^¦oH!3«γbNΠέyPΰ[(Ŋ_XlzTμU›Q₯™΅ΩlΊ ³°!εžΝέξ|΄cΕ7— bΔΪ•ˆΣ…’“Bό…^αyΦ~ •JΧοSήυD&yY&zxΓX˜‘Ÿ§y€χjrR†ΜގΧϊφ+SΥA{p»πˆ!κβκR*RΫ‰ύ€¦ ̜­WΔ¬Υ³yPβM?d°ΖΚ―υΆρΊύFI^oοσΧ\ζΘΎΔΉ·ͺτΆWβ\b;53ξϋXΘτ_Π†σ‰Ρϋs9n]FKζ—€_»Δ.ΎU(ouβ˜€ό¨5 ϊ6 ς6†t# Η—§Ύ‹cFŒ‘΄φ¬"4ycšBOL`ΜZl‘Ž>υH›5·λπ^-σΨνΣζ|)jΏO˜?8œq‰XTg=o•ΖqyŒ½ΤΡ«:Ή• +Ο“–{nΉEUεxοXI~ήͺΈ>jWL|œΙς₯ŒρZ`אυΘΪ˜ζqηcœυο}†Ιi2½Ϊl§ύΑadZΉ¦φ(Ί«l‡ 3Ύ1ΆΎΣ΅ο —‹,6[η«μϊ},Œ‘­λτJ$sΫΟ[!0elkI‹ΣκT«4|6HXc$Ψ7CmΒσLf:j–΄Z₯[AšΈΔΖ‹NιώχςϊP{d3­U%aJπx½Ι΄ήPTœVά°΄Žγ X?^a)y ί]KοǝΐΦ4!›be*Yβυˆˆ“ΕeΘΈx«Η|{•€ ΥHMΕ Β_LΟOΩ›αιŽΒ±f€ΠYcΞ²ρο&q—ΘZ&aΘnKŒΥ}ŸΎœe +$£«Ύr…]›½Σ»Ο»Ž½6t[+uη’7Xh7a~ӌ8@Ο‘Π“€0Η;ΉΤͺΏ)= X)ώ‡’~ߞYΨlΖRΩΰNsΥλ/uxˆ―Ε«„uάμ$"»έŽ@ΥήΞEŸλΧ-I]q«yUΛDƒβ +‘κΜγ™Ϋ-°”—ΆΆL·λσ\NάD6š*`ζο1cω|k<ξξ}¨¬°—,ΐλaζΔΦ²ςTζ Oδ ObAς1rΠχΓWUYBΌ#ν•ΠAŠ.)γ3wt moA\α`lΧpsΔ„5D4'£½T§q!τέM}ͺߐ>Ώψ8¦ͺiϊGρζa\1CΈcKhΣ“½{V£F_ γ±QOί8ώμπCθxN1Ϊοώlp͏«oO vn06<=c)‘σIρ)·ύΥnέ‰yŒx–Aωwβhy―@‰Ίπv1S("nεΔAΈιMΝ₯ +β ¦pγg Αΐ}‘Θ)ΛmΔrQ‘ 6›·#>ΐr€l-Wθ[ea#ά>-ΊΫg$-σ+ŽiNΏ£9ϋ£»R0³ΞWΒ2lg‡Ρ«β‘ΐ¦.=sr_ˆB>$’@ΰΥZkqAΦΝXΙ5#N@dT€•;}~AΚ›hV0ΨΡώš2LΕ(―o€«ύ[EΖ¦4ވ’r2ΐ4—μ-Π¬Εz£96]&τ#=άφ§‰~/] Ψφ«αήkh¨κ‰ͺ»#YUlK0ςQΞEέ—:Yl^‘l0Nu+2;4oh±r«2‘Ιό£υ–έ£–K-XΏνΙαΌο°ΠlIζΉq³$¬%Έ²‘Ξ(n¬½ 8Τ΄+β-σ³ž‘#!€ΑϋΦyr“·„φ“(.,…’Iuζ•AˆΊΉΦxύΧ§ΨoΑ}0:ZˆΧ¦{ΏμHβτ—ΜEU³qŸόzKΈ\‘Ίαu―΅¬€”ͺMD +7RwPN–ωά_·4ZCi=¬ΣΧρ^4$·]Μζψ„ρ#Ε;‘Εθƚ“q>dΪ Β9‹Qτ“XΩ¨sBͺδλάδ‘ήgGŠ\ϋ+ϊ ±½παΐ•YΟ™…ξοsnΛΔ†ΌQΧ·IΨΉv‹j\c՚‰+ί +2βε_Γ!ζ£ΡηΈΨX°Ήκϋםj΄wQ™ u₯z:œ’Z{Ml¦O7ζβf±ώ{ρ^Οs,Β͍ϊœϋͺ ΰR¨„9J¦=ogŽ/‡X °‚—L9Υaέvρ7‚=δΖέέλΒN–~Z-.‘Ά™»³PΎΚ+’Σ?ΰ’³D°k=…Κ@άAςMΞZ€š8[p‹”οkωΒ°ψ‰Ρε‡ΆYδτΊ|;Λ/•Τς”}rΛξξŒ »―jˆΈŒπ•Έg‘TW–ΐ±z8wi±ρ[’·N0φΛ ’"Ο³Šα#z_ΥΥ‡}k†$^žde©’§ /dbμˆΖuΖhoχOO#d%OΎγΧβ[žΖ‚3΄μށψΆΞϋφLΪIwS”τšΉοέ&όωeχΧz*‘$-j$k;₯άΠΕPόόΪλ‰WέΆΧΚω[\›Xš2dη«‘RfdΌh \$΅κ6Ύς‹Ϋ +£)D Ž:τC;1LέΦ-£όοo¦ν₯‡«Χ[€NW >:Ανh Δλ«λqη5S€ΫΊZ}—ϊœDZξ <]5*–Ν C§=η@n$Q“8XΨŠ_ŠςŸΕ'Ή£Χ“‘qω‰ψ•ρ6~μ-eΚύ@ϋZ&εzκl˜»…ξΓxNΰ\=Ψc™ω‰˜.Ό4ζ‘mΈΦΔαO{„ύ(G΅oI:-d‘2›˜)*ΘӈGl·LΪσβύ՟FC*N•ψžϋGh‘!bΗȝ†‰Ήί?Ÿ +˜;Œ³dΦ@}σoD•l†’!Α#ab¨ιŽvΤS4i{Eό«,θά<ΣβφΥ©ΑaΨ8Φωί`­Ϋΐπΰfζ/`£}‰:iΝ’[=0΅γAg‚ŸO³.Žΐ8Ωϋΐ+YNύ¦»>&,ώ‰;ˆΉY†ΧΤ.Ž‚DηΦ₯ ½‹Ο-Τχfϋ‚LΣio‡ΦΜIΉύωΪπεP-Β6Y­¨xπ&vh₯p΅3ηeτg?NŽΎ§ζC?EžΧ†£t$Δdώ„(βH…ΌN‚pωLL°οq₯Ϋy΄0MκιAfXΤsEM¨}i+Κx₯ίa5;± +ΦsΥR7c΅5‰2§Pλ;­OΊη₯²e„½;Τ·Μ€Γδ ρϋΒ(γΙ–lγQI vœ6{'ξμœΕΕF§ „`©ΕΛF Νΐ:¦ν3ΨWX”IMQ«ο +v€τώG₯?/Y»A95cύkΞp„ύ‘υebND‹ΙΊ“ΌF±Nxaΐ8€…@Ψs6 Hέ9OΘι­—X|{ΰOμ“ΣπΆ’hKΩ›%e`Θψμςώ£Ύθζγpΰa„ ŠR+Π‘UTΛ=P-α…šΗJŽMθ]­Υ—‰ R]ΙΙd“=“y:Oϋa³ρ»'²Q*|Γs—p†X€ΎΧ€4>\ΞϊwΡ~iξΚς'LkΰϊsxPη'‡E`4[Λώ±I‰,’ŠnΤ)₯£CP[AΑ΅Π΅΅ΈΉγ3θΌ…L@\]r8οFJk8DΙQkV*œ™Ηj’‰“δΌϊ-αdLm³ΒΏ=ήΜ/“Ϊ3.ΐμςγξ5l6Α5symΧτn.‘λ\½ε Ž@Φ ³οΫ"bΐNγ +Ή^ΪΎD£veΓ‹-  ~%C·bΏ%\«€Mύ6ό87©jf-ρc­š,FΘήj"—k„J‰RqHΤΡ‰f©C1Ÿl‘9C<'Šy7.‹2bUyZ«φ›£ΥζΌ€ξΒ ενi`kŽRΗμ]f’cω;ίMqϋΙ¦·‰[.―[ΡΌο―^ Hm+f|nƒΈύ eͺΜξˆŒεβ? κ₯ώ ΗωN<ωBΚΛ–ϊ©J3„MEr© α9»€X΅υλl#„XΐΑ½ή-cUζη:cq—Xόƒš³F‰}sνiως±οΠ±Σΐ&#%šω˜νωΙ͌‰›@φ½u‘Αε 1‘΄‹±e_Ÿ"§ΥJ +©VJP˜bq3Wl•‹_I"‹T6 +0ΐ†ψb>ϋ#טΨΦ™ΖΠpϊ2πs#« bœΉͺi «)‘κ{P{­‡uφ₯=h0#ŠνxΙΨΥ€Έδ½3»―WFΧμ«3Τ“ͺΦΫ6ΑΘr&Δ +ƒΗts3c{’‰w¬\8V²rI’Ž »H#FΞΩ—£f{*<2ΫρΒ ΐ ‹&R aέ‡eηυς5ήο•—σόήΚΙ4κκΟBq);Α"½ŸΤ]Αq™d”% +ΜhέΧΪβΗ(&#`†WPκψ~ΚΦyρŽAŠΫ+  §I6’BŽU΄]ΰ‰0ωκβ2.–ˆ7QvΘ„­€I%ιφ"€]š ƒ@):rhAϋ±¨KKt~Σ$‹ΟΠdζ”9E^ƒ'Β€έ}@Ÿtσφfγp°9―½+y³‚Ϊ BαΜά΅8Ψ:χΐ=oδάvήJ\rΐi/©8Ρ½ΟY¦I /·ΗΏYνδŸl‹n΅vA»ή…N±œDh/ί5’)Χφƒ6ΟÎM ¬έX,Y䠊΅"KY•/Ϋλϋ’]Ρ 6qΈm”"7•bΟ-Χξά«ΨψuΔP»ΛB™tC€Ξ=ƒ‘d6 +‰Beμ±ΞάZUSšζυ7PΔθ5{ω ΟoΠό‚£ 6”όA@°>Œ;Ψ…₯Ti¦vα¬4i~θmΓ:ΞΚ'=ΕΝjG·μet-A†Žκp“/Ήυβ_΄[ψ-ίY<»w;Μy†ΒΧN¨θΕ@€DϊQTέxχφo‚Υϋ–„v‡R@]:rΝϊ μ†[ΔTp)bΐ’Q™°ΖEX+͜Ι`΄IqΛ!y¦,kρ“ ll¬Ϋ’l]0‘ƒΛΪ'{AmΣω‘žλζ[; c;\ςcσ’;ˆ.½ZXΕ¬- δe!ΨόΡmTΉ'ƒΒ +ΊnR4Ν7-lŒθρ Ό¦₯c]Όηgn€9εOpe­?§υ­…\δlzŸυ’oŒ<ΰV}ε°,rJŸ΄@ε΅Άfˌzg’a%Έ©ΪdTϋLΎu΄\=Ύή=2M,΄Ϋ0U3t†Aνσ‘π~tιε&ρPκ₯ΚΩΰ=πi%1±ί[ju:€6ΧΛ’θ>7ήοηφ}¬ΉE 3­πλΑγUk.x/Bž; pAΖ-6ηΩ’Γσάπm¬v‡ !~Ÿην$^•Ό”t%Isηžp9™δ·δ°ύβ ɈΏ’ΜkΦ¦t“ΐΙ?l,ςc%ΙX„Ώ&O‘Ύκ£ΆΜΦνΪΔ{g‰'|5 Οα—TDβxΈœP;H/b‡»ΙΎ3ΜΑ†6ΒΩP§ζ1ΩΊ& dΏ@TΎxVzΉ”‹λ묳_&IΏϊ0$BΰΠ_§ί“©ƒέowΊBŽ _,ͺ~¨ea’y+1M +SΫFόδ„wόw²—Ή +ͺη{€°«τqMr₯λ0&.§MZ +±ΨEZœΪ>GθλRAk’ ύEެνVuΔo²…œΰ†p]έΌΜ1―οzunΦ-žR=ΟαA7ό@œ£ϊΐˆΘ K²tϋsͺŠgZQiJ™#ΫξDΟfN~=Τζ­„-ΉZ|`Υα^›­dτL―κΊΏΏ™“xυ—Χή,ΟD©ε½DQ–·e;Y·g& +Η¦Qψ΅«•-bfΝ9€UKtYΠ©1j˜α[-κα:ˆ +hϊŽ„S…Qί’ςυ±OΓ—<(IΛ §θ°hκσ’;Mϊ0%Ζ‹±EBz™gυι*λHΩͺlŽ•$ρτj…¦ξΌ1βΞ@νXnλ¬a(ΟΉο +΅«š„¦jΌύΛΏΊΘ”φΦ{εeΞΤ—SιΠ<ŠΟe±5Ώ ΠӜ#K_ΠΙ5μ±λ‚WgΛΏ%BLμ„G8Τ³/υfˆΦvt”λQ©e5b1ΠoZGο‘pΘε0™β―kΠΧΓθ–―S|ltΛ΅s’?ΐ'©­˜…P»–O(¬ΦlT»Υ,ΉΎτ?ξ1sΐ.c^"ζnnšcΆ˜—_ Ÿ΄΅όš„­:vTKfa^hYΛαJ4π[οM·©|  ‘?₯S―—XIώ² >9°ž>ψπ:jO”]P&•T’]ͺϊ³ΕsΗbvω‘SφέρzYβΔΠ»τΥ…ΘaU΅¬.οr™Ί‰c[kύke [ΰ.¬ ’zv΄œήόuA”V‚nοt"oSήξ^ϋ^VτJcΘZj8€c‘Ο±—ϊΡψ" +‘?2`=%!―pbζ―ή?\sqνA‘-{^“ί’ή6δχ:–3$Ν1πšη<ω8|ν…Aa‘/ Λ Ο[¨Ε€τŒxαDφχψ<ͺΗΟY„iHό9€ώdTj"«4,ˆ(Ι_ [“S•ΨΓΑ· Άd’6†>ˆ3δ}Aσˆ‘RΠͺ%‘ρ@09€ΝΗο’ŸΜšζŠ©wl—ςfΤοoZ4 +ێ +Γ; ™Ή.L­ΤmɈΦ‡vÎ[<ǐˆ,vY|]7_ Bι T&λnΡB–‡Eo‰ΑBR!μ];"bνV5Q`l―­Ζ&^4IœwΤ3‹΅kχ|@£Ε2³_]ΕΫkFIƒμΜο™υΐ²€―m’νtT@«η1…μ&EžAΛ…κϋDœΛπ™…[Kb€ιΎ °|ό 5Ρ™ +kŠ/?²”)Ωnš7Έ―Qr~ε5οΎg>δ\Υ΅°ρK;Τ\Ξv&π3 ::-zΛϊY$·‡φΜwάZζDίθ6W™σΈM¬UώZ;MΣͺV^Ζ3.«e³§[mŒhλ–Δ­·W]εM5ΐ +01Uςr䂁Δ:πάψBiΔ‰]έΏNλθgβ2–Œy-… j;οşCxήΣΡ—Αφq΅ %Pπ˜ώo/ς?T)6ξαKπ³©qΗςU7:Ν(DΛήqΉgυψ³K~t‘ΨCŸΆψࡃ @υW?PHwY.”Φύ>ͺ,2ί›–ζtμSN'§’ΨΚhΘHύς©"R“’”9e$œe=H€‘š‘B‡h¬΄ΈbzžΰZ€ Ο½Όd&g{Ψ ςx$’ΫэΊΎΨ«ԏa/ψn Άͺπyθ°-ΧλX›τX?*flρΫtc?ΜΏ ςY^I!D,Pp―Oά‚IΏR§ƒDG=D—荛¦ Ή’7‰YΦFΥ"*’p?)‘L—ψ Ÿ§U`Έgdώy—ƒg.Ί™wM$ό¨ΔZTxƒEh&AoΞΞ +Ύ•Š”Κ#›vμ’”CœΠMΨ'•Ζ"œL4#F Q2‘9’ƒ7C@σ“’.‹UΖπ/₯Žόρyz8ZρΑΔ=οο*Ύ¬+xΞ2η˜—vLω’θ + ΌAΥΣψDΐ0½FΙKό8φ$‹V‰?Cfν­Λ¦hπ„ή“ΈM£xΌ§Όk!•žά%ŠoD~l3Ζφ*’«9-[¬νŽΗN\\{2@–ΌτSΪ΄v"D₯Θό›œ'δΪρΏΆϊƒf?2΅ΧϋΚUUkG­QόΌηΘΊu;΄γ―hξs’ΗKK­Ελ³ψ”Υ“b^§²Νυδw Θv°%βό–ζfΩkkΑΙ±^ΪφώŠ-[x+›ξχέE\ƒD€Oθ ΌΘξ.λ)("ώ|ΐ_₯šg|ΌμΕ<ϊœ=ώœΰ&δλ‰βαͺμoτL+ŽI /œ9έπίπς‘oj-¦ΒPېxΆ>zZλyΫω@3XΙΆKνΛ9«Φό―δ™l~;†_š«ΐˆ!΅ιœάΰ υνδΞταν§-˜έ!ΞcT+©Τ˜Y”ŒΏO4£΄ΫσˆwKdQΨ¬φDUΨ•φμΛrŸw,ΙδΏ 0†υ₯©Ÿ¦£…΄°‘Υ8Ξ,cΚ’Υ;D;C₯Τ\‡ /Η^gύ†EvRP2.=ŽδΔϋOς©dφίϋqγ€ΦkMoϊv0tΓs»qŸKy/Hπα­O1ˆ™,Υ°ΫzdΔƒη—κΏYœ•oI&εΝm‘v,jAsy>Λ—zDπ=[€NΏG™ΓΟLΪ!TύC°ζi6Ο‚:Ϊ.ReΰI‰~R€ζί’2ΔΔφΟmΖ°‡•Ζ9’k\v›œ“ΌΦΊηšΗ€°ŠΙ#;ΤkλΩ“Χ½>ιΙ“%UΗΆŸδ:ή$4sFfnٝ%Xš ‚Π•½9=(q~Βͺν’sOeεθ…²GYeQΦ`ώ=Uά„zyπυLφΤ…‘·VJňY1 ΈCΦσφV|iοƒθ©ς$“«Τ]hΕ*Λ'@i-QUĞ: ΉC &ΣΣ·j$DΫph/\φ£ώΧΗ8Eωb“”„Πλ oŸΩ]ί4ˆ}°SQzΦ!“½d‹Q5'Χy\·ςΓΏ Fΰ‹e<„Hf"Ύ]ε.χCο]λwΞi2ρρ7Λ'‘»­aSΖyΰAΐYζ½³Ω™Τλƒ}ί†ήόδζΥ†„ ΗΆ5₯~υύž›!ysŽ\Ϋ―0ζΞxΌ³δAZ4„ u8βuO$nBΑ¦ΕΤgΐλ†X"”Ρ'`:ŠRG…4P±‰γ™NDά@},₯«§†ΝόHΑΎ[‰ΑמnkP‘+Ž6wA•‰Κώ©+Σ™,ˆ\ΐiԘžJΩ,υβ D€Β A΄\δ^XRΰρn™­3FœΚ,H@Ž$ώθωSζ*ίΑΞΠ]θπ4£- ₯υse Ο”•βεA©ΰa’Ί7Κ"Λ³wΑDΞΌ·ΐ8k#αΒ>καζžΨγ·Η+Ήž°*ήb ηo8A"ίΏjΏ₯Ÿ@ψ@IHοqL9&VΆDΩdDΖ―!°ŽςΥG₯δ~lΒNΆ0x7ivΗ Z’+^έΔΒ EΤξΰrι“kpΝΪݍˆ³6ίρώ)A+‰ +D#;˜„ΰbžiŸ1ΈΩ\zfηήBυ%Žϊ2[Γέυ"‡Ynχΐ!¬‰!Z!T*έxϊΤ΅Z&ʝ, +&fxBϊ½‹ώ)˜fI­ξΘL}ζ%'α¨κ»³f¨ΝΥVNyΐχ„°­Ηy&,bιŽη."%ΕŒmme’Gμt–Ογ'ΝαGΔτKφ°tΠOۊ7ΌOŽ‚θ%KMQμ₯”pŽ£Ώ‘*-Δύ5βέt{΅ε3TΏ?κ ₯οήJ\Ζ+€3ϋ(’>~( +Н δ™paθέZ¨Σ„ ‘’njώ8 tΝfWyΕ‘Κsυz5ƒ΄δΜC Βϋ6%ͺςuυεΫυuΙ¦(± SuVικ):t<—£KλΜ’n'/³xφ£6/’UcΎΑο7o*QζȊ ’™pD`X’ΰ7•₯JP‹ν΄ΐ +­v”FčŠίΐ„§&6‰)\‘Ftuኊ—Q· ₯֚Oέtjdΰ<6ε’ξ‘”&·ζ1έ“qT‘Ρ…%~ΕXΠ‹‚©…_Ύυ/’.βo-ΕεAΉr†cϊ)M ͺ)6^―9―Ezρx₯zœ±½|BFPάVXΗοΈfύic¦l'C© ^“€.Y"w6ΣΥ>¨Ά(τΊΟγpU€RΰΗP‚cnVψΈŽτσΫ^Z 7QD‘’bΗR|{‡•κ#„p­χž£Λμkvφ孏,β”ΗVζψ‡@̐;^όΗ†ƒΫ*Βσ?sΚΖiΧυΰŽ8"p©'Ž>—<ΣbV­…κΐ«ΧxχΓnA’ΌJγ–"FμφšφpxT¬'‹_ZΣξς7¨ύΗ-φbhuŽΥ£hLeg €~ˆhιW4Ίδ©k‚φΛ¦]­/‚Βxp ‘ˆ€μά‘ω0ΉάYςW,χΦS²εM{`XΨ*Η;ζϊΫ[Θd'Y=δΐΎƒœƒZsW‹―τΗI2œCθ{ηJžΙΑ°oς`v0’Ο±P™• ωΐ7½3ΧΟ€Von‹s'΅BˆφΠ–ΦYΘ‘2₯> Y4λ)εΠ’ΦΡiCΣςΖn#s3?βRξ…—/“α£’«*A{ABΙ% +«†Ή‹IύsΰzpΔ!΄Μ½γΤ‘ζ1e% ήΥ]7ΠΟ%Π‰O?ωR«YΔημ"DΩσ\ΪΘνVϊ…ΰ%΅ΜΩ7Ά¬ήν¬½Γβθ|rΘΎJ„eΔ±°qΈv`άTgK„χq$ΛΫ‘ΥΆνϊΞQ“=τƒ++W0£ŽŒ7eힷφB6ήζξκ™TŸ_ΐχR_#΅=“)#²ΦΐŒΪ–BΦΫD7h?(σeςa±υY§ΜS³#°=•|•`₯ΐΟΏ½ΕmŠΨΐ&-L’~†’ >Δ’KΓǟΑ`&±·Η|•΅P<š}s”MΉ¨k` …fc†‘CΎΨχ‚Bϋ½ξW̐§‘Sτ!!9 <Β +7ΌΕ\θύ—,Žm:W±M αJN? +ΰΔ°MΥoΦ²ƒ†Έ}遊9̞ˆŸšΤθ’œhnŒ53}η+Bbΐ£˜H&_€Ρ•Rc••Φ?£±Y돚3d“έ<€°€/$[t|ό*£vηuΌ€π²v¦ί6EΜΪ‹q~4“ζ“ϊˆΡŠ0lφ8Θ<\γB^Ζhp·ΕAˆΩI^|RJ&mδήLC«EΠDά1―q\jΉIΪΌΊg‘(|_‘#υjψMηΔ[΄Žΰ₯Δ¦Θΰε—³Φ¬OVgψί³zβ@·BΏΊ‘DŠfͺ‡ž•C±Šb£λOHΛ#0WhCi\Θλ9ؘ۞;ι§o@KωlΙθYΌ°LΘψ°‚ΒAϋϊ;ΑqW?W4'$ϊ rbeΓω+λΨH‚“ π‹1­(υ0«­‡š—½2faήΦu'_u/=Y¬Šq‹βψΣ;qͺŠUΉΉΆ‘½ͺπŽ»ςπκΦ7vςΚjΩή?ψρ~‚ εΪ½―—#4»NˆZΜ ;s χΛ(,Z<‘›§aΧύUυ―$Αl«9@Ύ‚ΝΨ]%£d€Ÿ_ι·©FΚ…§Πvύο-b[4΅Ϊ‘Έγ,Ψ™Χi>W+­σ1λ™"ŒΘ{–ΓbŠeo΄Ϊ"œ)”•έ'ΰ3 fuœkΫΤΰί +Σρ8Π?Πoˆσώ™ΧŸXΞΓΩζNΠ˜# +qaδž?θΈwIΞΝt,F‘£!ίά\½ΐn–ϊΠRwβΨ€]IΎ?%Suή_pΪ^€$P‰―ΨiK(§Θy†\¬ΧΖΕEκJ£Α# ΣΧy¬†’€ΞlW»dΣαΌΑΠP–|‘Ν)²τψι=Is Μ_Κ'‚Ϋο7WΓ0n1τ"*Σq;C½ςΔ"PƒX[Ά•—BΓ¨vγjαz8`uώ νmύI ό ΎvŠhπRa³–½ŽΗώόΜf—cΛ΄αˆΰΊύœΪ›_‘€ +pΎ5EΈcœΉaλ3OΖbέ;`†‰Ώ[*Ψnl…Q +ί€Ί‡{/ηY>β„ΪHxσsΗΘΨMΡΐo7jΩqq +Θ+δ[|ώytyRŠΑ΅˜η&p;ŠβΦθΠς~¦M»χ‰²us'C5t2ηΔo?Δ’ωΘ0d}²* W₯4+¦ηouρ€/t!ΞΥGŸs_—™3`ϊΡJcΠ-#jͺŒBTΘο–χGΐl•HΎz―Žηœς( +ΡΩGΘ&τ„γSS"ˆΑζΔ@'a'H^ΩbρyιΕμ'7vs…wΨPאΦΰΊηGkešnjJš_’ep%o<«ΰΈ,6ΞΑθΧ93!>ΘΖ³*z”  žΑMΕΡ1+‘™€Ίτβ?Κώ'›ƒz:Έ`HλϊσΉU„Pξbε₯όr„XΝ6Ξ³6©ε6‹^ Π>k¦βPΫ’\ܟO1Υ;g9ΔyDœ‡XA2ΤΈΛΔνŠίκΑΕ”GΗŠλ΅ό|3v€‚dΉ•‚Π(κh;7­?nC£7Ι›E(Μ< +U7<^(ψlB‹tθ +ižšXBΚͺSΎ s'Tφ$]#Qpv€FΜX&eΖα hΊΑ’P+ρˆ;n―$ήΪZ2 R!ΦΧΔq·?ƒƒ‹zoΙθONSΒ  .PΰΡKΪΛ@5―›όjΨDΧ¨>{φ`0R1[Π’ςΒ£oΌZW(!6ŽˆΗαQ=’Yισ—πT3R±FάiΠFοξTωΎ9{&s>vβSΤ͚ŒτΗTž! TΖύΤςΟ–*Ρηκ{btσfaΫ°ŽAOƍo¨ŠžA Uτ‡ŒMΉ|ψ?ι +>σ“·7 Ј\ν-BZEΆθ€Ε•δ0c·o!aΗLνxλŒ)‹#ς§ΞχJΆFΚ]!Γνφα|%aύBvO]B ;*/t$€c!€«-^C]3ςδIφJΩ} ‹#uθ¦ώˆ=ž—–„ΨΊš"‰PX1kYΗ뚻>j>³%!yρΆ΄Lώ-ƒχŠ(βx’uŒrν‹/σEδ°ε±v l]έi±#Ώ«@%r|Ποφ4颍§ΰ/7ΝμΆisx:{vΏKJo‘‰.ό°>'%ΓΒΓΌU΅ ;όΘ¦Z₯ŠώΥσ +Šυ‰=t$»γC&8»Λ ~΅Iη’0O€œήHΰΖΕ]xΛό‘ͺŒX%ŒΉτ%‰ύ²’J6ύΕ½V0’25Γ©v!bPΔlIFΥζ)φrj8έη όξ_a Λ› ͺK―«Τάθ„΅λ/ Vœgψ°aΆ)b$Hu<Ž©ͺŒqΗv=°κΐɁθ "BG/SfžM1™ˆο€G-Τό– ϋC%)υτQ}bψQ²΄ώQξεt•πy¨½Gτ7u†t’ʍ‡ΣΈ΄wxΏZ€p}.Yf/l…½ξRί)υk‰KO^ϊψ#Ω€υ»Σβ5Σ`°I@“dƒ«x`1rςΧ™νI0Τ8Q‡ŒŒηΠ&Oœ§{$ +ΞK:°o@Μύ6Ε­”ή@Ξiέί3mΙXκBο4xσψڊ)Ύ˜Θ}‚~ή/::ΚΪτή|γ&ΌΔ–Bι“ ₯™”ΞΝW[<πΊ΅›bJE€b7}ΒςŸ©‹+± gΒmŒo€‰₯Ά±#I£eς+·ρ +t +k$f_ )²LψͺERUα—α7*\Έœiόξ( •₯^VΕΝF7ΝύγŸX2;υc2Ζͺgβ•v<¨―υ.μlPΌEŽΛ°YV'€ύwΤ.Xϋ B@°ΟοΤ +Σ-Ι!+| šˆ ™Ÿ +†ko ±m#yώUό €Ρ#UmL +5;ΉVΠԎš*±θZb΄­_£ΐφJ―Ζ/Θ˞nŒN.8ώςρYb5Κ¬«ωt4CΫη—쬕 ψV^±‡g™Ϊ?Όhρo6Ασ Ξδ πρά¦+ϊj_η;§>Κ΄OJ€,MD£«ξRx2\²DšΖΔτZ“]Wΐ’Ί£w­β–Έ¨‘ΓSSμ\mΘAΰUjw³ˆσL#l8³ς »–.±°¨s¦twφα9',6μν£λΕπiXαΌCΠσ{­έ%)Η•L]€ΠΉΩΜkΣύžs[w/t^)Z" ²Υၠ‘X– δςQ?Q6E£M“Γύβ 1“ηγq‚'r>ρΑ€J‚ΥœφS @ wHδŸzžt ―Σƒ»y‰ˆwΠ  +ΩΙCJEΥνz••,αœεMv ’Νΰυπτ„dΜCέκΣp+O‘¨υ±₯₯£4Α+η7„ άTό%V|ϊmSg7Ί„­Β»ή§E‡ΥfqV6πŒ·^ ΐΠ‹Θυ i[τΧΣ‡~ΝΣXGτŠγ‰εgΟWoΥ·Zj@…[36vΟͺty‰VbGκ§jˆšBBRIλ †ό <Α˜x%n‰o[ΤRŒ’ΟV߬ŸμθΎΙc•g'!Άx`Μ{—GŽ ή7η9Ώm/˜“Θ½†ύα₯γλΑ΄/XjΓτΕ€‘›εDveώόψ`°jˆΓε{S³@ΕHφbί±yˆmAθε—P7₯p„«yT a3—Œ˜^ή¬>Ž΅Bž*Vr₯GY ”±‡Ιζʎ +όΫC)νC%XtcDr³pζ”V’4eΝ7+ί'PΐrΕλ@(šΰ°$ŒεzOό~g"cχBι¬ςΜ”ρ-Γ+'ΈλΎ€¦(!nαL²…Z*”ρw)ι.œά¨uΝ₯Χ1‘ΒΜdμολφΎΐ$ϊΔΑσ*»Bέ9‰Ιζ{?ΐ> Ετ&’Ζdk/ζΚ€ >.hŽπ ιOk{o‰VLLt±·ΡΓΆΔωr)έ.Ρϋΰwm½οdύLk'ζΠδ cϊΤ PeAqo"h›εPcχ!­NΦμ'υ£Œ€' …©Ϋjε…χθ­Eƒ€7‡kjπ}pyα@yπ>ο<Ι¬+―)_nC}…v•ς1Φ°‹•θv₯ζ₯Μ]<Υ`ιΕ—ν`λ†Gμμ’έt†φuεΡ­ Fρθ)ρΙa’“ψž'ϊΥ£ι’ιΓΛΌ£Ώfβ€Π8TmgΒ0skWιΪΖςυ)ΒΗ^\€ν0l΄Υz+‚•±,€»5)ƒ5jIw}ΖτΌ¦ά·2S,I ϋˆcΥ9%0²κΛ,œ&}ϊ€GdsΙ%‚v½e‹1WΟ½Τ»rr΅Ϋ4Ώϋ‘d‰/ΆΜ&'[΄ΙZ₯{θα@+(F₯¦Ο.Ϊ ₯oWjΩ ¬ΙγΓΟ•ΰ {(»rοΏΜμ/­[Yl²άŽ4ν}<€b‚v³ιu’Αγ«v‚?y/v΅‘D!¨€‹Q]dH·ΰψ5t¬‘˜ οίPψ;sJΆΗ9T‘'.-2^ΏΙυ±Ρ“c Ι)Φ3π~Iόό\ς.sk O^ςžχCπˆ*ΖS³κΡ©(―ts_-Ε8g{ακ΅“R°ΘΘ>ΊŒΏ ΜTέužΈΥ>5NP™ς—ρ¨ŽΗΓ51`€C§7Ό¬‘ή$μJ˜·!8Ίg άΖVΒUl*<υ±ύ‡ίΎœ΅‡ϊDόgE !x0ΣLoΊlΡχ @8ψ mΣΐΒέjρωmjΐ qήθρώG»˜―ο’-Μ:£Ί±>ΑlQόγ…m―p+μl”< %Ϋθ^›ΪΨ‘Ϊ1'fδ0n !4eΑ=ω‘d9ΡlΧ‡”R?l ?A7!YTηπ2”Ϊ)>¬{±%†„~’/‰»δšΈp:Ά6ζ½π)½ŒδiQΆNθK(¦ςυ{¦3Α7 Εz]އiV]d’ΆvΎn…W&<"" Š9>ηO“(ι—5o+΅šͺye\ͺ8°Rf,­ϊ „"/tŸS)Βω(·ΧkDξ„NΏw~π―μm5 Κɝοlg#φ;ek•?ικΩtΞ8 Κ\ΖnΨƒΪ;χ…L‘ +ΧEDΪB˜ŠΒ°ΚΪVυD˜ QVx™ΫŒ;j>W‚Β +<κzqjϊΡΡJΕg$9ΓΈΡδέ(χe¦θ:„Φν:κCΨkώ$O"Θxœbm$ρaP™ίlgDϋJίkΥV/πΛΘm`Θpΰœ©* +EΔ;/Α˜†+ΏΎ,5 Πωj* +>^š¨κΛX ΜΙΎ/7ύ”gυŽK“»hͺ…₯S(4΄ίςΐο<“ζ2'Β G@IΫτΕyhΣ ς Uβ©ό±žΙmΤΓV²}ZA₯ύb†Φ“1΅Β^Δ^™Ο0Ÿ{ΤeΆ6ιΧMC1Dι σbŽγ$]"GwΤf+aYU³ΦŠ©•pδ™³:œάψ΄ˆψ$Φ<Έ—œK:φšhL\Mί*ƒΥnΣζγ8£„ΈB}ΗrκΞ,€τBXC bΉΪδŠL‹‹έd§Ζ«Ά‡ΗUΜœψΒΦ”|Ÿ©‡Β¬ q·ΆRΖΙ&vΞ($ŸΫΨw›ή\Ε?έ»6pA€"· kšΊΘ–D9±5 +Άˆζwx±±-']ˆ½†Ξ=ΗΫgΊθjfΖW"ΟO8#‹ŒEτZyΓ>~°Ψ(Ÿ)θ!ρΘΟυ «bŒ0X œ4^«¨!ͺΉB‰ mεJΆχ$wU[ΫΒ*žβδ“ςbΎΛ‘T•=—w +˜Ζƒ,IΧ€Y‡$Ί_(ζ°•cΆƒŸ š“;Υ$₯G cVζφ§ά{Šš%T1ΑΤΓΆwgη.qy΄]ς<χΖ0ΞΎΤΓωέ¦ ½/Ροα±άhsλU¬μuN³_! k· ˆώ;θ0ŠAΒ!½₯kΗ¨~η‡ΆεGKρ.rCΊΎΔΪ+ακ‡ŒˆU΅ι;γ’ RκPθwίnL¨£ΐΰ₯”έ8χʐ†2+2AtρΫασ<]€«d#DΗ-ΔR{>8rn„²Έχ`Τ%‚fuVx €ςΒ#*Yšς—Θ³#}@&Ί?žI_όΚ8LΈsΜ4½Ϊ£jYG[€~Pώ: ΅»Ι*]eo €F|umλ½ς±/{ MΜ"˚(lƒr_pΣςgf' Ÿn™ζsύƒYΡͺ±ΝΕΞ€BψΧhQ ·Δ°ί]?¨Χl'@ϋ.{ΒΥ ―χ˜¨΄Φbξ)2—?Pΰ³njš²M.eWΜΐQq]gXuλ`˜β2Ά‹/ΤλΫo‰=τΝ±΄„2H·₯˜h—J₯„ °&M{6σύ)—Πœ°x_¦f&«Ψ²Ί|Už[XββΚ7ΈWΌC%k| +Ή.FΓ€lS{vq*Sή+%˜M3“ΘͺΦ–±SΊ©ΣΌ“_Υ!Ϊ)9$ Ώ£§˜1Λc-οψ`Ž΄nΐΣ]¦ fγCφ©GίΡD‘_ν0ΜJ‚‡cžςΡ!ۏ–½CΒӐ+§“„-‰|«,€ΰLVρ`jς$iTDΞDώ΅^υΊn=QθΉ'²+e½Λ₯͊·'œštήu£ +ΜΎnh½dƒ”{/t²-GρEΎοE³oΧ6F%Vβε}ΉόIzΐ%›ƒ 4n)\6UkϊΘRI‘ +«Hkθέ”ˆΌνΪ:Φ³`ŠΦθΤΓdœ™ A]ƒ(Ntϋ²γιY-q–›«` M}Nφ;›ΣΞϋ4εmY ,i˘YY%?x$Ή‚ξ½Ξ\“Iό!/€3…_κWχǏΜ€1¬!cCŠ΅C†*ež?ζ©kΓzΰχ{ήX«Δ™σ½1MΥeC|X@ΪρΞf Ά~΅Ξ» +‡ΣuΧ}π€Ϊ/M-η…2―G’ΎΣΒ Ηp7ΰiVηšΌ§ΣΛ7n}n2:Γή3Φθ£­ΈΥή½.ΐqά½#L'Ι(s2nN˜eΫΆW £Ϋ@–Ή3Αǎμόπ±y1 Κ›‹ΊRε/Σb•·YP^—jHQ +Ύ»Σ”ͺ}8―…ώ‡§Ÿo€«ύ»N ΄―'­ξ +—΅²Oάj4#!ΐω©mxx‘ί―{ρΥχWE†¨³ΡΪξ΄E]Ψd©w7ϊxΦH]­@ ΑιΓ——ZδKuT™‘Ά£…B]:ύόPκT³%eHyέw™EC=€<‰L†NŸƒbψΘ#e•6FaJπnv…Ϊ:ί©";D)Ι4>‚€IΨΙmav8ž+k–ΕΤwE8ό½Dέ-΅Ω;ŒΨxF»'3έbΓR°i&2»ώΙ¨.ΎSHΦΚΏιŒ<<ά©}";ίΑϊm—QπŠyC2λ§}9‹–RkΓ!§RΥρ—ΛwoζΌCnw0:²h0'ΰύk,›vό%m€ΊΗΕ|ζΰZ:hωΈ<ςΆΥŠξdχ=6 ŠO-{S1Ί<}ηδFŒψΓΒn“7¦wœ€mZ€7"DcF₯dω„3kνεDjPd9β£OxAG~μ^ΈZ―Φnθ _ΈwΘv²β=$ωq,κ$τ$ +γθ[r€Οj§>hEˆϋ ­S+U݌< +ΩπtΈ'dυpCΟ€ΉΖ£ ρk˜ΐυs―a– +tόW R‰¬ΟΝd .ˆΰΚ½.rτ_ŒΑˆΘΜιQΌώΏ­ψEΈ'GΔξΞΖ²Ρ^>v­Ÿœ2¦ ψθ4$2^š₯₯ƒι ]D>’X%2’ίΰ©^¨?²3ΕΉ>°ΓXχθyŒ©!©»WΖΟ¨Ι schGΒ ˆώΤE4yΔΧBοΝ‘-ΧErI₯F{\§ά²]΅ŸλωΚ§{χ5Ž\μ’VA +Ν.4;ž*“?YjYœ¨:Κ…`t½Ÿ‰žέ― i`Œ‡”°”Kt)]'-BΔ'ΑΣ…jρ ΑΕ3l~ζΏΈΨέ{…CSτΗφwI% 4αŠdCγaC?%ώ{%Ό¬μΩςΘTΩC·eΛͺF5€žΩ‰± υΖ‰9L<ό>R·€#”Ήt,φt\Ή?-–ΣφβΎ4YAžαEΎ(sμε‰a\Χ©[‚‚ +ͺ°^Ÿ ζ,HΔK)<̐ΚͺC=Τ$–ΧX ¬*ήk€‹‚ΕάΩ‘¨i}cn§ aΉV>₯ΎEΤ,3Πφ-νŒspCXΈ‘ϊe š€§‘€~* ϊ1hGcθν©uœ˜ύIr§dΦΓΚ5!ίΫ‡ϋ‚t£ς&>˜V‚‰IΔ¬=%jd Wη›γk'ϋ}PΎm›S{ξM40Ϊ hD|Θ<¬Ÿ0l·w»£aE<(Άfχp5qPwz Η(p±ΐ0\7€fΩ°Ψσ’B7ΞΨαΓnoxπ$;J›.ΟΒςm¨ςΎ±,΅ 6³κ§ξŒες.Ωχ}Ϊ]š^9ƒ6Ο£ζ3…Ž}‰‚m+X„†κ†Ύ]ΐ¨ƒΙϊ'xFϋ-&~>ά3WψL½ς«ϋφΙa}ΧΙ‹«MŠ”Δ“gΈ\7j~ΪEe*·οΧι l'¨|tτ‘ LY»άίΟonΩ¨άa;ψή·EόFXθr«B‘{x‡Sk}Ώ69#,D,)³.NΗςSkΜo\ΛΛ‚\eΐύ§b’s$ά2ή“ΛUkΌ"έ|6[ȍε0ΆψŒ3nG]vͺ‡Ξ« Ω7.§!χ—~W‹Ξ‚ƒm>ϋf_x3[t ­Β~}&κΤε eώbλ¨(νj˜΅ΐ,«ΙYF—1ΰ«ψΨMΘΈŽ°ƒς«+xςLλŠžΛ‚86ηΫ€§-•Νkr+°{ϋ ύ ΊυHΏ^»e.A6Ψωί‰ε«[γΑX‰~ftCςdy±/ρΡωjw +F‰]@Ÿ+΅InŒεisD@PU:!Z~q€Ή\J|\Ν²X<aχY{‘™&β›Gθq°εSruή.*'n·‡'ί{DΑ‹―Π¬ν Υςμt!Μt–XWρTHšS›ΝΌρΟD‡@&» ϊψψzΛ„»Ο¨±‘&…­y-KM΄Π6‚ƒΰ;IόθΈP{‡Πλύ{ Σ,τ‡oT(Ϋ›Š‹#•<’­ΈύΙ_’‰χί΅‘4L#7Z]©Οލ_δή"ΒΩΰύε“k°swίP§ž‘ΧIWŒ›ό1άΉf~συn•2!ω]} Ÿ•f+Ψ0χh§j };·”&^Ž‹³*+’|Deοϊ\Ξ‘‰ !bͺ αΎGμ‹σ8ΣeŸ>G|‡XW—kΌ(70Φer8+φ«°3ζ§ή\…˜ςιSξD‰Υ₯Η>jˆΝτ|—ΆΘΡ’ e;ͺΏ pΎ˜4Ά ώ­AH©Cτri!vš…ΥΚ)’]ψϊWAρ8dTύžTτΊaŒΌοΞγβαAfΊΘƒ¦Ρ@ι:PlΆχ “]VίΝG αVKŸBaρ8ΉaSW›ŒΞsbE^ήΥO©œ/|`ΦτzSΥΛ`‹φS·7°Ο9Œvb}±XY’vsλ±N3ϋ0@ΦQΐϊ£72Lχά`ΓΠz2QŸ€sπΙζŠjx+}&χΟ6ΔIn6Ÿ¬R ½(œŒK'5Qp@-Π±ΔxνΘΜk fbέ>΄Ό9N"―i™ŠΐQM΄ΈκœœWβœj pλΔ¬ΠΨ€†Y3kιΰΥ潡ϋΜYέKμθcJq•Mήq‚p,”‚fviμFΪuL½½5@M,5}MAΡtŎ86N J°½‡n< +pnߎΏ½Ε¬MΟ³hχ·…Ύ(«ƒi§Ψρπ2 •ηΉRδγ^(βَ οq'7ζ:iάψ6‡>B±δ”sSΕ½<}₯π»zjͺΫƜ’’ήΓe γֈk`΅ +ρ:7 ΒxVu4ΡηBYπ5’…ΊqAυ #Vb2§_―€―‘j_ςΩͺ―2>ͺGDΕ₯πβΖ)ΧοOSupOζ< @iΕn/ψΗηγφk†a˜’™ϊ’ςz%” ύžΰf±k0I/π†θjCm¨͐ πu»ͺχ¬*JWr’ψ°ή0•…c ‡γ’ @βr{«.=ώdβ‘„¬1|ΨD½½&6ΧΗ‚•ŠžJψgWZ΅V~F^Ψ'~WΏ0?UE'@zΔοψlΒ{ύΥ»€E‹Ug嘬h‘ +Ώ°pSaΚ[ή&ΞΈά@•Π 7»}π)½±sΐTQΛ€œ°Ÿο+»»4oC uό΄ΕC³zκgΎžJd‘}` ­YB¨Ξ’ΛεΉΦq΄qγχRυq5,䊯‹fάi?QœΏM{όY:ΉΝͺ΄tΆa•§ͺF·Β|~nCP…nδa8ΑσU³`΄}m½,Ε!K΄ώ$Υ§Κ"ΆώΨά‡WΉfΓo<ΧΡd'X-Χ5Ώ}±λ»³39Ν¬Δ Νb›’γϋ†xξu,…LΕCέΔύ5kXsuIΡ†ΜΊœŸύήt>m§μνMŠ‘―qA‹GYCI€γ#βVZδrω‰›ψLƒH6.Φ—₯ω"$]N$±‰ȘΝβΧμΠ‚ ΪV'¬Τpu‡e†Vη>>A5‰rWV~³7wTΣ*uEΫ›uν΅=ΥU‘οΜζ1M@‡I‰λ£Fα σ5΅£Π=)[BϊX@,g*¬ΩΫv4–tέC5{9W·H~b£Ζφ’‹t΅Γ;30Τ6W‰―Ζ•]Η·KI2ρ>z)ξρΘ³ΊΦ ίΎvΤb“Iώ!ͺ±lΑ`#”f]όdN€Η₯n(C„ ©Zΐ՞ΏΉ³“Fuo5ΆΗ¨4™1ή3Ÿ7ΑUΣaV‘`βηΙΰ—˜τκA,Ηνǝ“ 1=κ΅{μΌFζ:²"Dt„OSΨά–00i§vTΰΣΫΗ‹'b·έLUŽ`²No؏ф_ΜWŸœj£xKΖΈ$'κ%Qδ°³°&ψAΪo +endstream +endobj +598 0 obj +<< /Filter /FlateDecode /Length1 1748 /Length2 102745 /Length3 0 /Length 102950 >> +stream +xΪ€·PέΆ5Š»KpΨΈ»»»»ϋήXp‡wΧΰξάέ άέέ!ΑνεϋΞ;ηΤΉ»―nέΏΊjVχZcΚsΜnJRuFQ ƒHΚΑή•‘•‰… `m―`mrv΅Άi8™Ψ™X()5¬]mAeR δμbν`Οχ/˜Έ3ΘΤυϊ„©λ΄’ƒ=@ΙΑΐΖ`cαcηεceύsΓΚφO ƒ3@ΜΑ  ²·Ω»"PŠ;8z9[[ZΉώΙΝπ―d3/€Š•΅­΅£#@†  β`kΛπxeG=ΰ―Sάμ η?1ν\‹?Pk{K€”3Pw°pυ0uύ m²wΉπύGi4Θδlj Pq3³΅6'’ΰaνjυ'ή¦@žζ ΗΏ +0΅”₯ώ#ΝΏ‹ω§/Σώ>=ψ€α€œ-A+WWG>fζΏRXό΅ΒδbΑdr₯ύgυ?™ΠΘΒΞπ—εψΫrώmΉώΆά[žΏ-ο_–•ε?©ϊ³ΒΚπ>ώqΤΏjωCπΏ+ω ΒΒΖΖφί–Δό§&JI{ ΈƒέŸΦΉ °²€Φζ3₯΅=σ_‘4ΌAVdρgESWgkO€>  +€ε―λ_w†:ΨΫzύdj0KΘ¨)ŠŠ§*…sπ|adeαa0²qrΈXΉY¬¬lœŸŒ¨bjύVΔςowY{ ο? +Ί9ώ³xχΘ@σ·¬iIΙΑυOS4…ό!ω/ΜΏΌ£ 'Λ·$μ°ώ©ςoΧCŒy1ύόΏ‘α_NMΫrύΛσ£ΑλξΏ0Jω?φ«_φ3Μ‹όί‘φG±nΆΆΕ!=ͺQ)S;k[―!XτΧ{@#ζΰπωΏξΙΊšώaJΤήφ_ƒcν"eν ͺX»š!ΝΤΦτuΝΏšbϋ'ƒŠƒ‹υί2ςςό—- +kσΟφ Η?’ώ°όŸ%νΝ€υRέυO LZψkΫάΝΩωΟϋζο‰ύγϋΟg λ?υ@ž s„•Esώ`›ϊΰΞΗZQΖƒ ˆΓ’``ΨNSόœ[Mο©#D]LιgŒ§ναU1ξ8Œϋώ|φΆγξ½`ιdΦΰ…_Gπ¬tWεΗ²Š +˜«ΘXχΔxA-(ΉΔu¦ƒ΅ΓœMZ¨ΨP^£CAΞ¦L:φΌlsΠ#sηqκΝX-› †ίΌ<ΏlΟΜ[&wa=Ξ>R[ν„ξ”l²FΙΫρ6_Oη;τμύήωj[%;T‰Ύšη3L<Λά²Υfͺ2±ΓuOz’΄™'ΣQφ#ΔΒεαy[dI2JzσpΧρμͺYYvͺφ—KˆL^Hm―Χ:%©cXχ(’ͺ‰ύT'KLήCAύή¬­Π@Κ½ˆλ +5κΐ>a‹3ΎΤΥβΰγΉ₯tQ^°©ι˜‹@ά€k?&*Μ4Ο~¦Ηΐ>ImΗ=ς'€F¬A‡r΄«²…νe7ιŠIxY,ΐfκμA³o•Μ±΄dX7OμΏ›όYΏ{€m%—Ν +—]9 !*₯[0ψ’!쨉σdUςχ(δzΆgπg + +,]ρ‹βŒ–’Τ΅d>¦ΜΑϋPӡːǗ¨ν«ζZ*//κ#³_-X6/E1ž'·ψί‹γΜΌ6ίQΉƒ\6œΓ‡ν΄ RFh!‹‘I7£‘.ηZYB ΎβmΟ„Π΅Μ‡Ο ΤΎšΥ*kςΫ?ΗVΓd`ͺ ©$Ψro­e«ΨιΑ¬™ώN9BΚ6;5B*Aςn–撝ΗVvͺ ώ.!οTf½œ™Ÿ0B‹Ν§Τ1Rpψ–y/By7θŒxόζΩtΗ!Ÿΰ3Ξ£$bΌ”π­²γ•DJœzJ{<•*άnGe†¦Έξšω%Λa"pJ=δ`ώ4š˜`:Nvλ7ξ·VYQ,―}$pXq”mΓ3eœWšaB€ίππwŒp%7Λφ‚ωΆιz9/I +~₯„Πλψo❆»q”x³ͺgœ_―šuΐ">”—6±ΕNK„•ιΛΏΦύ<ž“Πρ/40­²ΆΜ ‘‘κaΪΚΌΆ]XVNΑς²Wv‘:ψ4B|2ύθΊωz$ξH]»΄½S/λγΈΗ‰ͺΰzΪ5-–FGΰά€ΨΟπμޏΰEΖ° +σγςΘώΙqSσΪΧL·νέ“yo;WρYΦ[‘ΔyπΜ +οS4LEπΤ‡gBgω‹«_όϊͺΈΘΥΖελ{FΈŸ]΄¨Ό'-ωΊ„Љ…θg΅έύBTθžeœ•ΘK`pŽΪZWu‘ aΘβΌJξ·ψ1ZƒΑά#hΠlPQ%‡O&ύ%[ώ†g’V—_·ΈE‚‘τ΅…({Ι€ΐ'w Qkj5]Φ< Y?­ΐ»iΦ.€χ΅(x± +U—Ψ0zP+3Δ«Μ‹άΉΡ$±Pΰ?ΫΚ ΣOπ’‘Š~{V\t"7κρtZ’«J•c©f―‹ΎΒŒλDړǝΧιœΧ$«8RΔΕ‘ZΤF6 =ŸκΐΎο‘»Ή§&U›―¦Ύd‰ έ$j\v*Ι4ŠmΉ’ΗΕυΊόHžΌY5—’aX° ΝpcΕ&N^η|.:žίΆ(’ά‰Ω’y9&γψgΈ‹Š$ͺŽU2£ρ―ςλΐαyV΄)u*~§*­šΑχ‚βΐ Πbqm‹dQjvή0’ώφzι.\΅-Α½‹m—1ŒςXύ)OΑΝ;*ΒCν!"αœίF?Λ‰k-κ¬aΆ³σhxĐΌkBQ>›ϋΑ?%ɍkOP½<7ώEˆ–-Rš +֞š>Π—€wtνκΙ™lιΙaΐ$[>G™ˆΫ0 > Ε[΅vΑΨp―>‘ω™P‰¦/<κw’½q*•κ‘γ&š€IψνPσW™pΥ$Rρ=ί•„§kŒ6κςβΪp,½# ΨΨ£OΎ|p+γ>ιιRχ΄ύίιυ0­Ί”edbυ9JaΔYΝͺήΠ9¦ΗW•ΎΥ…0„xŸƒχ2λΞ£6B_֊Ω[$2Θr7±Σ‘‘α\?n%Tw›dόX~Uhž’Yw%ΒΘτ4ξιEPc‹ eΦ[L“@ π^Ψw#ΌΞ)HgΒφκΡlρΏΙYΰr€;ϋΈΙ>σίύDt—ϊΞ“pδΗΟ&φr­s\N‚Ψ}Uη…φ\Q₯O±³ϊώA9μΠΔR}‰@Q9Š!ν΄€S›ο‰Kτωό˜’WATςΨ5%_iύeR/oaBΉΫ3Ροa0ςž4" ƒ`[ΦM°^υğ‘;CKϋkŒWyPcyΔΠΝΐΙzΛlα΄Δq[{­‹{²Œ™=ΎžΔ·ςϋμΠ<¬r9 ‘ž>7l”9WŸ]‚Ap Rf€lvB’IάΠΐ‘“Ε“³κlБϏ¦‚₯η–.ΔyΑϊ1o΅\ψςΐ!ό ι<Ί‰JSx¬Μμ<1+‚…R+7u8φΐ·¨*Ξ›:ςK"Χ^frΎ#ή2jΜ%~¬;ΔΞΑTZ™`―χ―γ2‡σO:½ς₯Φδyo£c02―}L?(ŒΪ€eΪ‚ΰ{ˆ…‚ΚωZNο¨)>³Ζ^ίOcΔΏ΄sί ‡±k +ϋ&Ζ KΒy…wYjΘjN-•xCl[Ύύ€θvŒ_Μ\BtWΞ@ε(oΪξŜ‘ͺ‡|(»ηήι™χ|›²‘ƏmζΙΥg‰ΌΟ ΙT|Ӑ6^ 6P6H!›_1ω +‡%σRτi˜‚š+t‡ˆj(υ'ѷЁA_ξfŠ–"!κ_—aΚ²ƒhNρΜΩsΘΪϊω,3cM”H £œ=p6ξ₯fœ›vΏξAh•ρfυ9S‡h_Ύ/―/Ϋ9T1“Ό*•ΙΦr μ4‘”0Υ€^QΕν=Ηό ΝΪΥτμzέΒlσ}X…ξuφ«m‹%’œ#Σƒ †ΙT΅Έjw;ΤΝ•ΤŠ±π$ΔdΒT…ΎQS©Ώ«‡™³‘3Ι΅Ρ32Ά1Θύ λ‡Υ»κ³ Ίs&'Ί#2uΟ^‘±]—Πžš±&ΞZ½XιΟ?AΘ΄jԁϊ=0x‚UG°±Α;π‚u +˜ŽΜ«Œ›΄y•μΨ“Ζ˜Ν<;>Qί-¨ρy?΅ΠH]Xͺ‡‚ϋ΅o&ό4ν¦βΈΎaδOΘ«@…ξ8VŒρCΊ6ήb τζYΖaΥ+oP(Ÿδ΄ތς]Oλ#0εΚΛ›l¦H±;’λ™Γ‘δJŠβό%δ!Q¬ΠίξΒ4ώlχ£ΩΏ>α»νOBβEKΉάV+7*5fωt™Δ³{i—C9Ίύ6Rd˜!Ÿσ€ŸnWJΡΩƒψ"TΘ\ΌΑBΠ_ϊ¦9ο΄:ΰUξπσpM!Yΰρ0“9c]άΰSƒιΪϋΗΣόΑ^iΨH“(+ήΝΕeπ1uLS>}Ι/φqyΝ[r£Ž·­Y|ήƏŽΰΏ~b }šm—oΕΟ°”³Ι6E#’£·ΡλΕ~#%!qή¦ψx"BόΠγ}šΏ%ƒΩ ΄dw^ο B‘WŸ’Ζ½ΙΫ)h$Ύσql.ότ’;qόx¨Έ_Υ^z@2ηω…~ΰu­’/7κjKΡ}ζ|hΏyƒζfπWeΧτΪzH'˜«4ύlβ~A;S Θ/·Δξxγ#vθξ€E©^·σ¨‰uiŽ`¬b 4|ε€Χ·΅Ϊ£ΨͺψiΔΫήa₯ζ$§†Ύα8.ΌρV­ΨΉ;;s|U‰JB΅[ΓΎT…Ψ Yπ„™€CΟΰυr¬ (ή¦0V}7Ϋ‘ΑΧΒΊZ€Š3[ +Οήώ_0—Šv y k7ΌG&œΛLηΣθΞΓδ~σ”ktmμ¨ΊB«1ZΟΜρ›Ϋ}n•«ΣFlj€Θ΁φXΏ4 yZ1„Έ§hH–άέΌ΅ήŒζj<|w$ω™ΈLG νΌ τδυd‘ +f|νJ6Δ@±Χ‰ΰ4RΥ·γΊ8dρ؊(Υ ϋ{Ca¬=δy¬„Ή¬ ξ‘Oτ”ύ³ΓΘ”ΫbUE) +λJ}—dΔή—Tv2ΎΥ)q£@Ή^ωΚ\³ό=Y΄΅"‹δ.‹ Σ!.m₯κZ»πδβ7 „%ΫΣ’#o² Ο_0$βnΥ`rZΠi˜šΣϋ­-AW•ΜœΘδN"ά9n/g€51―J[Q Ρӎ&>%ž‡*U7gηSΤ=ό »SΒΘŠœšΙxκΗ΄^IΫΌΫ§A­˜ί/Ά6Š(β$5lx2Ή,χ(mΑZg#΄Ϊ±šΘ+ \ N)UE-%ΞΗnp"lHU³ιzVΝ“ψ|8 +2ΣΔ—7Ήiφμ™ι-ΖπbκΖBό¦™ΙUζΥ…φ§n(2ΣOPs!Ηp΅΄|ΩFΉœΤΘu―eέξC 3xv›sμb[Ί*ςό_š€\'Ε‚3Γ)7ΐˆςŒqwI8c’²›KŽ›7–±‚%†A‚WΌeκη̏ŸΩκAό<Ό₯Ί^Ό0"KΩ4<†Taψα+2cQ‡Ύ²οϋEiΩ0/6η,¨]©‰wsΖ·b€ztώΓιΙ€|.@”@"±#ς{ΞΘ\Y™‚AΦ»θΜnV;zΖΗυ"ςοδ­"ήWkakxΙIVιY‹>eD E!ό±†˜/fȏƒ΄4ζ˜VΏΘ;Y}Œ !q)έhδΝΦ„œ˜δ‰Ζ½"…C['N_Γο«Ex«ΓΤ§Ρ2Μ4¨›#dG²έα\N7n>ΠTχ› -ϋεNG)Ei Φθmc»)Ÿ/Ÿ~'ζΣό‚Χ¦Σ\YεΗρ.Έ)‘ms‘|Δlο,λ/?BΕIΨθ³±p‹teι’™°mL⽍£"ζƒ]Ϋ& +~޳θ,RO€'¬ξ±7Dnς5ΩΈΌϋͺ5Άr”;EΏ{Φ„%AηΤ3‰Š‘ʘlςˆŸ»34̚€οCˆ~}Βqά+1σΙ±Jί<—™ †Zφ&\π"ρd‚e\αvt νΑω‚ͺ(ήόdψ¬.io‡§zγΝώ9>4ΝΫΊΘ$­UœWZƒ#0Γτe½8 dc:\}O@́?ϊp‡μ-[άΆ¬DΫ;-zhδ@9ψάΎ’—¦ pE1“a `eΆξs3Jc#ιφΔ±~CIίtTΆτšΔ‰»ζaεμDK€_ΨΛΒΆΊ1EQxΗa t‰#Γ‡λq™/ΞΫξ{>ˆm[ϋΪ k#Σ’8$Ζ»“€ιΡ ‰ƒ< *ƒ΄ΛσΎ;{Ϋ +ΑŠy°±Ζ5ΊM5}Μ7Τλ2Λε’~η½Pώ"ΩΤvΙEΦγyΩp«΄]ϊ6”Ψ~²˜Mύ­¦6”τ¨CQUχκSέ’ȐXφw†x§¦5ϋ—‚ΩρN)2©2ξW#nwς•d1§Ύ\φΨ‘N2‡ ™ έώD &`Κϊpn˜ΆοTΓ‚fz…>\Σ>VΈϋ'N ±ΌrΙ=±–Μ7Τώ’%vΏ,Αk R5q.I'3₯δjHΧ”γJ₯ϊΓO‘“œLͺτ_ΏΝ»i« ¦4χΞ½-cΌœΩ|Ϊ–χZ»3Λ6„XŒG¬]ε΄΅P΄Α#f|ΡΘ (›ΟaccβͺŒ* +V?c ‹’»D²ks ˆ3cΘ]_«―”,³β8#‘€χ’’ρKT9ΪΠ7e”ηR’kβk…Άύm4Ε½Oˆτ·-•}κŸW’œŒ~©‚]¨±ν"FL+^υΩ&]χ€|_:ϊɝκζυQΘβ±Σ\sΠΥSTψ!RοΗΏ^Ε’³¬r;ΈΓΩ9{ύaΗ Œx{μΌw›œhΓ5:‘΄αΔ VΉ>qͺ[otœ€ΝωCλ^γ“cΘΑcfΎ(Ξ„Σΰ“*² Tqώ©(DΕθ£’ϊΌ5Γ‘Ί1’gIδδgTEυGΌfα–|‘Ψ5°θξ$r•Ϋ·x+0‘8­±2†7Xc’«ιZ5ϋήΛFωnYΏ:ΔW Φί-ΚY(Έ +X;7¦5ζwχΟΩM¬ζ–$!hΟ„αύύΪ#qM"T}_ς†Ι›ΟξΝ5ί­πž4ΦvλέΔζβV^|?8Ξΰ)OyŠ-’[πΏJ_φαZ€\ ;ZV eΨx |ٜ Ω ί“&ύ†k™χ‚–(―J‚_½½άU¨c[ƒΕ}[Δ6_ΝςŒΫ‹šΟ^¬ν0χs% ‰ΖΓAή£‚’~YΓΩ’4Ζ}Eέμ§šŽ6œ³$Ξ Ž2*f‹Gϋ§ΆΔ±‘¬ΨKa }5 5qhκG1ΌΦ6)£ά›°γΆΤ›v]Εq5™0C“ΚΔ‘Iz»›ΓcΓ“ŒϋNΨωξύ„Z~„zp{NŽ?•τ zIVu|ΩΆ|ψθ₯ E˜΅²–>ά(Wν^ο'Ηugμ»ΛοΩτ„χ{ +­‰]*cβ Ιu Ί$B_εΗωκ8ށκƒIEwΥzx?¦ωŸΔΩ@Χψ‹l‘£kB2η]βϋύΓeσBjόμ»γΣ€˜°_τ₯oƒCaΝΏ^`WA_ϊ-ΰ$ၑ^Κ…΅ž½²£{"yΊ’Φ{\zDωυnς8y8„ήEΐδ·λ*‰˜œΈ†κ_ήpVμ/‰ΊΐA ψλAK=@¦:"\νΉΠ’½IŽŽ—*ς|Βψuͺt±ΑΪΣH!FΌεμqc ε5β΅τ¬Ξ²ζ ο'XœKΗ–}” 1ζ/θ‘`DΎΞlή Χb­_VΒ0β#άΜ4*³’rz‚ΐ­Χͺδ§ήΚψ‚½ω3ΑΩkΧΡ6ΡFBH­S·κ]—γγy@}1aΙ“πΦ㗍‹ιφžBΣ@ψ1€ζφOζΉ‘φ!Bqzœm?ο’«Ύ7ž£c0ΑΉ\ *œCTF©`V}_#Δ½† +&[λB-c†š"ΐόkQΎ˜=Wά@Β$΄|G.>*dΏ)Χnκu‘±bO]ϊόsΟΕnlν2«ς£”8$›²uαV3Φ[&tΠς’¨>FςGδ8΅o +9-ΡΚπΡΌK1Γ6cv°8l,ŸίΪηoͺ܈ŒDΠ> Ÿ7@[π#,*bΎ§ά&Ί†πuež—†Ή"i'ϋI)<Žvd=§ϋnc¨†·ƒqβcЇΏΙύ~D„6ΰ©Ξ`y‚τY&λαη)jJl6νΤ†ψ­Α’½ΠΨ…ΛΘΙ9πQHr™Ώ +STVΝ³:ηΕθTLΞΎ^D]'.˜‰»~JLΖώγW±Τγg‡¬‹ΤΚ4‚γ«Υδ^ΚΕ—.Σ/ΚαυTΗ»όάoηHHτTk0"ϊ+AuΜkήΏq-μίΓ b6ΩπΓWhπΛqκ;TΧρŒYb‘^’²3ο‡Fν½7Έ .λ"3u§~^}^Œ,Q΄=\Ϊž¨~8ΣαqeLh";ς(ΪΣ7λWϊαΈ–kaΠSΊf vΗαΗγβ€99}ͺ\C»KμαςͺάΛγO‡γCπ;υtη‘η3•„.ρc_fWŸωw,ι$ο,ŒH =A_«g;ώͺι +YΡ$.MβΪ7τω„‡Ϊ­@γζ4ΖHΟ6sΨζ[ŠfΏΪ‡|1Φ.Ψ7ΐ/οlσ5“.hP ΧΑ!g"αΧ<•μ£9Ÿh/5Ζ[7ϋΒΑ€Μ΅›{ɐ7΅ιƒθΝi½_Kζf•£N +OύμΗΏΗOτ£Ÿ―ΐvƒΟ6³~ +P@u&yŠΧ +-υŒπ[dx/ŸΩ(ιu»~'VΧό¬C)΄vΨ:ͺs{Zcτνzw©1>yAΥ1$ΘΠ·γ‡;šΆvqΊπsα8‘§…‰kDαUψλ7υ€bžˆP_U­υRw#%‘†ˆ{Ύ›Χέτ(²†^ΐŎķ!ΦΣ*v―€δ3C·fͺš<©•t‹vƒGΒ%6›ΐχΒ﨑 ^s JΛu|{49ιφ―Ngd΄ωu—ΣΝΥΙ /,ΏξΨΊ4ΛΣI:-lα5r]L0|Ιs:Ϋ­δρδσsxͺ™a₯1Ω€Τ_γ«{Ež}ύϊ>RueσOn²­―TŸξωO²¦ †ΆΩ‰Τ(’o(ω₯‰;6λίΘΈΤΚ \Œΐβ +β•?τ-L +Πͺj[°ΐΟδ #’Ή03±—¨/-+„šΆ”>"Lξνw5gs+Q‰Ϋ‡x²-2 `:mξ²-Hw₯Σ+K:Όβ’“Œ`τ³Ι>GjΒΓ!ΝO u;―ƒ‰/Ÿγ€ΣήχmCFREƒΠŸk…yι‰qU :hKΝ'8+­όεG¦ώrώ+w^VW>¬S¬Ϋ‰$ΪlΌŠΨΤ”Ε•ΰρ6QIh†μŽγiΣEΡπrρƒŸGί“'­™t΄1›ήΘ^ShΒ"Υ’‚ΣΆΛ*~egρ£λIY¬₯ΘD4}ύ¦!³Ζ’ΪΉϊˆ―κN©%ύΪƒ¬pl]“T­(oCΙΣupŸΖІˆιr63­Ζ°!™‡¬¦8ΈU*vhs/vέ[BΦ“ςCΆΔ1–AΒΥ3#Α˜j€Rৌβ1ΩA…c§ Υι…sΞΤ2ψ>ή-ϋΌvˆ6Ω†β ƒƒώ"Ν2‡«™œυiY΅εOΖΦ%ŽœΤŽ•l¦Σμ»"ΚΩΩ[ωXύ©VΚ ,μ‹μΛ€a υ{Δ\FκcξQ ΌN€/Ÿτ"²²ΣΧΒ=|B˜³<²;HŒ“,Σ!Ο€Ό΅‘±Κ ΓΦ…:sxίΜΙ¨o/<ƒΎ±yBr4ƒwΡg§~ΊE=Oμ e‚„KWΫF8Sε+σ’0½χΡη‘φ"€ω ΊΥϊ½ŒAΆΔ3,ΟΚ¨et*HΣ¦$R]|ΧpQ’ί”½ΞΣcΕ 5NZiΕlσΛ:ζ5,ΑiΩΐ‡Ιj‘PmΌρ>σ͊―U€ΉΉJΙΧHuε΅Z"L]ŽzλΏ'φ5ΙD3²Hb»ΩaeΨT’p k^Ά€ϊτmΌb·A4•―έQV–ŒΈ>°Ϊ¬RŠ$fΪΐ*ε€’2ŒΝSΔΑs ΙγίTκ+”~S₯WV *ιε᳦ΈKζ΄'¬ψΜ_΄₯œ§&₯ή ΊΑ_²HΐKΜ†±…[έ5Π‹t=}Qχ3Χ£ΐvDΑΝqp0)ΈOΎ™σΧφxΖMπD:pœ3¨aΏ΄4μΚπΛYŠΛ Ν*ZεσΚTN¨ϋ݌JΫqŠpHΈτ +ή¬a‹ώ Έ¬8Y‡‹ƒε3Ά5KsΚ–x—‡gΜPσL<=ιιN,A&=˜e#Jφ@`ˆΞ™έHžœ¬?g{C„φ―OXj¬¦x3ϋΝΨr~’•Υ©eɞa³‰7ή1τu‡MA{ΦIkοόπ\ ίoοy?Ά*ešίqΔLΫυEU?λDφ• Σo•‰ξc/}r‹—±]X ήlΐJΞ_η³ΝςKš*„°CΟΦ$ΐ}눼ψVq3nΈσγX4ͺ}JvΈμ˜ͺ‚Dο5 —’mG6*Γk»M°Χαϊκΐ•'s½!Dm/π ›Žtz§ -¨h/δ‡ͺM³uW""ΚuQ&μ ¦\ΥWΆBι©ζœΫcZς^š- €BJS’‘&œΤ ŽΉ`5ϊz$ΡΙ©N4 }‚Ο‘0Κγξ3,xΛFΖA}G«Δ7εΐ–CΗQVEͺώ€‘¦εa½Ua|ύr±š_z²ηΏΌ(„³ΪgΛ<‰ψbΦ‰Αγ^υ ŠUm7ΥΥk0%†ΜUŸ?ΨsY@Π§ΚΗ6ӜJDJ‰”iΜd‘–¬™ffΧΉI^λοΖΗ.»•™ίΘ.γ‘«Β‰ E‰χνmα?l”Ρψi εΖΠΩς0<Λαξ4ΏΑsΝγ=Κle)ŸΌ€8+1Db1"ΠeζΖ-PcήNιΒό­ͺ'CΛ[ 'ύDοА£uχ~ˆγώ>‡a]9ά|\o’ο_›Ή2f3<―žllΦΗ>'’όS ―VIZ@J7Boδ«r3K¦.*ψθ“ΥpΈβD’Τα€~[-ϊσg₯#ή‘Ύι‹Ξ8|¦αΔ°Ν£ϊ鸚h 7jk5³]!00`„άέt”UΤ=NΞσD;œepΩΩ@zŽWΰΚΕS½ΡgSB8‹«XΌήQνNJ0QΠ0:υ‰(\™Φ#¬jšπΚK΄ #Yq¬κoπs‘­ž(Δέ΅ηξζ±²G0³ Ϋ¬ς“yž.DΈŽΑβ§4RL/BhόSρ£’iΗΕό{<3b—™D…bΘYO·ό¦[ΤB(ͺ†½PgΑ©ΡΪ™Ήzγiόc¨Δ.οU0§ςΆ£°ZšΆ;¦\θK’œϋβG,˜~lς~ρίζxίγ1¬ΉοΠEΛ‹§oy¦VΛ¬Nέ –ϊjtH˜wq4έ,t†‘±T£TE ηΌCˆ9ϋBΰr˜πΠ ”?&[ ΎUόy"»τΎuŒKνγρΰι"‚ΊMά~?}ΰΕΣ+Ϊrƒ - nςQ\ώ.ϋξ„Ώ¦œϊΈω"₯> +Β›„ά» »Υή eΥe©Ϋ—4―° p1Ζ$Ώ«ΗHOY‘:σHž ·=Z&‹,Qφ.θA›‘΄S6CψΪ €…ΊΦίγ ›ΒΒV\x‘F™g³ Ύu(ά m=ν›κX•Νβa‡ί΄ +ΤuΎ¦+%_H’7 +cŒGώ WMN=‡LϚέΦχ) }„ΗψŠφπ«sΤ²Ο†F-™μ,«"κΞΧ…ΙτΛ†”?u>ΫJ0υZlγΪ7^FΦzG5ΒΛΪ>μs9Ÿ?V€ΊΰίΫQupχ8|Xœ·:Ι!0Oε“»Φ +74ό΄X0μϊVn:ζε:ΝΫ±ξ‰Ž©5σkn\•Σ)±"²‹D3ς]N9xΥΤ>Q~@&p|«=ΜΘΔΑΈ,APF‡μžο‘]δFu—MKrmΛŒ‰׈ΐΧ,Ίγί}ΗCΗƒbτΎχtκω’Ω«[H7K β@›!hΌκ«Δ + MxγE‡³ΐDm=S’JO«~kωγ̚·η'–Ε"³ΧΉœίΠmΕ]R½™α³mσβrQ2YΎσ$›±η+9yr5'gγτ;Lz}—δ’αi;α^`TaX'4§εΜ.f}ή‰zVσ{UΘXPΈφ¦`ΩΙ[Fδž?v‰Z[ {Ά +w:ξ€<œN·9Άsδ‘ΏβG3œ@9L! EdΩϋΐ1š«GΖ-»ελͺτκ#ΓPΜΨpœδ…ι`fρ†αŒl;ωpcΑQ$ΉάϘέhωj‹Σ”ε€ e² σu;hnρ/¨ˆΘ£Χ%6-ž*[,ŽΜγr6Έtα]A―8 jΆ|>žRΞʟΟ–išŠfΰ•N mœθ†8š–J—YΛ!θeΗ}Πκ'EσΓ(!Χε•X-WδRQϊ₯ΑχRkς.(BhA+Λ@Λ₯5 U&F˜ίcHsk²Aη ^ΥQ…@εΒϋή›gUŠΛ³½…‘'#θΙLίι™A΄{Ύc _5€*Ιεv/©ΚX]©„­Kˆς5:±Ϊ’?Q ³sΙΔ@Ζl‚ω³l ‘'Κώψ••ότgWη(Z±[dmœ=³Έš’Φ5OΔ£­kUMI64γχw]E:bΊ½§ šΕ%š²”bλUh£¬ηš]„κ3!Ž˜“œά;i$N£U¬5^ΈοΤ„ύG›haΤo_ΑH†`ͺs,SΪ[ π=DŒ―΅Γ0Ώ—ωrVΛnj|ί‹CβB-€ί¦=5 +G5±˜'V "ΠmΥσ]Ρ­*!―PC·€Λ+ΑCŽ+ Iβ€τ­5―¨h·Š±yŒΉ©™Β§_Όcˆ»Ύ·ο…&η†ΑŠπΦΥSzΉ52O¨iηRWΒ€α{ΡIΟ —½Ωbqˆ+ψ6ΒeκΔB'ͺ.ζT{]•Q†ΈcύβŠ—TΙF >πcλ€―ύ^wΪv¬D?ΎΟ‘ΞZ¦l-W™° VCC›ŽσΆΜw>š{n=P+Α]ˆ8rRw'’Eιp[θ*Γ₯ Ύ―™‘ͺίΞ“7ψ”Ό™žoŽ{ΚD/·ί80J8Ð'ο/œ―—œΑγͺUl_ΡΑm˜z Α«3.q„Ψbυ>oJϊhώύπ*‘Ž’tρΌ)*1[΅­£h{™_[ωΜαn ϋ6‚"¬+6”Υ™υά’ξ*ΘιjŸΪ_Ÿι-­ˆl- έίΡΕEοg=¨)ΥGͺ‰Vύ¬’G/ρ•©‰t%¦Π₯BΊ›χ·Κ“4'bYt2=7>΅^ΰΨΊ–&Ήδbj‘₯Ÿ‘1Ÿ]½Τ4‚ΫΠΚ>‰G!Δ₯Wϊh¦kΖ[Ψ!7m,έ£r»κ<(q7$z­Ώ…7ΠΜ€»wOΌƒΘY›ϋύΞk#‘QόΞ7=Z ΏΙ`2œτϋ VΕ°1•3― Q΄ΘoΥRΕ}Lν;ΕΖ—ώ‰ν#ΤԟsΥώ"λk4Ζ † 0―wΞ©θz]₯ Sx’ιτYφ’?w«„ ŽιτΈ¬ϋg dRΆΣΫ–υ@Β·ΘΟ_yw +^ˆšxθ"ρ»sΉ™L˜5Z°V|΄ί;/ρ[ >¦\m^πθ<8,Ρέ+ςαΌΚεbQsYέ +Ψ¬X‘e·ΰ¨x”a ‘­%’G?T†YΞΈΌLΗΊb:Iͺ…Œ.Z%ww…>ωΧ‚]]bHoε%ΡΫo»¦.!ΓζGΕλV(TCΠυ|~ω<££‡]°Scζہvh΄―…4κ²m1ΰ6δΖ%ΉUXrg}\•Ώι+-Θ’΅ςh$~M²ϋ­±-Rweυ΄Z"~ΖJ’ε4ν[•΅ΘЍ\Ɓ%¨Εƒw†ΏAfΈ™ιšΓSa'΅žψηε8i\ͺY?Š‘_Šτ35²]ύ¬G½ktGT™=dΧαύζ*1Ε&lΡ»1ΛUΥ³§΅±Ψ`Δ,ήΠ άΜ.yŸύMO²εzš°άϊZFFyΊ>5ΥLΖ³q£Εί“ψ:Ψf—Γυ'ϊQύεMΐd‚Σww ΛΆ9§Œν7m3(Sώͺ­‚²f­J ΅2 +–P'–zΙM¦'kŠ;WQ‰rεΚ*?ΆnG‘—±#έor‘j"3Frzb³€%7LΧό‰y,eτQ/`†‹Ν/aω§-Β|Ž’‹Yϋ0,eΗn:ˆXΫΰψϋΙΦf‘Zη²]YΑhμO¬΅Θ^%ΤΎ“ύ ;WΨ‰§λr±V…h€=Σ•UYΜΞ’“\”QqIv΅Ν¬ζAᐩΪΙfl 2$£9ηΫΩόhF}8ή:"ΦX―ϋE"|~Š„’Žž ΊͺE;ΧIΞ¦5Χ©šŸ~“ΦΡtgd²ξ]|υ€ØΓ«eoK[ϊ”ˆΙš«PY:a8ρΩK²†ΗΒΨ“ψψΗ2ƒ=b·ΰ¦d*ΗΝƒ›Δ]φ„υ%ύTŠ#₯U)ωeΪγΪ<Ά­2,ίΩpŒ€βΘM:6 +δNW9ώΝ‰―nrZ +žŸŽšxB^Œ Irc£ŽmBEψ¬o₯ΠΩ6Wθp«©bΩΠ‚%ηmš°”•$²ή¨δn υmΌ&ψΉ `€gQ˜2‹’&οΨΥ§ΎSPΛ§ΝΈ€Aˆ’N‹¬½€9Z|:c@ydEωόϋZγT,$CmΏ£tβ a&gYω€©ΘΨΣ'ΨηͺΗ›{}νμ..oœ2μ ω$Η·•5Ciœ1qŒžΣζ-Ȟ#ΗِΓ;qολ4Ρa‡—¬L\S#½Ό ōF·±€‘^ςVυ©*2ΰΥαI»θ‹³“Ί±BUΐρ„<qH‹Αϋ5ζr]αΓΉˆ­₯>Μy̘»έΦυ–hΌd$VyάcωΖΘ€r&η6₯7< ΫΥD±‚ξϋ}tLVύ9;•˜Θ‘o$ϊΦtΩ.ι^ΗΣΆ¬œό΄ε5RͺφTΆΪΗB[΄ά†_zΟ/”ΗsWΌˆ%kΧ‘–dΡQΪKaκT\w ύ]„=;yςHPYΥ―ΊY"_Ζ…θGυϋ2άŽ_z›Θ;-χ=Ζ|*/qή?~Α˜Έ Υ3AD κχ’z¦…υ-<Š«ƒέ5kςψ ς°Ύ‘[I ˜ +ΜͺQό~USΙσΤe‹Œ#¦DRz~<ΑƒχΥdJΚ€_ί5Ο­NzΖ“μ#δj―ͺsYͺrw­žκ…)Θ=·ό$ νθδ;Ϊγm…'γVUΥϊˆB8ϊtFuz<πΪ©ƒ–”—–οbΔΆ˜A}·ΗTWίwFe5onoX|AΗy/c.ρ5gΐ>Δ‹^e“•:πΔgX-ΔγίΓΪΐr πM'tΦαΜ_‹Ι8MΞύ―ϋ—'F΄­^dΐ4­!«MΌŽύj‘φšΞΪk轎Έk䇀¬3lζΎn5ϊz6”¦Μ.žΚT€»ύ™°/‘έk§δ‡ΆΙ_€γwG†η’ˆ₯4ΠΦ–vήE+qqVL#EWέfžQ΄Λ@“對COί‘˜^~iΨ΄Ίž*ˆ«λν¬δB S0μ@„ρ•ΛyƒΟ›Φ©δi}kΈΆ!ϋ¦@œΣ²ŽΎ¨]θ{!Ζ Ζm7*jγό&XΧKlUΥ&ύC&wώΥ+r½ΫΡ HS“hΟ@dއ<δgοHm>Όν5‘xϋπf΅΅·‘’υ +φ₯5ΠΑPμΣξ7άΆΌΨ¬‘ΏΚ'½u…€πuŸ-Ε―.Tΰ!X₯ΪΰŸήΎB‰]π€SNλQΩx}šΨ—δxͺ,0½ΎκƒώΊ‘“ΈL΄ΟΕαEΤ@Τ<—χ«VκȞ}FLzΈ€ω§@t–/Y.Λϋ’I©Νβ/Ž/ΤnΖϊbgfks$—Ζ7*ψğ?³ε„(}ΫΣΣμtqαΒE9]β1Ϊ<ώβO;Θk-Δ<{ΫoG³„Ώ³ά`4Ί {$΅‡jυJd:y·@υσξDψ±φ³½£ˆf†B ΩƒμˆqΏTόi)«ΔυMεδθ†Χ!«Fj~1ΐΦνJͺŸx€Έ₯`,šd2έ_{aρΔΜFvTΠnbžA}w †‘&~o‹ΩV³Χ2œZ):ΪvnΏM8§lFiuΌi”Ά*ςΪ |³.ΑSM*£ς‹–Ϋͺΐλkω οDXJh ‡!·Άt΅{B )³Ιι糋I Ω‡Z4Dnλ3ΊΥ@tyο5Ζ…ς{[ε@ySlQη ’χ:ΏkDπ-nχ›Q=²‡#P,Σ>“ξybί­•‰αHl.ξYL·W°•g;b0xy₯―„³δydϋRΪ–έŒ¦ΉΕίC2‹ +Κ=ΫόΎ/Œ£:\Αξ¦–cβO*α”Θ²œΘΒtڍ)/ω‡©'%ž²Πυιvx#½9ΘCΐkξΜ /·ΣΦΤ•λ²Εbςͺ1ΞΚz5t_”ΰ@ήx+2/γΑ2ϊ)rƒ¦l6—ΥΣ΅€ %9£‰Nl“ϋΩ ίWŸΰœŽŸhέ χ<Ι2έθaΜυ|]ΪΒ={2xϊ²gQ;;σ)₯ne΅™K†P ’‹fύτ‘ λ;KΕ¦ @W«ΖΕΨBlŽ ¨6”ŸDnxΐvWwo£ε’7)uNοΞ‰ Eάe―Ά½π}eMš?± NLB>%™Ιΐ3xαΨ‡B?σgx‚vvs”υέ#δC 2ΒhγΐέρεΔyΰ}Ι£Y&έψΧ?bΩ<ΌNΗ¦Λ»ΝΏ|τbΕ6X!ηΚΉT#ά¦:˜nΕuΖ5^–6υ₯%ψNXu΄C0Ρ©ΞΠ&ΞκΎ"Ix‘Ζθαφ ;Ί½«Y’Π…VCςΫΜΞH0ω#£SΤθl%‚ uUp8±‹!ΣΗμωΜ”$«c'{‡5~”YPδ€έE—š!:W½`8ψ†Ε=mΕΠ.½Αyξ@/XčΤΦξ~ΛUVŸΕ­HH}WΩuL³=ϊ:WY=„¦ξ) W5ιfΩ%SŊ+ωb£=–Ήœ|Η& ƒ‚I,’)υ›!\n£&κ°ωΆ{Πψ+cSTʐσνΚ#έ[`S—―ήΜD:~­u£\T·FC˜jG#i³Ω€!φ½ Ι|"Ο/sΪ ^"Ζ‰<ΆέΛΝ"NZ0΅Οr½a½ύ;ΤΥ-09Α—θE'”2›ώΟϋ*ͺ‘ΕΎ-beƒ29δY8{r«ΝΚ$AΨ=ζ#ŒυΏΏCΎXΕι3s΅άΈ«6Τβτφ·OΥb’Άψ - MˆrΚΟGΥ―\ΐ£QžP‡ΧŸσςŒυJ\¬ΤBΗ+ζςGo›T#5―79qš(3ΥCθ+J—hζtmLξPl¬ΪWΨώΓΉΘ†Λ―%Ο‹“ͺ…ΰ- °“oφ+sΙn8ώσΏ£\Ηekr<1%« ς0!ε ˜βεΦ`7}wά~°₯Φrσ8ۜ>ψ¦Ύέ‰-ͺ¬Ζ³Υρ4Ύs/[ΣΑ‘ΤD\υoϋά’"xφT‹’T!Zx*Ν&¬πZύΤJόFc9ΝvnˆΔLΩwyBߘšœ]ΠaHΝ*φςCŠΚν΅4\s²#=ΝL{ κt˜ΑwιJωΔαTz‹E#»prωύyλη`μMeΞτ_ˆδ ™- πZΑΨ‘}ΓγcœŠ4,·Ηmά5UήΙ}Ίwv]WΉ¨Ε.ΖΈ&/·Lαi™A~5΄uzρ–ν³Ξα ΐEΈ0Τ,Nχ•$s:«ivͺ‡"~‹½Ϋϊ[ΊϋɈκDΜ0oy˜’r‹z‡ΐY’{N•ͺς`ouΖJ’U4»˜Έ-κ―8Κ‡ίη΄ΉΘpƒO Š|vψšη‰Εμ7™$«¨ν0³½€TΈίŸΚ8Š=Vg-΄(˜|ΪΎc“ΣΙ>Άελa˜$εW7qi0²@. xŒθȐF™ςRωj=ζQ6ΏαbΣ0αa¨3{B›΄aͺH6fxφĐɒΉΑ"A1Sί²2“βφήήiA™PKΗΪ?ΣΊμ»»ψE‘DβDZŸ‰r‘οΙλ4±Χ©k΅4Κ‘JipQ³`ωϊξ Vηω²3]ΰΦDc­Α%Ρ<«–i y! +B.ψ5Y²ŠrΠα-c X–σͺ °†Ύ¬ϊ¨₯σxΉuΊq[r–τha"­'pš«ΎΥ +«%AqŽξ^‘λv―ΝΊ±xάΡ8QkAI`P΅σDƒ΄±-εqκ,4 ƒƒ…V,Έp.ι~*π#F$`}MRρ Ιlω??¨‘ΛΛ[y«t=cBaa΄E|!Pβ­ΐέh’+@τ•δ'3ϐGΩ+>`ΘΞ₯‚%Œc‘b΅^Œ³²vmςwsω]Ήμ)-σ›‘?Ϋ q9Γ8ςŽΡάjm$>τ{˜ƒGΫ£Ύ΅5¬p,β%:Y?”έ9–zρΊg—^/ΔJ0>ξ ˆNΤ€ρœžπ˜ΛΣPΦ°/KΝpX«ΑK―ρ°ΘΗ—JŽ;ηdέD‘έ4}ΊΠ8Œ_ΞGνdΕ<ηϊˆΔV ©‰ ƒΉ*ΓD'ZΝΙ²0YώLS˜"±IΆoΚ"x€7Ψςΐάbd„ϋσ€f +«o`zŠbWχ˜W/AΛδ¦AαΙͺ/m8dγπk”ΟΙέώ &–eŽθύφΈ1,FRν½KύF\!|Mϊ+(ΑiEX0Όώb¨‡eτ‚2ΌR’ΈA—š,ΝVbD‚ήϋ+‰JΎ9ƒy‘ϋ4buX¦Α—χ=ΗψΥUΘEΤ΅SΟ_ΫRh§χχ“ZY•˜N†bύ8…‘,νΤΥފšϋγΣ†©(wξDϊno†ζΠl =κ"μV‹ΰ₯MΙυ/I‚J%2ό:+θb­<7»›3ΘDΑRάt1„ j) ξ#A=(­{u†už¬œŸε@)Δ¦gN'υƒ}GDŽ–ΝW±A[E$–β¦FήκaKe,­ Ά]_θCAUε©Ϋ<„˜;X7+jhΰΎ$Τέz‹![ΊŸ2"O{ρlΑy=‹αoη™―›’¦ +ϊw„…πκ_κYKwύ1jΊδΡό’(νήγ²]€6TqqZͺΛ6„X…rπy>"i7H°θ΄sΠxAJ,μ'Qrς†ΐξqΰΗΛ]ξ₯‰3₯οKXffK΅7R«_7ßΠΘΎhA©snσXqΝΏ.ps(2e‘Ξ'§ϊb @w]² Ύ€”} +¬‰,t_’:‹ƒή ™ ά:ͺΝ‡ΣPψ +[ Όά=ςΟΉ–Δ₯ΌΊΌΖ&, sxήβ¨ΒՎڿ‘6“#YHα8 ’Ίηό{MV‘ `˜1ό£ƒŽΗ>‘lM†oKˆ¦Ώξ«ΖΏΌώX«¨§mDΨΠ±ΎΧ +ΘEϋCV]ΰVQ* ’#߁z>ƒΡ– tC²ΰŸQ^ϊZΕΗϊxΣU-Žjw­άOMΊhfΦ5­{ၠγΩMς+©`\jκl° +cpMΫ +a­’ΗζǜΟhΣQΙ):¨Α’ƒ™e;½κŽ…ιz ~έ%XοH·˜ ΌδV6ž!οΙ±όuϊžgτ”WX²fζ¨ύδDν8ΞQό‹Ω…Χζs'T lΫl“;3‡;ώA@hžτyR©p±Ε#δΤύ8’*žΦE·€‘Zo•<Μ―G\Nρ|υ4gΊ‡hνΈŒ½ή­ZO‹Šƒ‰Kψ―!Ψσh…2Rz[EŸ:ΥςK¬qσU‘0·wΌςŸ6yͺθΪ_MGyeς@”Μ•œjξ Ϋ;Ύš:ΏΤߐλ %d5@­E ρ’ΖCΚιC ΐ‘ + u™yΆ4Ίϋ+ΫλE%πm~šΤ₯¦―νϊ]ΪD4[X&%>ίΒ‰e―X[ρoΛ0Ξ”Τe¦υ!«/ ψGΧ΅@`Ά τ1CΦ’v™Y‘υ1=Ηu—‡ΈΉŽ—c>―f™΄€h5 · g黍ρšΑtN’F”έij¬pγ:Η·–Y]i›>Š|Cτόε]ς+…Ÿ{›_jςΪ Ÿ TŽ +i\Ώ=ιθ)―Aδ +lFϋΣx{Β7ΰV—‘%]ς·hέ¨χš~ΕιΑ•©Ξk}EΤI…Η]Κ·\΅^` 8μΤrI_@Š;RΘ$F’gOήώ1†ΠnzΛέ ‰?«²(Bέ9 Ζ‘)ΕTτ7oΣ©ϊ>RΗA«δ ͺιζ˜7\§YΛΛν!ίfΘψ?΅½pMc2ή?sύK}›Υ}^±θ±.$3 +™κq>#pοgˆ{δUζλZα"Ώ‰˜HνY_UwŸ—”¨Eαs&i/„,‘ό–/+φΗΩλ5wεVδ§K|Š[Ψ‰ƒ`Φ™žJ‘Q ο£ͺδ*ι›ύN€r9η%–‰‰’86Ο$ÞΫΗ—oξ0<ρ{χ₯i~«)l˜Ψ†Ζ|“κ.5Εσ…e6ΙΉ’¦θΟOiςν»άKή4&ΏiP N·›₯ύ₯³x›<ίη€J£)ή6 +‘­mUΚϊΰVΞψvΟΌ$rqρ°νH€šjον(Φ­2ŽL:μ― ώAHγ’ώ0A\}8>A-tx -e=FO'ΏΑfnŸ>Ιfš"‡ Nδ ‘=―\G΅•wι²­yσΈύϋ|Πm|Τ›8σΦΖh#΄Α]žΝ’@?~‚fΜkξ%(ykΖH‘ŒΤΉήštLbYΔΈιcρ3ΞKΜΚ%§ }XέΤί.NΓΦ΄‰ϊV05\Ώ ¦ιOι>š|λ9‹©€έhώ<7ARijΟUΧgh(ηΎ¨s­ΐ\³ˆ™ KRΖ“5А„BRΦΌWψ›Ύ€u ξ€Κ]‰ΔY >Ό1έ9δΙωDΎΐζσ«3:AaMΒΆš»Βj +!ψ$±H’=Bήv©3—•Σι5ρ"ψζPΫμXΏ]G (‹‚ΞFωBΣμ}UP}=FΰΟ1!ˆΏ‚€=ί4αT^μh©|ΒΊ•,“hω3€z&ΰΦΏ―4 3ΛeDŸx~{ΆΑL#„γΎΣ΅Θ²=Šυxo’18Ε4y°$YΩ^pμ™Hcκl8lIŠ‘_ N“ΊP–UΪŠŸΔΞΰ]όΐf»·±"ώCωσ3PΝΓΤ74 ΕξpαŠηδq’F5ΔyR_τ°„VΔrœ^ξ0ΐα·t#ΖΎ†³Υ­—ΧξxΊyU`˜³Ν¨ν+ ¨Rp–iΉYGe?²WΕΜ―υ«Γ +a„—z*‰ΗΛρ1/žd + ž¦Δ»“’[Y€WΟέz “E~OͺššΔ[5S“.άHœΥωιHεƒ56bQš—χ³ϊɊ6NK c ˆ[Βυ•%ς. ³ƒ: K―žš@‹΄ψ2(ξ2³YΩΎόΔl¬‡›g©TΘ™΅Ω6/kοS#"κ$5`ξιΥΧΖHjXiΈaώy<;@E˜κšd&³± <8]‚­nDΏ£Gh{YΕΞs+Π±Ά{‘)Λ)[‡™­¬T‡y7”lΎ{~<ς₯‹Cšx!buK §4Ωμ{Ζh4Π–CkoyE‘ολ†U”ϋwΘΛΤ[}€—ƒΪ/§dΛšβ^9α#.rΆHzΛ±π³Lj­xncMΌ38v$s>?9‚₯*ήθ―I­ύ?ŠsaέC½œ/kιμ?έP’«‘&½Χ+φ ,_Ijί½Φξua\?Ρ)ύEŸΡΌΝ€;Oʏš'Iφ]s΄ε±6iouƒInun…hΝ0U±‘‘χ—j@ϊ)Χ[nκέEZη<#ϋ{Q ΐͺy'F_±K…Z¦λ™ΐI2t²³…w>²Έ α­6e–ώόΆ&sΞqά‘&ΧΔ¬ΛόX’Fκ³L°Γ Ά’¬––&ΈήaΙ±—*Ή|ŒT6ίέͺπΆυϋέθΖδςΩ¬4Sx «‡T%ΙκΜΊη9wΤ άΩ€‚zCζz .2EΫ^ΙΈ ₯s™|4‘ψ't“Fζ}WGIH“όσ`΄ώBC7hκWJ.<Ώ—/‚Ϋ‹m˜rWΪ6–hϋΓPOpKhΟ<›kΊ‰`Σ ϊydΥΓhητγaN’ £$DηΩ3,θWŽxdβ;'τς6Iqe3#p’8JE­ΗΧͺ+ӟΰD(”BΟ‡T{{ϋ§MDBrŽ}kt€ΟŸΖΌΘ8,+ύI‹ξ—–V«Φnuθ*˜μ±Α–³€w>ρNšœ‹μχδk-ƒΚ„ƒΓνͺΐΩ#.s6zv•Nω /WbκDZΠ5: 0š T’6Ρ)ΡίA:πh4#DΏε+Ϋ₯ΌBθΧϊVΪk#η$K +νŽ4τž¬K±Γ~KSΘk>ηΡ•‘‹A2ͺ6l. JE°ν±a7*rξ AkΙέ<Ύ +>Ύ…\°‹a/£5ΙxΪΛ_#fδ$δŽ`˜M _ΫμΚ!™βϊaϋ1\I”kφ:ωΘΟτ_Rξ9Ί16χ!‡šΪΕ±-'+Λ6Q^ܘX=<&)λ©#TόΘz 5ρηΐρ?jίΚό¨«SM~‘#έ»L z΅~Ή R΄η•9 τ@{ψχ& +NJBζc~Χ/Υχ5φώgΚ:2ωRά|-Ω{ gmBfVΐTγ±+±ήοbΆΙ$¬eyŽ­ρŠε™ήQ†ΥOxx0›,¨'˜WώΊN™MFνκυtΛN*%)ΙΐJp–tK`ƒαΕ ¨ŽβŠψ(«eΡΟΒ„Υ„ΆΔ)?Љ:Ϋ½ηψΊ΄PRΎvme¦½όΗI0€ΎYΊš’ΑμŠp!fΝ³ΗQIΡΑOθΠχγ{[τ,Ρ?|=ή!Ω °vθΞPΤΕέE«.Τ’εω“ ε²&@€8ΆK…žμ³k—s†χΝ j’\¬ι†AU"πΪPUζ`σzω&ΖΟo +ςaZ’7Όϊ™ίzΈ[ΊνΨJΠχX°CZ4½Ουnšp†36”?ƒ\›ψj]I³δ•BdqH5’Ÿc1Β”I*yέυ’E–Ξ4΅ί³χERϋ†Μ\VώΗ*Βφ—λΤwr†gz˜QqΆVΧζ’( Έcω8«s{’p·σVXα ΙΏΈΥ|Da»ΚL†pι%ΣΑι;Ψ―ΨΏhͺ/±¨%ό% 3ς₯gζ‡11;F£‹ŒT‘/^αOΨOb)[Π'ώΕΨGΞydΔδ“3Νογ+Τ@Pζς:‰]c‡½FφΆžΝΦGΌ5@ΩΜ#ΪΩϋϊσ*·~Ÿl(7V’u]ΒΈiZEŒ9•kΓsb#‰—Ύ Z‘>‘΄μ–Fb^£ΜnK83%ΗΏ3Α`-~˜Δ†”ΘX/Ηd‹Ό°Ÿϊ+#’―ΜθίZZŒZA‰CrυβΥWθ‘Εΐd‘C«2 OαΧ]"§šR$ΖdΓ0žξ4›Œ¦€[ ”žόφ }› h΅ε]_γ7ΩΙ J^_ŠˆΘ(qL;4TƒοY=LΉφΌuFΌϊ• ͺ­Žφ\›γΛTΔOk.»ΡAΈσvγq$ονr·β-ke6Ύ$J£ [V(Ε2ΊQκqΥ€Κ΄¦g%©βͺZZ>j$SΨKš]ο%Τ­‘‘p9YJ&Si VΣNCP½εΐ%ξΪG6CΝB”Ϋvζ0,σ–y‘θ#ύq;jnωD"y½»3@tΏPižx“fLμG#₯]Ζ’ Τ![ΰΗ+ζFϊJ έΚ^œŽΘ(³§ΚTΎXά¨sχ6uΪΑυUπΦο‡θάάgΌθΗ΄Λξ6Ίe…Η»Z]ξHπ`—‰ΝLΡ-·”Π&8«{Οην|ΆH=^¨`@vf(ΦΆΧdh7Ρ¦ώ4ώσ·π,GΰJ ΛΏν`Šα8ηόW[δ‘Λ'O―Ω.χΞ“ŠΥš―1αͺ³Y7f0γρ’•€KΆGŒ-RηPNSjόb tΉ‚A9°ϊ£ Ψ ΕΎόνoDΥ)— θ»ςŠU^ͺ£]OΫβσέΨψΉΖhC˜ΨŠΫ«DŸq-!#₯αμ#υβήΜΕ“°ρΊ2œZp^aή‘ΦX΄dΎ2χ>nΎ“Π`©Ÿ]9 <]4*m>Φn–Κ-Š?{PΙ<4ƒHφ3‹Οό6΄#™•Ο ~XψU +ΙA₯H‘Wmy•©’Q1H:8‡ ύ^³Ξ’έ +™±? ]Η„ ’E ›’|EDe:κ—ό±ΠΘ+~(™GkηήCPhlUCTuΈLE²ΰ«oΏ$τG<˜σΌΘk;πΌ/?˜g~£:Ψ›"|L]οAsΚC˜Rf˜ Vš™F ιΊ°ίEΧvζzlάjF3(9§θWε'p΅€*ϋ}”ά5^g` x/cωΡ³ςΟOZ©;kŸ’νΐ΄SJ»œΙΟC?λH ‹›―ώΑΏ<ΝβK$aͺЦŸŽG –Ο±’-Φt7€€Γi‘%·σΠU”^Ό3s˜‹Τ# ‰χN)|=W…έΎšZψHΡeh_w¬ώyπI{ΌB+W£nž,sZς "ΓΖΫJpqΑj$E„”6U―δZλτώωΒPΔ}@žΜά―Q-Ε§δ/i΅ze"]Δ††+K@ςj*0?{$~9mcΈ”3υφΛLN5ήTψ[;ν₯&ƒf΅[\εuC\©6ŠΤ7ufΏΓ άt&υBBάψCŠΆ}κQcεψυžι8΅ηyΜΤV/!ύψάΔt±_»)Έ«Β]Οt΄ηͺb?^Š ξ-‘h¬Έ_9Μ{ 1(›„€]¬Aw›UΠχΑ/Sz·ρθmM«IFΙΧ‘Α ϋ Y!•^ιuu{ΐςΘτžRaΎ―ͺ‘r›N›”¨γρœ[,ϊ!Nφ¨uϊbۜ=‡€²Ϊ‚“bf(Tœ"Bua™=Χ6.5NΙ’>hλΘ=qψ+„ƒOυDŸ¦‡`™”μΊށ$d>φΊ ‰“Γ;!qώs ίΜΧ¨Ξ œnˆγ$oϊΎriƒΓΎ'enn\φVc&;ΗCv)&έ+μn„ΟYφˆ&› Lλ%ΣΛΣ@»žl²ωtXxfhΗοΆ,όΌπn©¦χ†%+Σόΰž–+Αž8kTΐ’²½τr†*Ϋ=E’βN 'φΑ5Δ*U Cΐ‚Xk2©ζ>O¨HŠ{ΰŸΗΦ θψU©ŸŽ‚Fo§…'²" +c€+έ'…ξ¦ψU›jwΩζ:δώ0S/’Κk3Λ,JΤ/0φQΘM˜x?žιιΦA0r½ϊΥϊ¬Όΰξ°ΐ!ΆΕq92“.€›€&bΆJG›ϊχΔΰ'ήIήΜ·, Π-Šƒeš&<ΒΉε7Ε¦mq!«ΜΣΜVw‡€p'>ršή<咝i—Τ±ŸƒsM _¬f‹›Δ[ŸΈ]Γ"‹*·›i½z9†8‰ύe ― +€…Ζi<΅€ερ==>”ΉcBj>ΏBLΡ…βG‘έ9vFζΒeάΔφ`͟‹(ΕιvŒK}ΐΔιqIQC·Cƒhηλθ–σY>iωχ8>ΙΠd4I„ϋξ$Θ…S‘οόNΟ©΄ά΅OΙΈ/ ε^ˆΘδ‘@ΗIƒ³ΐ+ηΒε%΅©H υϊ€π Ω.–€υΖά 1°QV0fοΟ"ηEΗΌoq›ώά­]N(•ΰνMΰfxyJ|[η‡7ΚΘ{Γ™?‡q νI σ菸4 όF„δJ/€G£΄U~9Υ’KsؐxΡ€IxΈΚYw (%π‘χˆzθ¦Ε½aΧ@±M΄νΐsiΦsd’]DtφDΨΦTCK9δύ₯&Ξ`$r˜h3j«Έv™K»?k ‹.'²’T^†ρ½n_7#›WΡ™ΜB?dm…MΰΎΉ`}.”sB8uύ_ΎΛͺVαeΔ—―„Ϊq]bΤy]¨@ΥΠJΧί‡2ΠΖ_υ’Œκ{78Αzo+]Ο%8Ӂ;™c±L}χδΙ!u¦!ΐ3ΐm€(Μˆo'DB —Η:A&Ή•άkύΌVΙΗF‡δ­ΰΗ-!mΝ’Ώˆ§8“œΣoœέtΜΒΓΗY+ΛΛxQ«œ£ΜL>`ωV^¨ΉγΫhWwΝ‡«˜₯}3χ ΪΩ#gAfΜιΤ/ϊIB%gG{ζX½νηۊ2­Υ—jt'„}Ω1@‚$)^ƒœΣ%ΜΏ =§xΔ]™ΆΆΧΚE3Λ`Ίψ’ŠΈ@‹Ζxο9ͺΗο’Ρψ³c# Μ%Wύ9·(Ξ7‰ψ“žσPu%γψ4œοv7k‘gυ‡8­C_Z5]  +\„ΝΚA‘8ν „οί˜kζυss]c€s ίWΨόŠoŸ2έyJK…α˜.w²ωΌfμμΧηέjoΉΰΉ< Ά§ π{²‡T‘ύχ•Ι~ί”GΗή½Θ―„mΌnzΗ–Ώ‚©ω·Σρ°π€H*Fs”ΎIe9Ωy”2œJ5‚bΗ―A’›z‘μ:A˜ΠΏBGhσλm!Ξ.#P&b’#Ω–Ry,ΡbGM[©,Κ|#—;HΔ²Όw;:T€ϊHKJ[ΘΆ7cσ™-Μ_ +_Dβ!Uλ.Ζg€!E₯ρs66ήpΧλZˆΫ"ΚdιYw|n8Λ+±;¬[\λf± +5"ψ°ΤΊlΝτ ΨϋAy«½ΓΑΥ'S +΄ΕΏ*wE§ŠfΈ†hu>$U»;ΣqMψ„tTα`F&αΆΞόέΰ=ΕδΠL2I‚ψz‘D(+Ί¬Ξ0Ž―jΛ~ S#kš@σ v%cέ$φ[υκΗΓζMσk­ή­6½9€©¬ζτ|¦$€›t—€‹Ο{]vSf₯t΅±i[άtB%h‹ήΘ[K@ÍΑ?νήZ4""‚χΞ#.ΐku.εOqWڍ Z†ύ•ώƒ”J1 φ<’Θ΅IIπc9κm{‘)¨qόΒγθy‘”δ£5ΐ&˜ρNΩ\P7p^t(ΜαAΜ^9N7£Ζ5W1%Έ΄ίoIβνWΩό«ΏΝېݭ,(€θ%ΣQΗS4οΪΡ3BΜ[L§ν‹xΜ.t|{£ΰΊŠΔ/"lε +ΓPΚσ©+₯£Ί00p«ΰγά'Z.ž`/BƒΖC1κŸα±ΤρΑyΒρΙvRc”&AHξrέΗ@*ζ!±>ζ§ή˜`nόσ¦‡V―εξρnΡξ'ΙΥAYRΪԍZ|oσiΏ‰M©BΘ$ΧΜ¦ ›»)Α/—ŽZi]zX°b‰ρ’ 4Όω!;bRr~ί?RR ε΅»”Μ³khόο7Ώz7Αέΰεήί ΅MΜψζΣρ Δvζ„ΗρηPΌ‹±ο] R7-Θ±Ύ§ zai›₯Θύλg‘šyχ}GZ}•εΪ=•‚Ή%ψ iāS‘a£šN( ―)BDάΜφ„q{!S* +hΔπ Ĝ{θ:o•§Šj>«»Ά)Н6Ϊ±œ,υΩψ?M^TσmΦ: X’;ζΐ-z³[Sτυh6’n%ήΞζJ ‚—ΘύFό΅΅'hZH—Φ6η]—Ξ:΅θψn κ)f tςΜν“ΛͺΎ!R"φεΚ/f‘41 +žLϋj~%΄!kΔ^ +/πy'nž0NoVύͺ&αqQΟηš„p€*εhΰœ#01ΩίAA` m@0mŸ;υž'――Ο-Oω+υy΄»69»«Αγύ»τΨ7 +ΖκxΘE° ‹(jw +λή‰.|£‰2‰τυBΎΡ& ΚUψ8νœ_΄gΓeε<ΤoΞ(Θς‡δ ρKDζϊΤ–R›ΑMάvΚmγ»9)Αuj‘ŒΉΌπCι?Ρ¬ΡΥ³ινιΡΘ,UψXθŸ.«-χVφUϊ³ΛέrάRϊT³ΑQMqΈκaίP0Ύβ:2γsV4:¦©χ~D”@’»Πh―Q‹δ”RΉ­»ξ‡ΗΓ’τδ©‘ˆφΉSξ―~k’»β™«‹šρ, UΤlˆHςξ_“3η7&ψ†2πnH©ΉaψO+vΖ<όοΨa£τ yO±RΉŠhšά‹φeδιΛ~?Β2 ο7Hh·5΅Πm©αχϊzΚbπ +šή¨C‘Έ.‹λvΕή-¬ε«ϊΪΓͺ²:°ρaž)Jp@ωwΆ:₯‘τ¨ωΓD?_`‹―eDΙ{γύΫΡq₯qš›"φ–ž}GΊJcά§hJFΕΎ»+ί†©3ΣO>GΛψΪΧ¦ΓΚΣf;φΆ|)=±S8ωΤπ₯ͺσΪeΦ"ŠrhπΨ°αωs[αψωl¨³*%ϋύΩ*«λ΅C.ŒΎΛΚΠ„ΘΕ {ZTu^u‹φ{6,° +¨ƒΔ­…:’'l»@pφΩr‰yΚφ=DΨ2γ•ΗkvΥΎTy –¬‰ f”.dvu8UξφlχgΩb>ΐΓ΄8θ†xψf޲!ν!Ω+'°U„Ώοώ2χX”ϊ…F;ξΓWΗ”\_ΟθJ\–’ΥΩΜ$8³·ύγš:w˜=EξΰYΡuoέ4cgΤCX<2&„{ΌΎ7>Lβ%ν₯1όiρΣ™3“D3sΦΥ=³yό―q’IΔ.Q-­t|c#|]%j·`0τΫ†A <»₯άτŘδd&±¦ Ή“ξΫω;‹Sžv©\ǜ·_ξՌ{²wΆλ«°`³suρ jή1ιό¨ό² ŽŒ8Uiίdϋ‡‰Bσ±Ιm±x±qˆNZκΑh™ΤbΛD^<\g*g?γVρΤ,σdES1^τ Αq5 zFŒC)‘€0“9[S‡r†J)Z΅RΈτ§\*#Ψχ₯k ͺhρP’°ν³6ΪΞ=ΤΨ8–Taξ¬9tΖς'ΰ@£€šΡΣ?βωεI₯…V²μ[ε’2Δά«έΊrωkθμˆΚA°ϊ§V‚Θ>(ΙCPςΛB{D&<μ…fjς‹Gύ―Hτ%P²)y΄ lΤό$ϋKp‡$}‰Ψ¨μΰͺέj‡Ai`§ιΣθw|άσNSυχέ­‹³&„U½gΑeT΅έ?χB^䊟K}™ΜέŽM žCƒ™aΓo +ϊ±Χyτ§NFt§)ˆeΑΜVΐ(6`φ)эHp0šσ‡ΡUςΙ¨8DW8NRSPFΙΏώLtν@--7ύXΔ€aΆϋ’fΪa7άJaΦ]J~σγλC‹ ¬;Τ°Ρζέ3³Bh«­’sD~kφά; έ&Λ$ήCY.F‡°Ο0ώ–‘Υ°g©h– +σΊ/+ŒΫζκ΄Γκΰ/υDjσ:oH\”„€νχΦΦ–5xκΠp₯1Q]bλ•`Ϊμž nPc¬Λ6PPDΤ>MτM8`YΥaήfvb ԎOŒMŠOXΙΒΎλ ŠCUΪ΅Τέξuκd샏’%sίRgά‹fS©τ~Γte~=#`[¦άk ‚Φ°ίΠ%P›s +,6₯’εɝ„³/Ϊ"q‰έR{Λψ8.»£ϊΎkε…ηJρ΄›JκΌͺ’Τ―m1=uσ’9ψ Ÿ>,}«a,γMƒι'1Αbž;ύo “ύΡAΧΠ<Ί:.Ώ„`Λ‹Φ_7"«ΤΖΉ½²ΖΦ &ΐ†kΌέ +<η]ύ^”4ψ2ν«:»`$ίΖΖ1™Ά}Ψ0Ϊδ/ωκ[Β΄’υuΗξ<&Vd‰o‹‡|΅Ίf)ΘωΩ‡pύΚ£ηϊί/2a«Φ~υ†¦y…Ζ>Ό,€‡£XςIς'­@MilS5πϊG»! V•¬ϊ΅«ΊG΅T~I@Ω=Q‹‚―³<γ=Q4α*/}ύ™ώ’'QdξΩη'-XγCΗPVζ–œržΕm ά >rL]†lΚί‰Ή*i}ΟEβΧc«¦ZΖ&`ΛΛƒ#χή΄.ΏIϊ/ζΐΧξ<δͺ’mj–σωΓ8G™-πŽϊό=+OΏœE +vΥνΏAdΣω]‰g2\E@Κ?h»ςΐα‘Vw^Bm4P€Ο±YͺS² "c—’–Vy9›•Da§Kβ|)ιϋ Ώ^ώγΛ^η\ Τ>/iŸΓΩ‘šοZ<&ͺ‘s$Ί¬Α“Η[₯ΆξA|Q…CBȚψ1ŸN%k¦Γ©α=wΞ„X•Œ Εαh8)$T¨Έ€³‡œ’λί՚~ι ‘ͺ‡ηNxΰ"ω8ΞΨ>%Νψ Eω±VΒΖ'Ϊ‚Ήχ1\Ύι•­ν»Β κ‘ΞညΞ0ψ pn—Θτ§‡Δ•βΰZ’2:φ•lΟΝp‰]ˆ r§4ΣqyΌ‚ŠΛηρΑhυ +δ¨OXΨ9ažl"Ω4±pΪΜ?+bIP;mTt˜βΟSbΥΩ†Ξξ½)}Δθ’βζ©_|κ— Πx“έiof@ΐ½ΕQ΄XVΕ16Όy.₯4cβg™Š0šύBΗIΜXɈ5­K δ*ΉD¦Š}υ}„­πM<¨UΥF―^aI6†όqaŒΣς»‘1$!ϋΒΪV;ΤO“¬Δ1RίF΄}φˏ¬zrP7Tπ.€/d· +>›?–πŒzyq”k₯”FΏWώR±LΪsŸŽDQ_\σσΕͺ•R[\ΒΣTΐ56§ΘN'KOퟑ‘ItΖΎu¨-㝸―)Υ^'mdbŸM”>ͺ€>Ζ¬I¬fΧδσΜβ ¨Α²PΏψ3ΔNΉ9j“βŠk +J…rύ’fr°uœœY%:mU/ΆwΤήρ8QŒ8\?JfŽyKηžφŠΫG f» 2³α->ΊΈΘ’ΆUN’:Η(w5œΡ‘QBžΌσψφΡ15xqπ]QpΙΪ°6₯Κ?FϋœƒZzών0\ΪΌ„Aωώ£εlŽ€Žξ₯oˆ$Y°ίϋ~­~ι‘½zΈ +†J.•ή„oΠ‰3zΆ«?ΡF;›α°2 Αrί;ρΖ™GΩ<΅Λ3—I}₯ΖR ήψζϋ + +›ΉΔΧ0ΌΞYν¦m¦Ž σς!Υy.‰κ‰MΌF4 v0²Τ3 Ύχ‡yJSˆύ‘ΛG™M3ΒX6ͺCΔυΐα€†ŽF”·—ό‡GUΞνvŒ·ŒŽΛJηΙξX(ί‚¨Ώ… ο΄ίτ;μ_Κ½pY.ΐϊ#ύ³—믍$S3ηι2_~τ‹’‘>Bτu;‹Ίό[¬Γ§*Ν3PΐQ»ΑŸωƒ„νόαU~›δΖrθbκ ίd”‹šNa„ΥκŽτ.ΰ}υΩ’D=a‡5F΅›"(Hc…pΊ»ΡxΑnΕΫΟD€h돚Υς“Μ™e¦6Τ°°άJBύuf” ²w(¦$₯Cptαβ:K‘cS‚TAά―6ρ=–΄‚½ς9‘@E‹"΄tτΎ[dvΛ‹rAυΗερ[]” Ο½ψQŠΛ¨6ˆ·ΰgIGΗς‘$r{°στ?Δ΅Έ2ΈΘ™μ£’4(¦ψΊ h C Ϊ-SEPmθMpˆdΚO,d¬Λc.}F‘+d€’C5HΡέ‚ΑΡXσ΄LY^_k$¦f@ΐš?Χ+\GωΌ^Mζ9(x;œ»¨ƒ,ΐϋ|acB V€Αω[οϋ3ϋc‚Vr9Χ>Νmωf¬Ϋ±z;jFΗΆIο +σi”©J³Ο¨œ€ιϊΕˆξy”ιΈΒ»FόΜY',?fqροώ₯ψ*‹υ7,ήώ€zΎ/’1υΝρd"θ(aΦh·εg€ΆΈ{Ÿld»i… =;μ Q™-Νυy¦σ˜((†‘u‰s‘GbukΑωι³’ kΞϋ―h™₯ςh+-οO⇀eΕ~Ύ[ž΅m•ηŊ:¦ΓΕP +Το16™ +C[€πΝHρW_&ο ώΌBKΐΙΪd˜Ÿ¨Σ?‡zΛzζOΙζWέƐ͓0Η…X¨>· ϊρΪ?—D|Π₯=f"κŒΰΘ-?ΥWχΆmΔgύ°MΧA$Ϊ;ΕόLC)%Μλ”_ΦΝΌξ΄η`CH +TχέΙ€BΣuί© ςeuQ6c+9εΜY«± u,ΖLαŠ'(Σ$7θ( q ΐΟO=ίšΗυ<”ΡίλY)3hα­Ψί1’m_’}Y‚0U€ΤFžt<ΒJFy’fΌ;<‚2ίyΡ'2cXΫMšγ;±βθ ψgρλΑ$₯zn^mm‰%ε@ŽY;ύ|δζyθ.89»(–εΦΩŸοŠ†ΒšΈZ*aφ?6P‘r8ι?©1`ξŽ4™‰ZmΤY–u’Ψ/ˆ¨σ[χ™Ώf‰:fώZΖΌΉ6UB¨’φκCω¬ί•ΆΌ0š…¨₯—0ZΊέBœδΜ]@^όΎyπΛWq1ΒΑX‚·Hψθ}ΥZP$˜Χθapή~aVΟ³Μ< ?£Ι²m!τ§CΦZΥ"}›ς$ΏpέδχΑ )Ίaή©gχύΰΙ{εΟ₯μ-gώα£Ψ`ξΰΛΔ_Lž–˜Ds»ΦgΊ@―aΪ`©Ϋf£XΕxt=„ΦBšΖqSιδ·+ˆ \2>‘ς΄ƒƒai,ίNqŠx^΅όΕ…$φ―k8r?χGΛ–}M€ύŠςU"z–ƒ―‡2JΗ, ―“yŒΎ‰Σ)9_θp΄%ΖΟ7όΐ•Š‘Υ~a&ώ‘"zA»Λ₯~’eΕb( ͺΟO@°?¨~ΩώβY^ά«YΙάmά§YθΙpb·±’ΉN‚2‹(ο)ξ‘άυ‡Y‰ϋzŒ +vγήL)&τΰςΖέ/hͺEο±GRάDβ‘ν΅Θ²ψ\<l}œΤό ¬†eΤώύZ!šόbIy\:@ =‰Kς{ψW γ9phF’Ψΐλk΄=ξ?Œ VΠχόΎθU[“}ΫaζRήδΟp¬›oΘ΄{k€Υ³Pμ ‘§Ξ(―₯ς†λ-―y3Υ8ΔK_Μω5‘Xυ“‹Ϊލ(Σuξ³Q ϋλ:5ΪAΉγβΈ%mτΆ\«΄6jGGΌΘΫήΤ΅ή§L?J“ύΈ{«fE»Φ2 5j16όݞŠΉ[f¦ζ΅ŽψWžΣWl΄”dΟ+ι*ΉΗ0σa…³bΨΥ"ε— ωτσΕmqEΦ/(–¬+qΟΖ…ό4-§@³μγΫ£L„ξ’iιeΩΦDo²όΦ§φώyωΑ&Ο`81ŠΉέΜ’‚Εω¨YΒ kO¨"/tWp MΥ•Ÿ—©CMδΏ6*ΉWΘ7΅*ΙΙ΄^Z‚ΌΦ'—€Έ}ζPΚbbΒ0r9αυŠζh5‘ ίΊ{» σ…όZδΗ&ŸΧΤ& ;έeρM΄&ζεΎdHΟϋd‘μΦ+…ω€6EBœ2†MP­Ϋκχ‘Κ{-2’*ξ Ώ9’Τ’ORxu”ΑŒ€Y‚K@œ"~ΨΤ©Ψη#JνΤΙY-έcI…ήH΅oτΚχŽψ8{vΎιŠŽ"ͺ«ΙJ^[”Πnυ95BΫKJ[ΦΨΧ₯=핇ΦΠ΄ mRΞπg‰Š‹Su»IΤ©Ρ>#7Άιχ₯Β†sJTF5ο€.Ď”Ž…9kMΧΠFaj±I3™Φ[6Ύσ \Ε©SΉΑ?Οyy?”Zw4lϊ½'!Ά}ημRθL9βΠCKƏx"x tڟG«ΔκpHj)0„Lφ/Bσ›Γ¦%»¬)¨ 2³~`2υxDόt2RM^ΪαTΐ€!˜mΖ—^TαPaβ:Γ-K€$‰½Ζ•Θ„i­ζΘl„e+³γ ½LP¬v‚ρ0κ{L%₯wΝ·ΪUΉ>(sα‘ζΈ«½Δi½ +0žΒbΧύ-ZΏύΙAŠ―#E}NMΑΑ<Θͺά’c=¦7άEΩ €΄u²N"%Ϗ—ν'ΩΎwΥδμTMc·°ή|.”έŠ‡ͺcκΨY°ν²©Κ%vώΔ,“₯ Η}mmg͟N­έ‘ηhς_Ψ'Ώπ–ΥH­ΦlΐX p:?‡"±Lβ†!^e₯‹i–·₯GΤy–lČ+*N΅F/Q’^H"PEυ”U΄!֊JFΛdΨB2ΔΨμ„v/©€θ¦λŒj‡θIYΕjΗψΔ-©μœl§εnδΆΚ• ΗΈnΆ0""“mSσ{ +n}φ!joϋŽΏD,8Ύ'x &­ο ‡n-›‹v5nLΒ?-ή„Ι5³sΞπιJΡ‚~’3εžέ ”YΑηζ«•?›¦₯Hsς‘¬˜'Ί€s:A{ή9’(Σΐ­t…„“ιηh1š`›ŸΜrJγIJ;tBπnοZFŸΔέαΑχΩ4U3Ea·}ΊΗ½Έe`ŽΔGΗO5Έjόν<$ցͺΑŒΰ6 +€¦₯q'ΖT)°βθ9OahKKc’»Α²ΐpΣίL(™{SG«σpm™'_}’!ηX“Vi»‘ΟkπXƒ΄†λ₯ε ΩY6‘0-–ΕκžVΒΤΌΟs‰Λ³ψ5wG|Fkρ…'<‘+nωΤ&ίΗGR}3κηΑ CŠm%όaJkφPiΐn¦z€†gw³v~XS0Ώ'δΑΩYV€ι™ό³ZΓΛCWτ|£yΜ4IΚΊN) wOνfCΖ_ xYT;ά§q^ΞΑxξςο|>7Sυ€Βb+ςE:a2<ζLlωn§mΑΏŸλ·xγ4NmΓΐ Oh@"°s‘ͺΥΖx™ŽΈX/ευD„ωΣN …ƒ7? w±ΧΎˆ KδΣΖ«°­d)5MW„..― Sμ2“q΄tΖ}Cv·}Œωϊύ6i}ϊFMΚh5Ψ.}ŽrX›τ)Η1ce]­9έg}žs/ΎBfξ4UΊ–νVΌΐl΅>9hί½ΚnΔ—Ήί‘&ΌreΪ°ν|^Ό–?θ©κ½Μ9ξ–г>wKUyΎύ?δBEζ­L¬œ%ώ–*΄\ΏττΞώz*MLφΪ+Εγέgh₯rkβΜxμ:ΛUQac©ΉμsυGTΐŠiΉrξ_ιq‡XlJ1υuό?ό/3ŠΞεΓt`v LΌkcτ¬ΰf”ΛΗzμΡT¨>Φpύ†^ΚonFXMΕ³dŒ˜’Ξ“£ CuAι³0u” Uμ½δΐ-•ρJ Ύ<±₯Ψ,2qή1χ½­±ξD½ρY9₯"jͺΙεϋ’A>,=ΕcH»­Δ0;ΎυΡƒτ%3οNΞΎ 8eδτw;ί΅+DΈϊρ‰,ΑL©ΩρΡa@$–Ύ Sκήp4CΒΠΓs³#;dΎ,#κˆ?χΟd‰Ω•0§ΰΙ~BM>rίyh™Η3 Y$)_ρ#|tYσ•Q7Ά'­VλW κωiΐN#Ÿ%iΙ°|#UP=LY l§ ΅υΔ+›[QAσ!¨2XΙ‡A>Έΐ¦0VΫ“Ɓ`Ÿ4 €›ΰ^ λΛi„?—΄ +ΐΞ½naΑ½wζ ’±δόΎb4£φ―χ«βP l@CՁuΞ½ ”P^Λ³«“ω.GZ?χ¦΄κΜ*X(ΦΩμN{;…ς­%ωύŒ9 κ9¬\+-{¬κ†m&p9±Β3‹΄‡Œ¬=;Χ•€t¦z!AGΔ<_Ψ,Dڝ&ΩΪWν‹Γ―Mη©N΅Ή4R’σΆ/ΣbD©Υ†bl(ζΘΑŒΛΑω/τι€]52»ύΝo₯Îώ–Υz|œΞˆΣ‘d#d%Ψ0—Γΐω†~(αn,tπ’Λ\ϋ°ΘLΤž΅ΏαΦ™™oςxKl ž»–7$`hυΞά²»₯cΨMT;w€γό@ΨzλFUQ^BχΈ:‡-Q Λ¦mΫΆmΫΆmΫΆνΜ›ΆmΫΆm³«Υ«Χϋ†˜DΔΑξWΎΧ—ˆFŸ„ G|¬•υ`x_0PΐΎΕ­ε0Q+­?xρΎΐ(C § +vTvž~š%@,ΊγΉΆ“ΰ9Έεκ—Ω/”\ΔϋΩˆ`½ͺΰ€^VÎ\±)‡i£gLQy¨/}·Ό>†ίϋ…}ΗmΣdŠ”υwtΝ"­ ζν5!8M=4±tθ“’%Ύ‚˜KΖυεUˆ«Q]x ΪΝbσΈ «:Έ +σlŠBr:·“81οgΠθ+U2ρΥOx†qgm‚ΡΞΜTβcόFwl θ― )sHW[`hiΚ'’A—F,Aψx?ˆηΈto4α +Ή=ˆ†Žϋ4‹Τωoi€J„F­¦›·';`,’’―ΟΓ?ά\κόjςnΫ«ŒbO™9δΗsΔ'―"εόeAφ?mkL±4™νύeΐσjm?tPœžnVΛ©¨ΆΊλ“u\¬‘Ιbι_­Φˆ9έ'γΆι"Y€«|ι3yθ—ΝTπΊ#sš“B*0žNFPfNθ”3Ύίόt€˜’fΚI=1˜0MΒ ς}Μ3“X–€ήQAƒ†z +mτqχ ξ6όNόF +΅"¦ARπGήΥ‘d•‹VlwƒUqW.E‡kmνc`FΧ!@1Rψ +ΣςΉ΄τCΒƒδΘ™oo.Θ5\GΔΖ6ΆZαύωΡΈZt―U’7ό„\}mΑƒ4;Cz>ϋεΐe{ΌC˜€wΖόWΪuNζΡ˜Ϋp/‘tggbSW‹hΑ)Ωη΄ε«˜Υτ&Β›ΕLΈzeΏˆ έ1$eΨ”IΥ€ϋ™ρ3Ή@ζ]ν@ΆΫ9p?υΐƒ ΐΙGJZbYΦ$φ•OκxeQlΉΝ– 0ξΓνGw αﰜ™3ΐ½O=)ΦŽRd)π +οx―‡ 9%b―'+hκιφpHo,»T˜“χRoΊ­€qξ²}Λy.™²ΔΗΙΈn§ίψKc3ƒΡ‡ ²"ρ‘­ϋVΩ01A·S“žψΟJηΦ\ +"ζοοjΰƒΪ‹/χΊ=ήβpΠΎ2υλdφ”|Ε7HY[OK…{Άkˆw“,QΎω~φ―»«…Ψ•eŸc‚Τωĝ„*η؁ΡξΎσ–ο X+‹XW½ΚΏ›Τ_―Ξœhrτ”E6υ˜Ώ^‹Rϋ¨ωΆ©{φΈ₯Β–)η:©ΰ‘ͺιO•Έ;ΧϋG€gώ1q;Κf¬εКΥ^Cu$ώ}mΓ`μό~Oκfxο-1ή‹κ¬φΈtυ₯ΚΪ z§Gό;,w8§±΄J>„Ψ¬+ΒρF‚fց'Ήά€Κ4e€Θšx–uŠ~ -―‰Ά1vΖr ήψ;EQˆŒμΡ΅‘œ‘NKΒόόΒvΞ£)«dƒw3hɁ«εn{α±ΈΔ’ύϊ‘“ΩςπΏ{ 8™ςb_CMήcβΛ±―_pΙτκ—ξκ―LΪ‰.Œ?ssœ |πH'¦βΖ±½ΜŸ(–ΊŠ¦ΒJ¨φφO_(7ιΊlN₯χGΏ Wࣜ‚ΆO~ςΉ©!1ώΥs ˆφF»ΦΟ­?mΗ˝ t3[š©ynΊ³Η κg/`φ’ԞqΛκ«oX’kd.ΔΗ4"FέΊ―-Φ”#š₯NΣΰ‰ +εώΛ=ώJ%5hβ₯νόŸKΡ $y ψθ‘ΓbZ9¦χcYΛ²Rί!Ek Ϋ‘ŒS΄fϋέ:*Ώ>ίiγφς9·§‘B$Μ†Μ ίώZT‘m/Nu—+‹Α՝΄΅ΙB6:θ"kβΕμ‰’ TŒ,:ζQ9ΡYw+GlšͺqΒΜΡ_μθή>O9u%eθ…²‡Y₯QΦ`ώέ•ά$Χ΄ŒΪΈ§<ζ£ύV°ŽgςΡ'5»Έδ‘Β`η[yβ­±<‘―ϋ8R― +όέHς i‹Œ‡°ρΌώSVΑΏaΎηΜ›\ύΠnν¦‘PΈ@žΧ?G(έq°Σ―;Ρ6εDρDΙu5§|Λβ%–΅©IΫr›κ'ϋ?† +Ηεoιnκ/Ζ_{sK°h½~–x^UEηφδEΰθCΡ +V΄HΛζφΌQηEΒδV+9|η{ηz&}†UςSeΈo&Υ'Io€>‹‡β"Άγξ7Ί!‘R_…ύ™KΎ“²'y‡τΛ +±sok2&QΉ=;.Ξ‘‡`Γ.šEΕ•Α/Ž&Όfΐ<Ιϊ[˜Ž­™xcIδ€0ΕΪ¬"·30•8zΛΖmγΆ9gY;?Zc°UqΔ2Σ<.œ#¦…,ς-ΆSʏ€ΙόκΈͺΘ³K-LΓ¬.•Δ˜ΎG RžΝ΄‡žUΝB^δ`4šλ«<)GΘΞnΉ‡·½{ΤΝj:V0ΪŽΕ†@PΗKΐeΒςρχρΚ\\cφ…™ΑOZh&Kϊ[¨EGCVb‘Έ 0yψ&ϊG΅°jbmΕCvv’j‘²φBaŒ€IN»¦‘ŸAιU…ο,UηύrΞφν$1dQš]‡q‘ΫΗθˆor@ƒ―Bs, έΪ“X2Μ;–zήΜz1ϋ’DC”qoP–NΕΒΌE9[ ξ!Lξ‚–mασ0g1\,μ=‘S`Wε­}Ψ#:ΓύωξhΝΓX–Σx…ͺUΑβκ½€‰KΘ‚¬4 +Y³:ͺŠέ1, ZςOσšŸε¬=Ρ'ώ*ημ93ΗFj¨κkΪΔDΘÁs˜ώn΄γdD?€t3χ·fX¬,ΐδΠΘΨ’ο8'mƒΉM#cw έvh  ν?U‹°.‘€(©/ͺsχ€-ΚvόΧθένV½1’6έΘΒ^ОG6ω4'΅—Ϊ?Εά«ωk`€rfΙc$°‘«i'aϊpD @ŒC™΄\τ^Ώhp~?&ΩRͺΖΡΝ-ΠΛCΡ όS@k-ΆΘ‘*ŠΪΩyσο6’"%Ώ9…»ζώ Κδδ ρgΓV/Ξ{ΰ°„γ-ϊz† ΧοηzΘ‹\9t#4bΈS(v$j¨­dMΔ,MŽf±7³6R;ΙΒΨα*z)]wΰψκΤ+¨αΒσ^$£tΜώκ.LέDTϊ²ZΟ6;ΈΔς ΚΠΟ Š^TΑΕ<¬ηε~#ΏΪ°ΟUFπ‚χA£A Ε_A#Ρ9Ϋfc΄;%@QvˆBΆ—ΞμŸ'%₯ Ά%‰Βw„ςΜP4¦& -\qώ–ΥφDfή ΛοZ£ε-ω•A†l~P˜.όαD ;˜7/Iε›έ@φj jΎ‰Μψz‚η<θ ι*~=V¦ΆΐɈ~wΰΆΓ~σrσg쿚·>Χ©:Yh¬‰±ΓžΟιh°ΨA‰0χΎ­V:?9΅ύΜεBmƒ<sρ@Λ Οξ»₯΅°θώž₯jCnδρ6dj7ι9 hgιn‘Ιq#ίF½ͺω#%+!—$ιΨ°‹€4bτWw/ΰ_μd+Ά#FIΥ½KG1€ RαrΆPJϋP έ‘ά,œY₯`gKΨO π[^v$uE’Τοdς\\”DdΡ'¬/ƒ7Ϊ£#‹HSΪ3”j=ρ$υvίοwέΤ +,m„xωεΞσ™„6ϋ“Œ…¬!]―ή­Ύν\=Π+ύ—^m‰mΠ₯Έώθ`œ˜PŠYι欀IqKιG˜?΄Φ'ΗΥ΄ΊS¨μm™ ›\ΣiΔB"³Θ1‘φНSrς"D“”–*±uό‘Lsͺ‘ύ¦Αgš;ΎΠC&zh“·ςΌφ₯0fώυΧ φ]Kιg‘ΗΨΒqjkˆq䑆――2ςφκ(}‹Ο=M'2nz(Ζ„bξi”Q›3©ZƒΥo8Α5’Χ'NΰνΒλ2λ$Κβ^Ώ€Rss<5Fί?ΏBΑя½šκC–Zpƒ _©"²9“α_*?‘‘[»‹M·.aƊV†h‘1έ<Ε£η QΏ(΄Yΐ§ϊΞYGΩςYtѐΔBα$f>TΑ@΅Ϋk8+vνώpÚlίGίQ9Ξ9ίίΞε₯†WΪ±"Β ά ξaΚφOΫΏaίδξζUeΡ’‘šošƒt)JH²y:’HPƒ`έηΟW«›2]1\ŠBΡ)ζΥύW²€­‘jΆ@‡R^ΡΩίcZzDΪ7&Rϋ)ΦWωψΫ €wBnh,Εv/8fΗΪΌ‘=r”ΰςΌ#¬&ύhΨ±ό]ΰ#‚Ž«θ.·ΌΞΛkA8j†tύ&¦_Ροέd5DTΧyω¬Ϊ!dπgΑ ϊ6Ηcια‚ +_ŽsΣDυ|Ψ]§ŽΟύeγΓ֜“NrηK!Š3cΘ3Ν΄Ξ±0_*@.‘H"Οα,a…Μλ=.rΎ˜;ΨΨιΚu}΄4€΅tΪΠ΄Ό±[ΘάΜCx -mΰ!6¦Τ=ΪαΌ +5 ¨ΥΟ₯xrm\ηΝ#:LWŒ|]]¨5§m΄e묃5ͺτJ:DPύ›θamoŒψœΎ[•θαΌΉ |S«s „qΙ8ιcσ―Qδƒφˆ‘ς|ΔV^ψaΟ΄± Ζζyo DIݍ.\4/ίΉω>—EmΙ¬1ΰΝυœΆ5ˆ@Kžz* +VUͺ™zΜJ…Ί§03zKx•™b΅δerκmΉJ4?‰5ιI‚‹Ϋ!ϋ¨#ͺϋR΅+υ»ψ7άο*ώͺ?„|.‡h ”›^žΑΌoΏάJ(θΊ: +›F­γΟ™[2zF±#³¬ΨRΈŠ’eŒjŠεΟ6Ο;₯ηΎA3ρZμ%(nΐΊiθŽ o³ΩUƒi’eΩSϊX¨ ”ΝΣόΣ‘Kg^ΓΐΖΓi]šπΗ·w@WŒΖ@ͺJ\5΅ccΑ¨ΚCš-†[།}\±΅RNΌ}™P¦Οό͊‚ρdˆt­„γ‘»ΘXΈ”xvχL²νφΥGi€Ν5:+Ώς;`ΔφAQϋhωΝFψx1₯Α‚YvAΐl―ϋΡ91ŒΩΔ ~kN@ f…TH»aΚσφͺ”•S۝5,]ιτ1—΄;ΦPbΗ#₯Κ@†¦#ωΪ£iΡW:›fρ‰Ε[—―oΪΖeŽΌe>υo‘κΐθ&άk)¦•ΐͺ±{›Η:Έn”Ί‡ωf§w3ά F{f[2LΖͺXΛ~m™7λΖ+n±’@ς}…γπ5ӊσ„[syΓ―?‘EFβν΅²YV`Vό±–TΪΤ7m^a쟘1†‰­»΄9Ϊ G ƒs΅™zfΎF{|Lΐ¬*ιϋ’’ρ^ssηϋ—)gNχΖΔΡSc€”φ¬i`Υ±ί{φCœΫϜ΅wLcΫζ­‘Xθ­(E“9φ‹œ{ŠY­ €σή–&Γ–NWιΞΪβυϋ~MΞ’0Ž!ωΧήLΔ―alΊΪΐΑM@ͺ‹΄“Ÿ¬«Κ cŽ?N-Gϊœ]ζ„ω5έψN1ΔΏEΆ"Υ°šπ΅$:΅™p3ǚCριc|dRj­γl$V€+Χ‘ύ€xŸIηώΧί…μκοϋσ –ΎaύΚήQR%V’΅ό.;\5W±{Ν”·AοςΖ χωψΥξΎNB&}—³*_c½ž…ΣOI^δγƒνSιυλφΈO—«°όΣ:‘?\›Ύ'Κ_bή€α½b&$ٚαzέΆžΏ³~–›6Wσ融_«‘U Œ|J6L Ή΅Š‚rVρ·dΥΏ/Žˆ δ°l‘r’’υ±Ιɏς,"™π_₯[ΧR +λ %©_šσ±ΧjA―bP7•±B0qη(₯έ(Eμ#Ύεeͺ ²§φG9buήV΄]”«5ΫΗwcAyͺΐDΊ ͺ_Έpqη‹BA£ψS©ε@„™σe |£¬»Τ +žηs."o^ψ’“fS¨Τk˜-bΣ ‘_5]0^€aΌ²ž~Z<,ιΫB3γšκΗ7W³>P1φŽsTΧάΰ„Ρδa¦μ“ϊςeqΫΑtϊΛχ³ P΄PJ-(΄KΌρC _β‡rεx ΊAΤd…{ŒYQZ#bJ°S Ψ6qrbΦΕΗvBϟ³‘L8؎μO©[x”Iف8‘h5|ύGωΑyσ6 .τ4„VΎΌT+hIί£°ΑΆ »2ua> +*΄¬c­¬oήYkΆZ:1 ά‰Υœ…ρΆ-,zΪω|†' (E\–>l.PΜο‘’΅Ϋ-ΖΎO6~–p‘υGƒxa β₯Ή² λ~&‘¦€dή“ηΫ,^·Α‹ό ·λΘSƒ|l₯8{λN¨&‰M|:ˆAσ;υΡΘgϊς(H‘-$k£(©Mz‘Κ§HiΒ,e ²˜ΤΐGCΟ|ΊΈ²Έ!ξςM·BO΅/ώ°dΖS‹ϊ€ΒαάΊί? —Μόβ}Ττpœθz*ώβηkΞ@šc‚gfΕΎ°sFAGEΑ(?ΡΟΛϋω#(q3L—Švιb ωR ψa'#Zμκ^ 4ΰ‘ϋs·ϊ›αA6Ή°±Eƒ2lςΘIλy¨zkjΆςΊkq©|DΡΪd©ηΚ"ώ Σ9R¦‘ώ6wςg+Œΐ”1ί…ώ²Q”%.+yPς—Ÿr‰iνVχΨΨσV{@{iЇQχC!ΆaH·…˜]•pϊρ“κšlΪZšΡΛhάϋ§Γ“-#™½φΚ£*Ψ£oζš.]>7¬ς;ωˆϊνFN¬ΐmΥύωΔώ™­±Œ]ΞΗͺύ”μ4[8Β‚§κ_f?Ό‹;Ϊ€©žέHΣΞ}r5Πb~2{K†Ώv6πω‰Χ#ΥΌ4ΜΟ‹’eε–0ςHzΑΕ{{ώE” ~˜{E6¦O%7Ζ~¦S<ώί,tΟί!uΤυΐFsZ΅»0βΝ6»{€t§Κα”3–zΣΜ1 ΨΠηDΨŎγ8J©ρfe•S;#@ΐL<Χξwe5“{{Ζ—e¨”/ƒ'1—­ηήr΄Υ%ήύ|»‰¨ή­v콃©pΗτ-ΗΛY@ ‰e ²;‰€%vΉ΄^ɟ±/Τ$£ WΪ1•€›έπ‡`?§uSmˆΨΒ_WϋixŸ%Šζφ˜ͺηb.ΖZχ5€“ς“½ jσpο$ΪΈΦ“ !‘ž;G³#€ίXT§=#”Ξα, τ/І{Ξκ +ΕA:Χί­+’tΛš)Ÿκ1Ϋ\,ΏιΒΊU`‘ς:η>Rϝ^pΤyΰœˆ:½Th‚)ί§™ζΑ<rўήnΆέ3ωη+|CΒ81w@ ―όΑζΩ‰Ρ6)7 KρΓc? Œά”σδͺ²Γ‚ρξ[xBl¨εŸj(\ύŒrŽ,½š¦k–]όLyΥr·7\€+Ψu Σ10B(d*­΅ρ‘€In«ί2@±λsΊ„b/ MkˆTήΠ΅XZqzCζpΨiάI@ !χίS‹*B‚…3Œ€Ϊ,„‘Ώπš†X{5€¬˜²qέ‡‘ΐ9Lm7‘ς―₯ˆώώƒ{,±Φ J‘P]aπ`Zι¬O8.u}ˌϊΡ2YMΰ't}Ύ Ωη±ξŸ­;ΊˆUMΨΠίoc_ήR ©―ϊ_t̞ΓcNζθΈ•G»,Άƒα0EŸ}² ZKv9ΰφΤ58Q€uε[‘’ šζΛ ŒFL¬p<±2…Mž›Χ«jj«+RG|ΌYΘ>Ϊθ#Wς σΏΝΆwζ°#£kŸzƒΡo°φτ72yΏYtΟέ_ΠFδ‰[^/‹œz—Ύϊ Π9l’κ+kAέΦΝwfJͺ³3IΙv4°XB8φlΈ(•Bž,™h…Ζ“‹!Œ}xf_K‘,QLωύ$ ΝΊΗƒΘYαd7ΆkAœjΓΖfί£ce#3HκftŸs‡G‚υ†)vΩ*»Ίυ&79DΆN0wyΫΰOE…z^*M?vΪ,θ ₯ΕE°€ύζνϊF> =5Oe³‡lUxσeihGy1Ώ²wpJοaY‡!ΐpŽ.Yƒi$##:3ΔπωΐzΖ_ΤΚς™,–ŽfR³ΉνΚώΐΣα—ƒΟ'²S_ Ψӈ}ό3ύΒ’³‰ωNΛG'ώΆχϊ’±αλα™K\θC免ίΦFΫ’0―ψΆηΆ-]φα㏅ šΝﬞ“έj‘œΌ«Zω±ΚPˆΙf–ήρϋ'£½a*θ H΅ ‡z/z‹¨z~‘ΜJL‘Ϊ%ιά!}/¬5ξ†Φ?h‘:BQ6Ϊ֏…*DώQΟ©PΦ.αΜσ 4}τ’aWCvrO€€GŸ¦sꆄ\Ž…n! cI«3Ε9ˆƒ›‚―Ύ―]Ld +ί3”Ώ? Μί_ΣΜt/@ +F}ž6Εͺ…KŸ ˆψ›σVρW‡έΊΑB©»Ιΰ'…ςHΉ›B,½IΆ4ϋΰ―½cβŒβ>FAΫέ–—@†@™°oήΓ¦'εΘv'<)I«Φψρ’Ν ςm½|ˆCš!Σ‹ΰsrκ(=ÝΙ€N2[oA²£»κ―›ΦγΒρΈœ½ήΝσgs-3WΊ₯;5(­UΣ °frΡJΜΎx¦ ±9E]”†bp@βO"„?Έ*½#OΊkάΠ?piU_νΫmΌΆΈΩ +*KΩ^9(ψcR*2’<GεQεGά·‘„u•§ίΑΜƒ/ςΨ{Ε§Y;ύθ£ΕoΨ _ͺˆ™Ιπ~ŽΩ‹0*Ύ»Ny£θZ­φ]QaΧ*Χ—Χy—©kVΛ\π–%ΒrΝ–QR:ωΚ‘I*Ό”’cŸƒ¦šSΈΌΏ”γτσnςQ7ΣtΞΌp›­\UΒ ²_~&Η6Κ³ΰ¨VΚ*μΫσμ >—μΧHK^:&˜—PψI# + ƍTllΗ]ŠΊLτY «"Š9$XΗX6k„œ¬žŸœj‹ΊΫϊ½J‰οώ΅y°}ϊp"έx UFOΑ/޽—ά +s8½yΝ£)η‰eψ€Τν‚ηm»δB•ϊ£ Ι«ρκ»ΩYx]g‰&%Ÿ7•)ΔPjMλβφ|kύ`KηKF—9žϊ?„Ϋ¦L΄#³υaθ[2Œ>:)©[»Χλ½έΑόΖ€kΣ‘S5 3DΆ ώ +—1ݚ€Rr/(9!£ϋd χKŠτοή@v¬ϊ§ΔΛsoάτ…λQœ³`Gζ°F…•4M[¬!mž$€Α'QΟΖPϊΙAXBxνm’ Κ—ώiγyOΦqJ‰yκ0S~7—\]«€ν‰β―ε«αیpΗ *Ή”ΰv|2$fΟM\8FH">+ q«ΌΤ’/j»$ΐ^ΆZ5Š–±9-ςτœΖ© XMΠFŠ»9ΠώαG«°#Eo{GͺxΪ.q? +—Ρε~ΠΕ? ₯ٞ β>)Bϊ aΕqAΑt Y―>™β;fbΐz―XΧH™‰1ξΓ ―CZύ–xKY Α.„W«#H‡ύœPί σϊ™΅”»Aιν§^`°όΥσάN"΅Ν²Oi΄ϋM‰οz‚Χt`ζ|ϊˆcWΨ ‹Ε5TΜ¦Ύέ·¦W£₯ DΓBI’·,c€ώKZ8PX€‡qιY”¦k‘χ@S#ΌŽjPQ>rς›Ρ\·΅08ܞl{i«ΑΟλΎ+n|Γ7‹ύμΚΦύ€ŸšΖΙΚΖ=šΫβ£=ƒΝ€e«τΣαhυgij νgœίtηRύi σU™“UͺD±¬Ϊ:3 ’°°sŸώ†Γq’VΖ΄»ΚΥ +ΏΖ ΰVˆ8λqZ’‡Ÿ£σXbΎ3ξύevZ–y¦φ˜†LYJςτ ’Y€―ŠzΊ[N·š>dœ%9›‰Ϋ4K’^Ύ=wΖe@‡C3Μ;Yψσ«$ΒνOL˜Kγf—…|εω"eo‚ŽTΘύλΕΙΧVC\Ζ~Wt"|Ÿ·«\ζ6Ί”Œ4 ‡<„δPΟg‚1ηΒDΏ-ΰή„'0- Χ„siΒ7kΣbΞN^“э· |¨5z€ mX?_™š6Ρ-βUλ<%§Μ³]v¬’CΨm0ζώ5ε‹ |¨Lo¬ΙΈŠν6Ύ<±­ό2jXi<…ο%iΩ”σšΦΊιͺ4’.L1xn²ŠWoτ7υh:ˆt\7Δ‡“-υήΰψίΥz45ŽΝ’˜ΫΟcyΞΝΜ΄0r/5£Ž₯&YH# +n2‚€PŽΙ>8ρ€"ψ]ΛΘΞIšΌξ*,¨ίΩ$Y.n―Ί`ͺΨ†;KΦΜ²₯AΐÚYΪΊ²Ϊ“tH‘[&φEΦ› ς“s”χcFM›ζ› D nΞέ§pdΌ Κ’ηŒLφ,ΐόjmlU.β¬ΞιΟδ=ƒ{γfεΛν\!– •)\•Φ―”―Τ$#’1+λΡWΨ|8!%§σΐt‰‡ Α<=8hΉύfkΚΚοΔΤξ€h@NωΎ‘9U60&3Wν +:ekΕή™9Άkf@0βޏ•>Κf~aFηΉμήΉd ΑςXg0νΩ€τ§G+;ΐXήWɘu,­ώ ¬~BŸ7γ%Ο‹άΐΏ§ͺ0o ͺ«β‰υώžμcԞš΄Α―'4&λ ¬Χ«5ΞλΙr 5Onΐ”N€š­ΣψΟ灃ΪaF§€3;Ny6W^οtφy¦θ6τυ"Ί|6”ΤP²8PRf’β+KMΞςΑLfm)†Δ‡tκΆ«ΥRP­’cά3|Ψ2“? PΓ«zn‰ή&;²U‹OΠ~ΣtRVώ?!f•WSbτ¨²ŠήΠ?Ί.—HΦ`#eΡ+eΏΆ£ +dOΔγΫύώaβd"Έχ+QΰΕ¨ji½΅E5m‰uՎμ­£κϋž[YΏΦ-*|k%“ώw"+ώsτKΌχπ#Γ„»-ίΧ–l΅A―ƒXβmΰςΣ§ϋ€₯*Jš©OΎ†Ήs[ΑΒ:S†[π-@Ε"ƒ”½Ή.ΈΰΒe π\:ΝΩ8«ψδ[>Γλ%)D…Žτ³LϊίΔΔΙ[LΗ4cd„fΓβM„μΆIΪε$ΤYσκl‰π>ŽdyΫ΄ϊAΒΆέZπΟ‘p«Μ,žΣI²ž0Λ³–8,†Π« qqζmt`ΥlΦ¦zκδVŒεƒΏη= h’Ρώλ+7άB-α4uJ¨ ε ¬fwΥ9MΣvζ„>‘ΐ-ΓΔͺ3±/hLjmηŽlDtQχ–ύ›ΈΑ9z,5<¨Ύη'ν„’ΥfŒΝι$:Š©qΰψK…‘A'‘Δ»Χ€(Ÿ䍙?ο½EYc›GDΌΕΊ%λϋ"PD%€ΒQŽ#]!΄S/b€ϋސB•₯€KΎΉωάΨς7<ξ«"υΰ¦ SRΏ1ˆUHΓ€‰>1ύΧΦ\ΜΨJeσu4‹*pΡͺμ‡Η!ŠM­(x!!Vρo=šŽj+c8FβŒBMσΉxn|ΒΏ=ŠςΜZky>" §Ϋ‘€,6 +}CΏ>U’δG|°ΖQœέFBΓΫ_ήΊ•8QfΣucw1ηRJ0 Μ·­}-―99({œ4ΣΉƒ³–:Kb‘ˆCΏ9DpΝΠ΄žμ8»Ύ;΄@Ϊp”ΩΠάο+Ώ7ΰQΧΡΊ§lβΛε o›%’Zκ₯¬υ\ΊRε‘έMG˜]{εğ½‘ί•>(υ1ώΰΞ±¦… τZAΡ₯(zgšgΖΜ’ESς)Sofœ·έ&¬gϊό3Ή: ‰HΧ„ζͺ°Ό0-ΎUipςA­_ύF-ΙbλΥcZ +εH0λώ0e –«κŸΚ7‚©#~ΌΰIΌd‰Χ#"N—Yy1…,mϋvŒ©ί_SΙ&H)Ώ¦t/ηυγηΖΌ°ω£Ξ°Ϋx±Ο"f}Γ8^ό iŒ‘ωΌόGτŒΛ5νηιZi‚Mu°v„~cw6`Ÿϋ-ΒΟΩVuζ6r§=ΛώΒά~–,Α/SO}s‚ΥKό`qΉζž €ΣΡ Τjυν'™dς™τΗτ;9΅ ?ΰψ!6αϋ“Ιύψ½Š<5³†•%μ³λψš$EqΨχsœΞνͺΔ΅sTuGRχό<†΅[Ϊ~ΜΧ½&σ£1Ν6 ‘™“ΊΒ nέ²½Yή΄δd!Αž τn¦˜SPρ9"Ub›:-Ξa‰m Κ8ŒηΪiοŸ"yzΔ`―AτgοζψY’+“ߍcκξΕx «ž\OjMΏ$Ž4‘DD'e©ύΨ™ίycΟtΞ‘ϊy<Λ©©ά¦¬ΘΓΎ‘ηΞΙ<…Q―μψζHw$oqR_ €ν$L{:1ΖBNώɊζ––vlΖ*Θ4|Ig<\|ΈXΗΫΪ=FŁhB±'NΙΉŸέ•sƒ6ΜFZΙ#³‰Υ戺μ ώ˜»Α–ΛΚq=Ÿγ€<ώ;Yžέ°16ΆλΫέ)70λ˜Ξ΄¦  +υ€±ZqZAΌΖΉ€γ Iρ/7΅^OA]u0X#ύβu£ζΗXΊjrΘΏΤΞH}ϋyqVΫΔ Έ¨ύ1un#³3ΫΑό+«6υηDpϊ'ލ‘‡\6^δAˆyΘW_‹χπ ©ύ°Tώ[Ω XRρηˆΤζ# + 9‘υύΛgajΎ–ύο†Η”ΝνW$J©EŒzεS±ω-W‰YΈI-x'᫟ΖΙ—ϋ‡4J8sΏƒ£5“Δ‘o…Sή₯tδ+-£Ιoμ‘§ƒψΏb`=iΫ Ÿ- ‘ΔΈ%η³_Z·υοΜ!‹…Έε‡]:zjΔcŽΧ>sγ‹AΪpaθζ:ΌBΫ‰ν£—1ΨKΫe£#m ΞLy&"Ί"aΑ:χμΥǞξη5ή!š]ZcκΨ0χ―ΗuXΒ^ϊAm »\[.Y3Φt™ϊΙΰwΪ²…+uœΎ±CSŒξ· ŒYλΈΡ<ΒDš~©ŽO…gNrϋ’{>ƒ1qxΐ^PofΤg€~ωT)ΘIΙΚCθ*υŸt ¨ΒΆ:Ÿ¨˜QΕN2AwlSgSαLŠρc'Φ«!IθφΈ- ψ•γΧ M€γί2”ί’q˜ύΎ@f&·Ψη ° σρZ’¦†±΅ήL²μ¦QΩ^€zD$Γλχό~ڌ©@Χ7)…%hT.ŠΕ¨ΧTž–Oi&ρvη_ΤN¦>Œ†Ί}‰C5Ύΰ‚γ΄ΐ…#9!ΈΝm_'ΚβiΪCΎ=lM윑 Αά‡pcƒυ™°ahφ…ΐ¨±’e•Œ}Iͺ›Θ~ψ)WIbܚ\Λa£6ςiNΨߐbΐ ° +fΜxZ ‡MηκηέeΧϊ,―\ΰεΨJ'ךˍΖοU@χΰρ(’ηφ*·Ÿ.ΧΡq)π^>V’― šΥ³¦τTΜ™_Ι‰€rۍ\_ZΞ’~ιG?ΐΪftŸΦKϊIΙ|U}Nœι―@ nԘ»^Ÿή8β:¦—4Γ(˜‡°gΩϋ‘χ’˜,ŠΩΌΡEhώZά2ςV§ρžδ\Έ"Ί½ΑψωŽ€ΑΗ§δ«:£q  ΰˆ₯ˆP•ξcE³yΈΏ·!„X€…(¦»3φΧb·hςŽ6τμ"ϊ!ΉOέ»{¬;Οu'›‡ΏK«q7]5|ΆΠS”—jfΨsάΨ͈@ωŸήβ!\?aΗ~™}†Ϋ$[ΣiοΟ܁i ©Ÿšο­0{”ύψP$Ίγ\°VM¨ΆcJΜιŽlcgθ‚G΄{šΡЏxθg$ε8˜ΕΑYŽαGί!ͺΌ>ύ*ύ„0Ρ}ΡnΥ–² Ɏd(ί‰ι€.l&ό9cαρœ ΆφΣkψ{ΏYθ +`xuκdΰΈ0€Ϊ43ά»*^ά#G5sŸ»"}Fͺ Υ'„‡¨m³e΅}οΈuξ¬#QI•m₯£χZ1V“bv’΄μ:σQ [¬Φ―υνŠ0s)žxΧΣjn—½qι€rΗ€. χ̝€jZΏΪΌ’σ4βΠ|$}¨ΪΟΑV’F1Wmߏ0j.#ρ”vΩ96j2ΊšP§P @KRΔH+Ύ˜ψ'ͺ;‘οζ?br !5ΉγΧ5™HO\ 鐝3{πšR T2Μωf]Ω92βG>ZkΡ£Iύ'α%{Ή³{:μŠΡO~ŸΨ˜‹d8Η―zΰλ»ζ›(Άͺ1rC Wz%ΘφΨ»˜ξf75XΝ\ž₯LΖ3;N2τjκ8r}2QΨcƒf˜£³u›©Οcρl?ώ5ροk[αžHΫΠKͺKŽl:7Ι»@‘ΉŒ+ΧΧA?ˆ½Dœ§‚O…Οα΄K4_OΊ’ΖΖ„š¨φκώ< In±Ϋύ#eΊ†Π¬ι₯x‡Ιλs +…xΚΗvΖ Sf5—κιχ σH½(!ϊ׊"τOD lΰF†£ΑΉΖ›όŒp+BΏfΉ¨Ÿ3hξϋΖχ°fΟjŽ}έjτƒ™τEρfŠSQκD·‡eΜ\Όθœ蝨wζ]άV΅Ug₯5½ζ(r Ε ˆŽΧΔμΚυςΗΝuŽ8}7¦zω7+Σ₯ڟ>w~§ύ·8z/Α©"ΠjhUλiΉnYMyΟF‹H΄'ΘΞΨ—€Kώ'z(lY΅Τ@tz﹞ΫΚΡ5ΒβXΠƒoςS ͺβ‰Ο:Fδ-–ϊι’P¦.<ί¨TΈ¦EΨ,N€a +ςΰW7^»΅—βω€έΒ cWk] Σ;ω²1u˜,JT +«©aκ![”°`vUνͺ"6θV29η֝[YτΥzΏ(³8‹τ"¦Αb‘λtoΪηœΜ’B:’0?‘ϋ{OΠ’²Ό½γρC ϊζ 6ΦΚΊKμ|C›ϊ9£`ΞΚΗΌ¨ΠYϋcΦ’N]Η„ΗAω¬,T‡Rͺl½°ώΌ‰ο†4;Ά¬ςDι»ΨjΦ‘ώέ³ώLM‚D•UǍFžˆ3q‡©ΐy-­0ΰπΒμ€A1ηŠΛaζΰI±Ϋ£ώΓ…\›Ώ~χHaΒ@€™P¦£ί9d7ρY%Άύ[hμS‰Bι†`ΒΤ ΧXΙYBή(Pgi€'…Φ +z‘Ώ:Cš&VY)yΣ>,ΗKχ!~₯XEkχΆ~'’ω»°+ρxα°ˆ±$<ΩύζT3p;†¬Η„–πIΆa»ΑρoiΏΦwͺoήFς.Ε%SΊ:œχۜάsΎθ£-tΈΪΥ ©° ω„›Τ€U·>Lγ€ϊ™επuLίΎ²SΚ(@θΞŊX«ρ%Z(#ΐ˜@i-RP}rυ |₯܍oέ^]ˆq%“MdSK‘‘I=ώ€²I ³Mρ€Ό·:ζ˜.‚Τ̝Ι]§π‡8‰bL&Gό τ&1oͺΊτRγδ9hΖζφφ™γͺρj’_΅KλtS·πDŽ*nβ‰ΰΚΤΌ€=΅ ¦τXψRΌŠ‚3{ο*&(ς0P&Φ–6©U.E>qύaΏΉiͺκ³”₯ήJH‰­υΉvE₯u£Κ±l°΄‡‘ΈÒњ[`ᬌψa™\r…͍Φω^Wθ n4νKO ±Β*¨‰,Ιœ6BY€BCΟg^k +―}Ά‘ IνzΑπ†`A©‚L«…Όκ:²bηARcL™«‘kNι\ΥPζAΌχΤ² ξΟΙ›Δ^k©rΝZ'Ί`lΫ+EtjπiϊΌψza lιλ³Χ+/ƟMΥΆΰΊ"U‚\Keρ­#Ρ«Ο+ηΞk„M:‰M}vδSL[nκ€BRΊœ'qκ‘²ήω†δέ*:¨ ΏX‘ ϊU}β΄―R, Ω‡ΦΎΰŸ²¬”›Ϊ™½ΧwΞ€KrΞ{#¦Γ΅Ό;v,«”pΊ'qrΜιGC:aα„βhŠWχΨ΄bκ˜MήΧUΈz™ΧΑt[Š ?mbEί Li»Xi¦Έe°σE~G΅Δ’Dpaκ:©—€]ΊGο7ήχςχ6lΚπ +f’ͺ;ώ¬»π—&„_zΚi¨1ΜώαX 7…Ύ ͺ]έΈ$58ςZy:Σ=sΆ xWίΘ]φ|D’bZrνΆρΊ*Kuι$82Ν†ΑC9Ÿε!6.Νσ"]ΩJ‰ͺ^hS|ΤΨ΁#ΊuD™ώ/1›wΐάΘB#dΣοcνŒŒΌύόBνA‘FWδξ ‹#‹…L2μ(M鸝Em’w£ΦϋΤ1Ά€„8m;]TeΩςχzO›ΏR`°“ς”¨ώ>MlΛ ?VYφ’‡Ξ*w8’RωrίGΔθφιΜ5ͺ/ƒΧςZΪέΰΒ:΄X‘ŽΤeHX- H±D~PΟh4dg₯ΦΝΉŸ`Ι³­†>ALŁ"`6!ΊφFπ΄Ž:I-;ηΥΡνAΙG`ύ¬rΑ<§Ή²ž,Δ›ρ:D!›*ͺΎ%­΄ IΥlΗφšH†­\a…π$iG܎f] υ“ΩΊ©&ŸΐΨ?ξYCw-<9ώf{ƒ­«œ'Ϋ"₯zϊ°θβ‰Βω`y(ζ:ΗΫ’δ“ΞL·»Ώζ&|2έVtΠ–/S ~3CώψAΉsr έ3>ζ2rŽIKϊ©ŒΏΙώτƒŒζ]6k‡v3+ͺΜκΉη•ηγ)Ÿo„z ts½υAUςΏ;'4W“Π Nkξ•ΈF΅ΐ+*α(al»°­<3€ξ)Β'±υn§9'o „θ'ύeωŸ^Φ"Έ‡¬wEΊ4ΛΆmΫΆmσ–mΫΆmΫΆmΫΆ]ΥtwΔΜΌΩη*Ι8ί‘άg”NΝ±Ά)ϊϊΆAφε +#Wž^gΎTP­fνL-.’nœΪRt‘y¨XΊ[W@Σo[°’΄ ΐ`£ϋ¨ϊ3`Ής(χ56[†e>4ΞesΏ ι–Ÿ4j Ηρ‰3w ΣΕ¨ήs±ŸG&£X›<ˆ!%).Ψ6„€Ρ₯·―cCΫΝήFhσΣ?ŒρbβŠ.’ΆbΘηœDσ;ƒ‡QΊΦk˜6+x8ŒW bΈήκΖ 0Τ ύŠm˜ζ 5D!ΌHή ¨Θ%zxxšZO#γY˜Ρδ5Ϊ}s6ΙϋΥέ…οŽΏχω‘˜}ΕΘCd=lXβ”ϊ&΅Ε¦Ϊ8=THgσŽ«H&ΞΞNΰmθυŽWSš6Kφΰ€»ώ> 3‰ƒAχS₯ήΞjΉxς ‰ΑŠšbAi݌QŸ~όzXœ! ’’»ΉσEG4pnKαL[nαύrνq3s8VΛ¬&0.ΥXΣ‚τ‘ž²Β?BΆD>•kϊΟ³uBnt=¦†mΔΩ[ξ*d{6ε:*πξσͺ„±%Rj;KJm4³9θžΡ¦™gΧ0€†p"p nΜ\ +@^‘ΝcT“–‡M.Ν^±|‚4Ί6ΓΝΪΙ6ΜέΜo_%!02Ω=5^2?άaL:Ρ‹ΉTVxšžΌ›‰4έt:ϋο8…Άλ;g―B%RΑ@§:<Ž_Χl£Ί³Sr>Ά§3Qζι,PΕ QΐΥ©yά‡© «˜†ο>ό'‚¨ΨsαΊ3ΩΝfSp³=ΖιΌιlΰζœ„ ζŞp8&ΰ ‚iyvΣϊ>t‘•³Δ3ΚyρcŽ'] θήθF2κ, z?W4š>rπxΦο‰ί‚ώ’°K‘TY\ΒΏϊU*3†;υ’ ™XT8k?\ΐ2ς‹›βηρœ­σΎ±’iξμY•ϋ=i„œmΞ|τΏ-ΐiψ§Uζ,WHŸTΆ―)|ަsΈ^؝ˆA*ΨΗΪ+˜X"‚»Eήƒ–:Ύ3cΒ‡ςqλ2σ³Gy¨’PpΓΑ¨>Ζ”ύ…‰]t2F)τ¨ͺΑώΞ₯’A'šŒ-(²φ5#έώΎ”πΑν;’€ +9ίΠLžύ7 Q48nτiXΪOδ6ύΪΩξΗ‹8{ `¦L;GΝΥp«)ڜΐ •ώ―@rΥ”•Ύ±υΡ.S³P †[ΣΖw€^­ŸΜ›*έE—˜‘7λΥC‹1=Λrt,Ϊg²ιUΦς P΅‰7Afυ”φcνα²tΥνWΥO;l ‘δeHžεΚV[ΗꍅfKΘ‰?r·η³B2ΑRϊ%έΘδx©M¦ˆ‚™Ÿξ:δDάξΪΤRΏ\ΩΠp£Ty#κ^XBkl‡hΥ‰ό[ΈkΈ†žLAGbπέλ©gμ"+c{^ƒMε=3ΣΓr͎0ήkJΠ’οm/ο¦lΞL Jγ’Δ:L5tΨρ]v؞&ζplpσ·C§Β_ήp9ωeŽ__ή +ΛύAήωΜS…‘y4sMSlλ Υ‹PW_ή7Mε-gΘZƒ†\Τ½ _“»T»v Λ‹ +ύ œΣs…ήͺΫ€^nŒΛbPvMυογ”ζ*ωˆ΄­l­ΒέE³7Do—|4κ-΄Dšjΐ-lsΗaΒ·ύ'›bΫ/κUή\cρ‘³7œG1ΛP£ΕΪΨξ«=Ht!κ£ιi?˜~έψ˜8›φ[ "©Κ)Pƒ‚Π₯ίΏι.­"Ξ‹ζD+8Κ‘W rv£šgΒ§"ΰDέΚπ­’;ϋΫψ79TNΔ%)m,QŸ€G# ‡O’Y(ξ«»?‘ΫΚ„ηΊ£@θ| „1<ηΣΫΆ\^dίρΔχdήΐΡ½HΉmNχP«ΏΑK’’™Ώ"ˆ1ŠiΣfΧmΐŚ\=§σ½CΓ:† rAι€py4bϋ›Š~qŽT.ΡT3a’8βΝ0ώ3†dZύ/Ή^=1Fώ"x›6¨›τΖjwqeΐŽNBw«¨u†ŠoΒΈ™Φoό"ΰβWγ½k˜½Η/ yα@ފ]ΊΛΏψχέZ ;mŸežΡ2–Ϋ5\ιΪ[ξκ[-c ώΗΊywr P(%Έ6“'iwK ωάm¨κU»nXΑ˜qξρl0`™BύJKz‚UgW²₯=ϋxA>sΧΘ πΠθΊςSŠΊδ»ήCŒ ώWjλ+ΒpίBj[G½r‘=S›σLlx–Ηρςo]eƒSVŠC-`Έx<#ΰΗp2^φ‰σ±^aHhC™»LΚ%Οbξ ΐαΨ#8š­‡Œ |gμΎ,\2Y +u•(„_»iš%r„^Ξο«ΨgξΡ`lOzv’€γG sj]t|.\ι‘ Xw€/ ">§ΌKήή$άΐύLσŒ{νY…‹°)ψΚΣαr”υγ:nς—˜¨]o +HΈ9ΞΎ£Έ΄^tΑ΄TΣTI1ψ&Ί+2'b]f—δWψ +L—‘ra'Žΐθς%ΑΒN'Ϊτ‘1§(Β­φUiNΎww8H΅’X:‚'σσυ•σΉηG+°½Ο1K`ϊ¬­c‹,―sι«#ͺo’ŒU"­Ύ +›~ž|5hΛώΥB­ΊΥΊn~B΅Jv™nњς9rMŸ± φσ k$k€f +m-=Γmγjͺ(³υCΑεRbœYψδLΚ¦εr~ͺΔ›nMΕLgώ`’όjƚk OƁΜ3$GτIΥ‡‘ +Ε—O4^ό០K―βρ=ͺbͺ₯Χ ½0pSSξ‰PWΰ—ePKΙ`LΙ™ ‘²ο@Ώ“Κ‚g’pΖξ¬ΰ}•ύΟ<,Y= ΔκD§3!C:@ ΰ\Η~-ώ«‘›ςUΊk―xΦsδ›i0™Ύ€7”Ο},Μ€4cD5žΰŸpqE݊²xd!έ$H9sςΈ‘\„™αΕm•–όΕι£ϋφWΓά·:σŠκB°ιŠ»%Ω…Δτ)'₯TίG†Jί9£*ϋοN„ΪΊλ_1W‰ν’)sCαϊ ‰ν +{±MלωΨ»ήΰ±W§Œ~€Κ(‰ΐ’—+‚•«έZ«©fΟR1€²?]‚{Š!Αη1.=)‹PD}`* QOΌϊ7GΪ!ΒBNŒΓΠ“D?=YIΙx@-«j"0– θ₯^ε&SwJχ4*9cy(JBeΥμt”IκΨ|u”Θ­[Ζj΅:Π€ϊŠyΣ_~―‹nόύ’H܈t}Šέ‘Ο4"\υ&Λ)„φC†žZrYΪˆ[Ο έΘΫV€Σɏ˜3ξYΌΘ]τRͺΐ―f¦«-$Τ;r•j`«»Βmi±‘f ρWςq…9@=” φOV+qŒύ;nK&Ÿ½ΏŒ²πάΫΞnϊyΧ4yٜk[?Lλ&U¨βšŒIz© ,πJθαΩ—^~šωKσ‘©9Ά«[Hzγ;ΘcV…οԊ½Y†§Ι,JO1'Ϊώι +ˆ=©μη&.KLβμiŸΎ* JLο /‚°@y¨³Σ`βίCTCέΚJ»Š€qΠXΊΧ`N7CΡ|£½ƒό~5Ύ–«§vΑz8ƒn΅;κξΆFF:YΈαŒ +bbEύ…CΧJιpJϊ§Ή½W&|u‡uUθ)³£qH€‘t;n-ΥμΝ'Ix3r n,CwͺtaΰOrΊ$_6›%2>Ύ}rJ΅έJ~¬#ΈΎφή6‘Ž^ΩΓΟ Ζœ7²TcΞμl G1xβγΣθΚGy=Χښ”m$θβ4Δ`‰ΤΘeœ(λ›‘³/›G­6¨2Ιπσ£ΰ|οŠ· ‘Ϊγ™u΄UV/|AΥι–”!ϊoΫA‘°» t!ύΘPiώ/δ/ςˆ>cO¬$nπE$ωuW3„—A§3Η°ΩgN?ζŎΉ˜ΞŒk΄pVΊΩx©ZΩ΅Bυ©/n—’―OΏlΠ9©Ξ#-/~a°-o@Ό8·Δ-ΛΆχ5ηάΰԈοDφjX‡_ k2<„˜θzfΊ8"Œ}ΔΙτžD-ώ@i_Jπmν@ε.Ψμν1>ιϊ€ζτ’Ός‰© {K~¬^W\‡τΎξ5ZR άβ'7JE,۟zJbx4,BTζ…ΡΕ”†ΞC₯NΆΝΠ¦ψ[€™2ήΒ5jVΪ :…½|ρ`A’ρGUnŠKχΎζZΈ”ΑŽvF;}ˆ+παΤN Β[Aσ2ρ­Ί©¨θ£K•!θŠή™όͺΰΉΥ[αOρμ$Ή»²³¬}›φΟΑ'sΎν|ςN sGbG%Δ!isΊ ˜ΡM@]₯”‡Σχ‡Λϊΰ„5-g€UηiOφ)‘Η ΄M§ΙpμšfWψ)ΝΈ€ ƒηΪsUδ™IM¨ΓTlΣ-,κW\²θυΜ’)jι‹F•χ§ύ(@»6r}š΄žŽH™=Gεh©@1Β)SVΧY/‘2πΜΧζ1ϊΌδΐ¬ιγχ)<ί§ŒΪˆΡΩόγFΘυ$]τωψ¦ -IUˆ!Τkˆ( LΓCΓΚ…#2εηdκ Υf{™{΅Nf‹ΝN₯Ρ%5Ξ“6σ¨m^ϊ³ΉLgN>Q’ϊίΛ— ±ΆeΨ%ΓΨZhΎxΒrvKΎ5YΓΆhίδύΛ4‹±rL‘Χ‰PŒ„k*δ+}‰;πI«4.„¬έ~˜>‘Τσ5φ?”ΣȐz5”ŠΓ, μθp%φΑίΈžφinΛ F/ωŽDΛΧCαΐ‘PψϊΏšτe΅Ι•’LΓ"fϋΘΉMΣ^t†zLΫy_¬ΤU©½#ίνΥ­-„`˜Ν²ΘˆX_΄y·ώg_5f¨Κόχ»’§M ,―Ν&RΰΡδ†—[–Έαμβ ΆŸχώβ(ͺB¦'eοeρNT}W’ΨΩI&²Δkκ‘5­;EœfΧ™yμU™ώ±HίC+₯šƒΠϋϊ™P!Ύ_0Ψλ=z42‘f¦‚ρb£ΧV,0κ΄JŠ6*ρΝ0c†εΏ3εitΤημt.iπz’ŽηX$쳦8§ψ ΰό₯!σΉ fα˜sσ)⁊γ.νNά xΎό-ίΫύΡΎ©»D_^5YuυΣc^ˆ"WΌ­Uψw™2’3k\ͺSθD€!+Ξ…Ρ¨•³5ΖcrΌΊωΨΓωΒκœρΌ¦Χ.ΆIBΠtTρΦ©zBγ3*δLΖ9{΄_ !IΝι]ŠίW₯‘aјJΠ’k>πΧ{^Ί;WŠξΝ~υΕćΨ)#ΗθμβώδΤδu*-+’BHχΰyβήί ζhHm˜ƒ™Ω§…ηuφJτ ―Ÿ +/95qιφAιΒ-:κ6`>>Ρ2† κ<φΣΘήη ‡Ρu=’ΈέKw'‡Ψ] oŠ.‰|Σq­5ll+!ο©\Ψe₯Ke-Ηέ±ςrARFE§1‡Ž΅ν_c +δΔ‚’…•–ξ>_ψ[δ§Τv.€]ˆŒ€ ϊMIbΫ 7a-cΕr[Ξ–!;«ΈΆYΉDόΫΧΠ€_ΔΦ>“Ώ(6-?ωΜP_vα»ΓEΒ‘Y₯ΡζƒΧD.ΔhΐœŠT`£ήγ”5fΈ|Νΰ½’+£=ΨΦ†>―›ς81νΜy&Ε—xXϋ³ύέ±ͺΩΦAΒ~x’gTΒ>3Nd¬κ€@‰Ρw%κ)ύ4˜1©#vύžΉŠnφΛ™?ΈΒBβQ­46„aPp™} 5ۈš‘+KΤΡDγ|+q&ΚJ΅ΖΥHσ°Ω“Π,ϊ-…>^rW¬ε‰2·υLθ<*ΊƒΥ* eΊΞGn¦Ρ\-ˆ8>Fbΐ¬Hšλ«’ώuŒ}T›'XFN΅Όό‹±Ώ $‹ΐ"(L΄²[hQNΊ(5ˆΛΉβŸd'˜MrQΗύ―Σ›½r&e±Ο―Xϊb›y΅/0‘"3ΫΟμ‚PTOœ›-Σσς)Κ™jLπ6~;0‚ψ>Miΰ₯ΰT€8- g&g~°/vQ9Δ€-α^€bA΅rI¬*ρύΥμoΰ—ηmζυκ,νw»ά¬λ?ήξ*π"ΌvΗσ•mΪσ?h―Υ:Γ(ΰԐυήμΛNϊ’έoθΞΥ£pϋ•ζcŸλCcut§p²σΑΔΣ£Št§‹Aόκ5ψyΘd€ `ΞλXνb«*ϋk눩ιΎ^—}_F,;Λ„pΒΩ~ΤO"ώ+Π fΌvژ‚θωLZq3Žz oαqG0Ε‘'ɐΗΘΠσjσlLόόz€ ΎcfΙ>Hμf·a=4Ή: +λWΖ±όb£Άnqθξ ‹ΐΏ‚«ϊ`’τ]ϊ‡Κ³Ώ:(ά}όyίΆΗ‚γJήL‘γNνLί7p°qhɈ’ŽfŠΒΧή»ΙΤWξΫ)[0ω?Yt·ͺ#Λ€ζΐ‘ή ξύo’ϋ&0γž•†`ΜΞjςZͺχσ»Ϊd?"Ψ!$…Χ™Ξu +νo€¦Ι/μv™εΈCΈ{άι””α! ݍ;ξ©θ’ͺΖœμDσVΥ³Rρ:zƒB'[εs-δt¬€ΦσέτΠ™τ-ΐ°|T”€ο΄Ι Σnη9€K`6ΔQvυtXž…ηw˜ΆΆ?Μ΅Ε„wυΈ* p–ͺ™²>2Ε\X.wΧκ†ν6–ΏAς +}ΝήzΠΆD‡MN7c©Τέ•»KΚ:φΨm‹ΓʍΆΨ°₯CΖτκœ €υBΠœαRj\vέ§τ$XJ·Φ…ΆU…e¬¬r ‘}„ΖΕΏ±ή’Q2ς- έηQ^›P˜ιOMΫι΄π£|¨SuΎέθž’Ί-γ p•β½Μc5Υ>₯†˜bil£κ SΌψUΑε8έα-~Ι­šUοΏwυΤ±Q:)‰,ΠO_x0|υZΨF©Jγu•Y-Α?)"Ζμ7[8`ΐ«βˆͺ²Ώ2Χ BšΨ9e’©"~‡³LήΧkI3ΗA—)ύψğΌR”g»«+2xMγ¨’°nPV³…CδΖό ε²n‘ύ–v¬o _@nNc ρτž>ο3 ¦†ΦΟ§δy ΗbΞi­Όc‘\ΛZGΛF!”§~±‚—Μ¦'”Fͺ#«ά ½ΗΟΠΆΑnΑ +΄ΌG?œΨ8ΠβYΒP<·Ο"Θz€Σέ’a―ώŒ9E@•.EϋŠ5jm‡φηΊ€£Υίέ†$‚V!JVΉ†μ–q•uΐ6©–'©λΣβb[oIΙωωeΝP”ρωDνξwς]ί$efZ΄–άΆπQεhq·Ÿέb•j‘›€ί‘ΥΗUc“ƒΆw‘U! +fμ‚έωœ•?±]ψyaκˆπ)n߁uΆy€ _oŽGτΝν•ΗξΏcr –ζG:Χ;x›Μj3Ι©,α¨Ν‡ρίiΊ°žΔΏΈ–ΏbυΑIΧOKa ~™ΰξ(ٌQƒ₯ωΈE&zŠ!rνyWΞΝ|5Š|“DΜΊότ–.a#¬6nηFΧήφκΆοWκ ½-±#ίs;{x‚ Π;I«•G9qfΡ¦ šΏΏι!₯Ζp΄Nϊ·Oh/Άεέ ˜ύΜ* ~κρ‰qά―Dι%#+‘[ +<πEΖ‘―d Z!tΕκlεjύ†p2όP…]ΌΫ³ =TCθ]½eθN―ΠU;dΡlΧQό[a2Š€₯Ι +xΘΚ€I²˜|N0 LΌ%„"§˜zΊΑΥʟFΫ^ζ”B΄’aΔΤeƒ–iΰ‡£,-›γim ―Ξ +>20MόΝ δ²π󔓏ΈφθmLιΤf2¦!}–eR‚.³ύtϊ|qτ€μƒ₯ =±F›¬sRί²`O2pυΡeΙӝλήΙρεΙTm:Σ oξΉh)ΉRΘζ/ šO 6¬Ψ°Ρ*±UH$iΫ¬vT@—Ά~₯VΞ?.B:JKRZUc:~ξθx鰐ψ€ ”ΉΰόF£QOi₯•u’TσΚ0{iΒΈ,ΏyR³VΥo«arΚΪiΫΗ’€ŠP«…»vŒΒύΕΩ£/&GjΦ‰ŽΑ υΔ¨€me;ό£±1g΄βH@&7$I(Θξά$Ο]‚χmγ9S _300+_Iβm„‡›ΗLMyzήqηΎZ=ΝnΆή±jwΚu}CΖOύΏΰcj¨,Φ|Jl—ύ‘όΙxsή»t?ΒΑ’εFΙΩLΆzΩΞ Œ¦Ι7& +pν^E†ό―βz‚-ψΎ½ lΆ4–hp uw‰‘Mιw8S +†Ξ—₯›@9¦C*aΊC€wΟ{ρ'uεͺΘnέPΝ“λ“G^‡΅œ4‘ξμΞoΠΥC.c¬@QŒνΒD_C>©2Φ6'D"δβGΥΜψΊ$Y΄‚…ΰMŒ30>%=Ώ@νοt*†HΨ$—0Nή ¬eJFISŸ₯Ή‘H¬―ͺ Pn›ΰ‚xηζeqX½?q©η΅Gι”jΰxgExr Ω\‘Ω'‡β gk{ͺCΧM πέΑΌIπ‘σOΞΤυΗ βγRˆύ3Μ„.‡5ٜE½aοάjά½ˆΣ»mύMΝϋ €Ϊ?~-‚%ιbvƒ[:E±Π ΣψΗXwΌF˜Τ-Ξτ²~ψώΓA› ζϋΠ + +φΓ»ιΛH>ώ]j} #―n}–Σ$₯‘?Γ¦–Ι=’-H+~›‚·IŠZ]ΉγΦXdg„ΘδŸ‡C}τ\Hb€ϋ‘οξ[έ^˜νDL/Š>ˆ$5»BF1_b,ϊΝ‘ˆΑ€Ž§σ0LΗΠ|¬RΑφxΪDθhOΘ.±·Ύ(’?ΎπΚWέ°Ύ"„KΪQ½£<ΆhόΆ΅^V€r΅’¨,~»(+>+‡»EsSΤ|)ζ%ZŽgŽYφye’³”θ}Ζ™αODςΰό[ο‚–_μΆκ9` άΩašΣ8ΆM©€%Ξ Šεލψβ›ŸsyAݐo1„E¦EΆ΄fšΟχ€ΰΘ7S;ΈqMJνΘΏώ{{WtΛmΞσότ²Π^φŸe›}χΝΚz4mΘσ¦υFα£π™Φ`8_IΆό[‘Ÿ”"–ΞIdƒ†C@Aω~ϊζ\“Ϋg΄fƒ₯ žω ‡¨&ˆJςPMΓΧΝ[ϊ5w«ϋ–ΓΚ”tq_#‰°%hέT%ˆ₯‘UV7"ΛKοςzοπt‘Υhζ‘RUΥα #I‘­cOό%ίή›œάΜ0qn~u[‰YAuΛMޘsDW΅C‚ΌυΧz―Χbό8˜^pΌa²ΖΫ*Ι zbL‹dΤ,—9_ψhi”}L!7Φ©ι ­ςsωI tZo_ZŒ1”[έC–3dηœ(δ cωιξKBS“2ξ2KΡΩ+fο$Π‘` κ½£‘ΈŽ{ΜΖβΫΦ„Ϋx•gΈ²³|yβθ?P#‚wΝΥk ΥXUΖΥy;Λv?JΩ>όπEώ*@–PΗCύλc·/‹ Δώ*SΐφW³™ƒ–€θ”:”Ώ£6›±ς΄±Š•ιύΥ^*¨πσλ3mKf³Qώ HoΈŽΡ1’ΆPη[F`T3™Γ1©r«h0Υ6βC―ΰ-³ …μ‘|m¨φ$νΪ½šΔ‘ h(wΰbέƒhA¬9^W)Ύw^td˜\Κ&˜ί¨ +Ή_k―“;mKΩ 1T+a5“–ICŠXΤ)l―‚*ΩΒ(•ΚWI»KVrΨΦZΕζ…`#ύ›jz¬"δα ϊfeŒUα™β΄DNŽ λD½ƒRxαDωόa?zP!bY#W™οβS(ƒΆδ½6ΜRΐΑβϊ묳λβHDΠ£Ο7 `τ6ΚΛ±ί„Q­(Ω·τKΜ‡^ŠZœ² ­rΛςέ Hiήz 7+l0―TΫ΅$Mdγ\βSΜ’Θς ―Έ +©œa1”όϋξŒτ(·€ξd4ϋ1½ΩM’†ο7Y19yΰ†eVΜiΉ ‹9½!‡2˜QΔhΩ©Χ,ώzΛ¨fΐf&„[ΗEνΊ˜Υlœ‚Ζ‰π₯΄xέo$zό8koΓƒQ:nΫ¨›έΤL».Ύ}YŸ‰3»bš + (ΰ&ΰœbςοh€΅¨qVΌ0?; u—C†ŽPΨ±œόiy½r„KΩaC/u΅η0yχλˆ\—δZΔΖI? ΌY$° τΙΆ–’ώ^= ͺΩKΣEg$±―Έ{Pξ_Ί +Y°—ΊόΏSό,%.)X•έMz‹|Υρ:œ!‰τ¨Πμu4<αΘ³R8ΩΉΗδ}LΤς{πWΘ Ψξυό°ρ5^I@ζ6VŸSαKHγώ'ΓrxRβn JZs—ZΤ(_8<ηaΝ…SΡϋ u«Nφβυπ‡ΨΞΗ6ζtŠΜсcΨ‘π?άl¬Π°b­3‘+ΕS<©F°μOšDb―Λqmx€o™έD'+ςμKXNmTa) yC8nˆ*—mΰœͺαQν½ρ*Œ[…5@μP³Œ( TΡΐ«γsYŽD—U³{w―υn‚ύŒ’‘wέ.”'…φ»Ψ@œ|oΔvΤ¦^Q,r‡oΒz”΄’;€ί«΄λˆΉ―)ΰU„lΟqΣZœΙΒZ“υηπ—ώ«ΪΣνšο₯AGF2ϋοŒC4M$€zbEΎElbΉ«ΰ²άI `yωc49††ΑzΟϋdΗM₯dύ]kPΫ„±0`—Π(& ΑxκmΥVO +!‘0ΤDh‘«Iώx@ΘY "U£Ž«rS­Π2ϊΟͺώ‰κBι“1Z].9o€Θ|LSΡ‹ͺ[ Ɗ­²”ζ4Ά ‚5^Q:#>Žφ΅¨Υ¨k¦ZφΙΖ2ώwCTS8Κσ ‹…Οˆ™•Ω4΅ΜGP!μ©δΌb`[nΤ†Q*SκύΖθƈmF«:ʍ÷όZ0X©+2ͺ xωΪθU"Ξht°L +3—£(]ΕεeEo7`4Ώ πͺŒΙ€ˆ”Q4λΡ‹’`‡θAμ‘ω\s]VΡ€»‰kIP·€Θξύυ.±έΩNvρ +›, όζ”}1‡Μ€›ν0Ι6€|:•“©‡"J₯7ΏzΨ΅QΥΚ|•ψ&Bω$̟’.­Ε25MVυMHίͺ‘ L"ψ£Α•ν·ή…^πŸ·βh‘™θ–Ρo‰σΤΑ’­i|ƒυζN§K8ΈHΦ:˜°:†Ξ‰‚mΦ5mΖ +ϊUΦ‰ k¬–‘λfϋl0POΊ:¬―‚Pm;)³ %ψΧf [N%§{`jF‰lU-E€iAυ£Ev­#‹f%r—̏wnm»2ΜVλ c‘ΕΈ,§–-έφ +ζΖΏ^9bz=iμψβN~œ±Ήj>Ρ3vͺmίdΌgϊZS | 3¨z—rΨ•²RΝqʘž„WP-΅χKωͺ@τuΉe¦μ`ΐ˜ 6Iw3LXΣ†gχ·ν.lωpσ_„σ!έXΊLc²W¨U‡J`Ήh&ΠύφŸ3.€ΰ€Ώ]%΄·p ³œ>Fm_¨rppΗΊLS@ςŠN τβ‡–0{wKΉB_VΒατyEΖΑ KΧκΈu`¬Eρž΄ο §Q)hs.[σ±HήΣμIž*LμGpe(&{јΚ—ύŠŸjs’Og˜λͺΙPχ{ά·…;A”EοΠ{ϊڏΕQΓΛeκεZΑΑΛΓΪ€ƒ¨ΐΎΙΥ‚ͺΑcΟΏp­"†©Ϋ{”L©΄ ­K.FζϋΈ™½zq“>`¦³ΐ»‘­ΆΌž4±Δy+D75‘bαζ«©^dζ-†\hμŒ'Ηό8‚φL`J‚Ξ€ιzΈ»½ΐ6…πίuΠΏJŠΩ§{“αyΐ³žδ(Sz9λ«λ@Œζ3aokή£?ζΐο²§υCSdn‘„4c΄\b­ΖOfJκΥLτ¦ H+OR %ν±ΏZ¨hPζ²ΜwΗ[iΫ*Œl¬Έ +vYv~΅ + Y|QkKμΗΑΖaΒMŒu θ-Ζηp#λ—1έ·θF +(ow\ŸΉΉ’4TDΘίkŒ@ 1ίbLγŽΘžΆ»ˆΒƒ/}ώΎID˜eΘQ^OΗ$ι―ΔΟ+vڜύcΝL§C‘φœhαY‰j44―4»ύΩ³)0Σ|ύχΟhζ €@4Φ6g€>]n&%ʞMFŠΌα] €δgyCΏε©ζ¨%Šωš_ˆ  “₯³γpϊ_Ϋ’uΊƒ8qDΆ‘›|ΈBΟΩƈEϊ”Qq.Ν―lœίŸŠζΛμw§+h»#.kBέ eΛ/‘Mޜ#SΔW½¨ΫtΧΌ^••/φΊΑ[ηŸΨy,ζˆ“ Ι$uΏ9Ε§ηˆ!ˆo½³’Ο/2Sjš“„ΐ{tτσ`wZpP‹Α3ϋTίΈΙΪM՟-Š\Ϋ­)ΈΏΗ6Ώ"Χ/ΐψU“’ ΎυspOUBJυ¨XΡΜ£¬Ύœ5ΖΜfx‡1ΟOΏ(ˆRβ·έ(Mqλ’±IfΒN_0―©=ξΏ|ΥμΰmoϊΞz²ϋΙ΄ω­ΰΊ°φΈ>01ΗPΤiA¨¦§‚Ή£Χ^+‡τ|l¬s Ζχ:4 …θŠι7οΡΦΣv€’2)?ͺυBm¨άτ§Hμ^πτO±_ΖW°•ΗsM„` Y”αZn +7¦2™K°/₯} A»΅¦υMπΨ ¨νpξM*ο ‹_|g2§τΒ%ΪΣπ άυueͺŒπzŒ₯’_·oψρ °n¨©wrMΎΤ"ςΦας¨ +ΎIšΌ…ΜŒ|Λ‘}E€œk…$³δή6SOkˆ5YΨ/΄ˆsκ²SΧdΥ°WΐΝνqδsύK0ψJ"·@† zWTΉο ΅λ4TRIβιηΛΥCOΓ_Γ9ζUg-ς(%Nφ?έJ2UƒΩ$zξ])κΟ₯ce€Β‰˜3Ÿ•₯Ώjξο²`Ή–ύ €ƒA€…ΌˆϊΟΊ€ƒVΜŠΒ„ hΒsδxώrMy–XΑKςhk 5†ˆOjέV%[z2l/ΟχΈα*Γ₯WZ03}ψΗή-γ&ΟςΌ΅β^T½+L Ο™fLΌ4\zz}Ÿ&}L°€8wμ&†©΄ 7ύY­+ΆaΣ+οΧΛΗJϊθtΨLΕΈΤ½#L2UΟ~GφΘΛ¬sΊrφΜ‚!z˜2΅ϊ­Ύcά δκŸΰΈΜ7”γΎ+³…Άκ™Νȏ€‡yΛS9jŒO°₯4ώίTœϊΜ œV pτXτΈάœo’uςωd=Y4Eƒ0?άjψ—ΒIП°ν:‘8tΛz Ύθ3ZGqZiP ­ j«ΓΝΓbΞνKŠ­jΩ’j'$ά™5₯ o[>y:'F +‰σδ!άzΧ‘’FΪυͺ7CΤ*ς†=d‘{—LΜ\›“Ϋό= ͺέ,ΊˆTηςLlja:8υ3΅ω•“~ΖπΑΫφΜ ‘°Š―π¦›·„ŸRΐwX(֌ +’Ύ,•^vπγ π³Ψ&::ψLZD™>ˆ}₯‰LŽ<τ‹χΤ2ΪΎΑ:ιf$X[8±1Rπ™*ΠhρΌ_Qˆ"iH«ώˆ/€šΓ8 ΣΩ7'ⲫΒlhttλšͺͺΞ/g° +Μ4ω,ΆzΖΪξoΑ 'MξΥό:}|:nφΨAοS[hΰ―p‹7½Ζε^› ΅ε +G:l‘Ηε‘‘qΠƒKΚ) wyΖτ†ήUΛΥαsuξ|›oΟ™ν?νχ:mj‚ž·#."Ν"W­”0χθŠA‚Πv0Kϊ£pχγj9ξρ¬Όα‘ζ%ΚΟzcj?Υ{ΦΟHff}ΛΌlΈ0•/_\7}D(„I΄₯Kψ¨_Ν’:N}ΠCε’›ͺ  ‚‘K’7šΛ½“IO˜ΕWLυΌθηŠ: ›Φα%Dΰθ₯uΆ…Šš™tŠj:ouŠΊ=ΦξφV,Τyερ¨>䉦½QR<V—υ₯‘]—L²K›ό•K+ΐP£κŠJŠ2ν(φ\œ«d3ΐ¬<α/da±x’s‹$šu™μ¨σ(Ίκ%—Π‚ UΰΧσ ‰"ΘόmDr9 +Q„3C\ΫOgDΪάnΉτ’ΈσΪςr›Τ. +ήΈ’DK¨)μ(aš[Θ€t–hΝYκtΤΦΠGqRζ6•‰Kέy»]–6xόWAθ6)‡Ίβ“ωzΨΈe–lbΈf^|ΓρκΉ£Ωϊoœή³F\φ•1‹e=!Cr¨y¬˜ŠΞ1ŠS‰hΰ’ύΦƒŠϋεߐν…κmdD͏-ύ>Yw:’°hι™ @£ƒsk‘Žτσؐ7‹΅퇣50&d&ό7Π·θG’Βšu'Z[Ή‰ΘΟ1 +:«|‚β ‰Σ„ΗbΟ3¬σymoΨ˟›έdš§zR +Ωsύᭈ­Nœ6‹v5‘|Œy3η2—iΞ2:-R‰E§WI6E±Œ:ό»χʁΪΜ¨δ<Ϋ&7’ά*8`#?nXΤO—δOl,T‰Ι [3’Ο½?«ζo!"d8…Zβύ–Λ΄²ϋ.σͺ1tH(Ήp v ²˜n»Μμ`ΖΪσπ εFο?1“«Y‡wϊCάΟΊφυ§1—ˆλ₯γΣd†%K]$ΣκχU@Κ“>Ψ2x^4Μξ€·HŸΑΓ‡`“*νϊ¨rά?Bl[m«7©q7φ›2i₯ρ+θ2΄§π“Υ¦ΘCǏf‚\`¬1ό·ηδšΆϋ‹™xK*ežλ];eM#’’¨θ‡gΙ‘Ϋnq R ζ£+Œi/½WQ^ζ }9•v…‚ƒ|?η~^TΎ\'σ§DΫnωRΰκwoχ7xΥdΗ"­έϋžι‹NW½ Πzsο‹|YJTΛΝƒtΩš)ͺ£ΖPέ5H 6ύ€©$±ΰγπΆμθπ4Κ"ΆέΖ€όݎ$! +ΰ€ρ-›αo…ΦGλLlΤ¦;ΗXϋ‘ΰ‘7nη!Φ#Έπˆ–y€ν šr³un}p‹³ŒΙ.ώς΅ŽRΎ†Ώμ†~2‹σς*½σδ0zH£^Άτ’ΐUόty§έο$βλ¨e,y>PY-_}‚rΊ-…7’4$qΐι›){‘ζ₯j2ΏΣθNl!€Ρ…T€x }οI/oΥjΝε`χ.σEΖɞCΘΈe”;•Θ>fŠ|>Ι@‚yγn"NKu4Χ•`•…?Υ" 3ςΜ±D‹χ!9”;5ΕΏŸύ>΄e»S»Ιr²{s”άTΡέ7ۘo[€HΝ£»’ € +yχSŽGE;Χ€ΒΞφ°ε5=Ωπϊ}χ 0πώK|Pό κτξΎ),jWs'Fώ0ώχΛύχŠΐσD~Δρ+*κ§ˆ[pZcφ€ + BΆί•Ζg¬γIuΐ3(Ζu<€υ „ Ž"ξΫEέ0δœr Ό‘σ_κ£7šΗιUΩ#ΐYVΆy3nΘwΒΡ K4Άg&ۊšΰ>ΐJ°γϋΒα5(κΑV–<τI"Ζλ4Tσ²os…β,SΟδΘλšΤQBΠΜFf ;¨‚TΠχ»ύΠ·Kj…ηθ¬ΰΕyΠχό:Ϋl"0~ς3ΤΤfε${MUΫΌρ‡0μ\ ή+(‹guΓΥυ?=ύύ‘{@Ϊˈεd»wτσϊ%( ΗRθ―δ~?)ΰ‰ΞL?΅ςCXϊκC³D:πλ顟8XgNί $Ν5uhΦ`2ΪοΞ²ΛΑΘ!χλPAΠεşΜMη ’ +ϋB‰ϋϊ{, Pϊςsg¦εψi‡ς ‡t‡ŠΤr)ΝxΒi-„© ϋBl° NJfš™Ξ™1—˜γΩφΤΗβƚ9Έ>rl»™€ΑίιΎή#κ:Ϊπl[«βΉπ<|ή‘d ΪΰΑlg$Ÿe‘2-τ†ΈέœπK^Άκ‰³·pΥMλκcΩ‘σζιRπΊΎ–[sΙ(΅½ι~«~0=+­yŠtΫu€XΧΜֈ€νCΎφ"^±!Ψ«ž(Αco‡Ά±ώKulΪ­ΈΌκΤqζ)α‹ ±Υ±kδ‡|Γt‘k…> ‡ŸΠΡNΦd5s¨N™H§4ΜΧ+εξW-k΄fμKΙΑŠρθΧ ς£ͺ.L°Τ’ϋuφQΞ!ŠΘο-(«°γ-³ͺLŠI­§?ΎŽ‡ύlqNΠ43ΠΥ©²„ε€c›Ο—τ₯…ά«‡Όw’¨ΜC;˜ύκ Iάή―μΊ8ηΊ@ΎΦΔ?Hχ0Μφ?_*ǐΕ# m/ —ΕωΔhk‰ΫAΘήΊΥyέ Π³£’†0½E«Β΄-ŞSW"¨‹CΑβΚk+UBPΠζοΜύ &π^ˆ2½ε™πp£107‰πuUΗn`ΡWΝ$XŸ ν§r‡ωHυΗ‰ηΐ°·•Ζ·€-Y_¨Mbs(46›ΙνΠeQŽœ2P{cΩoπCΜuε>}α5¨δ¬w›vMΖjBΎ|’ΓuNμmo3ΘyΚBtG5z5ΠVΊhφeg_XχΞN:Ά=?nfό“[ΐš'(Ξo8C’ΡzŒ±€TΚΣ! eKθΎpdΚΣ Y•Ε}ΗY–„ˆΈωc8}ΫDV—‚9]BR‹fUQΗR²τςΤΡx‚BS-‡ez~ςbεSjΉoπ cR2―‚5†xmήBΔy+y«* ‘bk_·B«mθs„ax!UW{‚VΡλκr6Ή›ISBωlχώνz‡Δ «Οχ§œR€ +xcδ !A€»γ‹!UΣj”ΞΥιβr€πνvt<'΅#/$βΫghjgœP °ρίΣ\°Φ¦–νκZiΚb`–`JΔ½ε+˜Ξχ¬&ξJT«ρΦχ2f$·«‘X_ +eΜ0U΅  ν’½δ“‘=ΙΛχœ©k‹lβΔi!wχ‘-묛TΛ5ή^Χ ­z©°’Υs Ε`N 6+ƒ%Ο΅ΫP{qΊTΡMί«Ψ‘Μΐπς tNΘ£λU|υ…±ϋΌͺٝ&`›ρϊγ!έ°R*XZΖ",ϋQΩR˜ζΣ; 0ή›ϊιΞΠ΅r‚yώ―žHjQ+’‘Ό^ K•cΠeŽ8Š3ΧγΙT·Ώ΄-ͺQo?£ΧΩ?πΥ|ۍ@NKυ8 dTρΔΖeς{v7χΌΝ7δBφ`Ϊ„ρπA€9V‰Šœƒ wۊ£„Ktn›­E΅<Μ‡|#‘Πτή€[mΞ~»Ώaυ§œ-€£„†ώ^'’™‡ΰtΑ·9_OΟ“Α’0\γΛTD&?9ϊ;Dpͺ/ΙTυ°ύχ6 οό%°ςH#ΆλΗΔ7fKρ1v¬ύ–έαk–pKvΥi€Εgiλ*ΈŽΆηΐ>ςο8Κ’(}„·Œ–7ωήύ*ΡH‘Ά4ΰ₯NXρZρ+ΐ‡ΘmΟrτw˜±! utΞ§sF]κŒŠy5|€ i„šΨΒF±Δ>0S’βӎž–ΟΓ7’θ4@›α1ϋ]ωJΣ²‚σYρ€ΌX='%6›JŽ‘ΝΎi©4A¬Ό$ΣΤΊ‘VΈG¦uΞ#΄£ Ώ‰Sˆ»‹v”‘@‘ JΥ±$og‚ϋ‡΄0ΙV\m"7‘[Ί˜¨Τ°g½ΚΌ™$(Λ£—¦Cd3€J‚>:KΖXȚ1Qγ‹ψεŠUݐϊ‡T«™ ‹₯*v{q‘£;^dνͺ'׌φΕ@Y3χ‡‹tOT±ξm²ΑW’‘Qo’1γͺόRΥρψ-\‘ηε₯λΤτsCI‘u ߘ)NΎOQ, /0ƒ\TθeΥ)t£WޚWΌV° •ΥΕ°μΠ0ͺB…aΖ\4ςΐΗφ±.₯%νθΪ·VK`NXϊ5πkQν4©34.*₯°δεν³8ErFΈΏ₯Lq[X©Gc °eJ‚)-ιιά©ζtκα3‹<ͺ‹ ΡQN™Ή2΄Qο%KE*ζΔ&KBΈΪξW¬<NΛ¦―‘«1 aώˆΥaοΊΤδ™[―|uΗΑ³υ:Qs0œqT Ξ?o¬ϊ‘xxCLl˜Ε₯ОD‘.a +°:ΊδζfF“δHμͺΈ«dη†IΚ_ das»‹fΩωKtwŒ>E f‘%ŒžΛΔaφ³χψΞ™oŸ oπ6 VΙΗ%ΝΫC+Œzέγ“β+nΏOƘ½\²ΤΆοh]3}&’ΓRK†’"yΘ»·Ϋ΅:ρ`‡ψζ+p<p'ϊ’VlΑ<ε€ §"»—ƒVZOψΙ KϋSGΐύ4“…ΌΧΛ Š+LY3“„‡ά1}λΣσ²ηTΗΆ paΗϊάSc[|DVο}.QΛ’—D¨<ΨΗ]ί¦ΘI3*^ΆžS>μN3mΥTΈL’Gλ‚λ‹"t0ύW:πg‹„…ΗΓgεSbWΘtw‘α½kυc±ΨΩΐΓ6βrk&ϊΌΐΛ…‘Ίž1u³vh,Ϊn„(⏠2G°„¦ΫœO΅ηλν†0TφΩ"χˆΛΌ™ +Wo>²6₯/ƒξΣm .-ͺ†š5–—v˜·«υn Άƒ9’Π8·ΧbΜLͺŸ—p`επήyό%TΘΙtψ‡nkΓντeόkdβΔ&ΑΚοσ¬ζ‡'OΤnL™&€AλgVjΙ±†u©—†„s 8%ύ££αr“ώυ_RXHaQ&₯:•±Π—%βu¦—A+tWq4;β Ω±ΊεΨ~nƒ=ΝεΒΟγ'΅n * Œά«\εX0ƒpύ¨1–«ŸOuίήγέ?Ω&Δ€Ο9Q-IΫ|ΩώEcM^ƒόSL‡Y— %«t4^­ΖSwΙ»ω£ULΘFDeDmŽόv³ψ^Ηϋ(Μ ‘˜; c€λ¦L Y;e£SŠ€–ˆ9FxmθμΠdΖ€†Ίυ¦€Ύ…–’:tιΓkEί}·ήΖ}1ί΄qρ·’Χ™…ιέΖΊCλτ/Ο%3wH ƒΕ,νŽόH~cάΪ5Μ›‚CCGh~(ήZ?νH‰Μn‡.ΩΫ"54”΄­`¦k… xϋι>M Άθ–ˆκ k±Η&šΐ*@ Το‘R…2κ #(KσRLQœ¨ŸeŽϊD4ŽGΊ΄`ίΑ’Ȟ ΄Uμ@q ν,α η7ƒ–D^32gFΏ0IH‰8ŸpϋR|Γ#θAFˆJŠΈ|ΚΣ³0JργνuzisŸ8FOpνΎHv€΅Ψϋ\β0 œώι>ZΗ#WΆ™τgaγ—Ρy8ΌB“.\][ϋ-YΞ›WΝbψŸ 7 ΪόŸ6«‘‰›άΙMH_bn<θ*ˆ΄_NΑWxΊ(I‚—‹ZτζiwnΆ’Αετπ:fΫT©Ο P/D‘ΑBTC‘­ 1#iώ”`ΏΛπς™HΙδΨex&Z­>…Rπo[ι;Ϊ/”†ΓΩkΑ‰΄œ^œΝ/ŸY―ΞAώp49“υϊ!GˆβΠ€|ZOgbE~ 5ΰw…lŠε[M36qσVΡ6θ25žΰ`ΔψΏ7‘P)žΕΓ9rήΪΜΆ8 ρŸΙ€¦HΝužσFXo„ +7ͺydέΓ4@7(΄¦€"!²τy7ϊ>€άSX" Α:πš ”Ν=£Ψ‘hsrΏ/Ϊά–o„υ2ώΩz‚ϊ•h~ θD³ΰcœι~Χςu’™¨ΤΙΓ<Ό)ΐBΣƒˆrΫάΧ9ΐ™Ž”?₯ e<ϋ]dE“X2‰υΩ/ €ΐ˜·Ήt0΅1Ηkˆ]’E!θϋυdΨ…+§ŽΩ|s§EΖJ“ίG§Xu7Œ1¦bι<ͺW’σεx/μΏeΗΖϊd΅ΧZJrΎΝπ΅p£σκNWξΜΈ-fζΦ䦫iρŒΏo'“‰'ϋCa<Š!κ”υeœ}Η±ν–}ˆ$‰΅Q²ςP$žˆC`DνN]£Β€ΈŒ€ΖΪΌ}3Ι ¦1?v­Ζ’#$»εΉϊΚ“χ4(μμjtύύc WœSL n|l૚Ρϋq»Œ§l9jKΨq–ζΨ'~¬Ί@΄©!Ηo)JΜή-k@n[ό–zΕΆΥΞηΧnΟΨ]€·Fžm» αγ*‹wqmX€y~iΘΏύ-Œω(Y=k;?ž X,]3ϋΘ–ΩΔwΖ„`%tˆVζ[’e`Π͜B P88–N¨Ζγ`ϊ 9έHu₯ κ—P›πςŽXV›ϊ΅ω§;Ο0ν)@κΊMί6=w\λ½; ‰9&²΅•)ΨΏ)Ⱍȭχή΅ˆ8;= l竃䛱L p둏?-ΰ 5ŽV{η Ί]Αz…ΔΔ1Άτ€ΐ†½6ΏΧϋ€ϊ•ˆEi‚Ή\» =ίgHWΠ9RŸΰϊΤC\>6HIΡΡ¨NA‚Ϊ₯Ήδ iΓbtσα½ΫƒΚy±Ύ_[_:awό0žνθΖ ?oΔ[$8τ2’>u° ΛϊΨύ‡γ&ƒΩQqΥΰ΄υ²½νζ/½N!-έ[…=\g«βυΤ¦ΛVό¦ΉLx ύ£Θ{ +(n‡lƒ)!Βί©ΈŸχ7ΥαFdζ.ΩΣŒί…Ι?›ΉΟπYγZ–7ffΈ~ηΎτπΚ'/@uW’εφ} .<&^Δ6Ζ#οm]Ϊl©usIH"v β~›•ΝR?i_₯Ί H‘ήΗ€’ΕJχ$ΐdΧΗʏYΚpk“]ˆ™”Ύˆœw€Ίp5Ρm9sŒ‘<.[…^"#ή‰œ~ŽŠ_T€1`?ιk0Jξ+2*=Έj»Ϊ‘[Πiτ<ϊG§g BŸ iΩ½l·r*>YSΏD HΐόοKŠΤw™Q¬;^₯b`μ°ε¬Π?ΉΩΏΓ?ο»F΄}“wΆ:rk HΑ.³ Ξι N«ίγYފŠφάΑnP/υ)‰MΉ;Ν–N‘ͺφνrf4Χ3ΟˆπσαΤ ΰ›FŒ–Zf¦Ža„gzΫ™PΥ³Hνέ€ͺDφL†Lސj(δ&—Ή–3LΜ€nΛκOν­xBX˜Ωρ ’φJ¬Ι„žζΟœC~΅EMξέ§οfN“Eξ=-ιΈΟ^kηλΩI?ϊ˜ΫΗa€\[₯u,7:ζv)ž·<ΥσHσ#Rg²Έκnn:₯€αP)Ω *›ς-εJδΈ"™Νxσˆ?Ɛ­Φ™Α’™@_€ƒ0YΡ‚Š +>ΐΟ'͐:TΞμ|.;ω’)1,W«•₯ž¬Ν«  +~Θ$¦qCΫ Ε’†ό +Άΐ^AΠΖ¨…mΞ[hWυυ[u7§šφš†²‡xJ•Š φ­<Τ ϊ7u!‘$’4m‹άtvžcηέY7w€ŽJιξn,$C$OΘJŠc‰MŸΑΓ£š½Ξާ#Γ·8X΅&Σ́-&θε-δτ³‘S½X›xη˜Υ ΰ€ΌaQτ΄ƒGΆδŽ Η»?νS―λe?cψUZΰ4θ‚Χπρ-`v9—­E4Κε}z]χ€½Ϋ΅άΪμe%ώd,Ψ t†ΫΟV\ηRlΡΛ΄ΫŽ@cΟ}^ŚΆc*0πv…fC}8 TcσL†”"9’g•—|CΔ0L/—BsHe-ΚΓΫϝόκυwŠύπ>ΓrkΞϋeR).?G€lž’qξϋύ>΅ό%lίC‹5h’¦ύΟ&A@a8Ψΐβπ#ρžƒY3θEΣdΊBd½Ϋkͺ’£irΤr ;˜5οίΕΎφβž Α₯6vf[o6 P Œπl5Ώ€Z ŒΒ~abπλ¬N°/…Ÿ‘9ΣΜf>“Q耇υ€˜%_FΑU[nlˆίχΗ­μ)δνΞPΊέ/(Uη¬CUΨA –WιRZi[υ’ψτζξϋλλΔΪζ^²½KoΖ#¦n +Κ¦” Y)KG +Κ=σ·­™κKτ‚M™Ξ‹"ΠUHΆ·ŸQU‡ϊNφ‘Η-ςΈ&Α‹Δe+~e―j~0 Ύ „Τ$0–\Κς―5#Ί2yηϊ©e<Β]Ε#)ˆQxV•Π;΅x5ŒRm³μ°οv~μμΣsŒDωΈiύl|m{SόΏa“ε!ΩBφ,ρχκ€ŠΥ +\]l1S–:xξΖ qx™JbΨοuO5@C a'Σ»μˆΤΦ„’l ά~Άο7a›όBφg!†RXΜζGερz'²}Όοτ”›©uηΞ ΠX€υJ%> ?3YKΒε « ύη½=χ؎Hφ₯Z + ι.»&“¦>ΜρΠb»1Za7ωω?fLls‹ΐ€DπgOl€Ÿ·ψQ£υλαOύN/ΐLηΆ«ΉΙNˆ₯τ½gΫˍ +ΏTΚ²˜νdώ―γψ<Δ­=–ͺώnŒΑΎ…47uXŒCΡΆˆφ!ύ•βω5>z‚€πH¦tνC?@ΎΨ6½ Ήο‘–\o^s€Aχ€WŠj=€ΪΧτνbqςΛΞ!Z[¬ΈΏ‚Wη„Oo0@“ ʌ‘}ΰΡXΉzU–bσ/DšΛ#³ΓJηŠ(KN<€–ηFvˆ„ΠwΙY-h‹ͺψG‰AΝ{vAGόζ“0z§e)”Κ›+5Ω°C)yEr­#έ©«&t7{mBΒ70£Inw;[lžš/ϊϊ•οcͺ.–eκΆΕm‹WvS4¨ΐšGmKWφ唀Oj†Ύ5ηz=έ0ΞU{!r‚‰&}L‹|ζώθΤ‘ΌKξΗiθ’/$½q³œhΙά0" ΰKΦ{‹d/YUήΩΟ™sΩ/„\B&v΅ύ¨ΒΩ…ϋ3eΖ·©£Ίhδb˜ $ΎΪΚW²ΒωΘϊ€–©Χθ_©PTg ΠIαdζΧ0„ZQΆŸ™Τ€όαΥ]a @gύ+|jψj’DV‡ύ²Am}²’ LŸΫ[b;μ†0ζ’Θ= qχ‚-Ρ&%φ\εD„&ƒ¬ήΌΊ–²_CyZΆ £YώΝ“sμ²-5†8˜tmΊΑ/N6–€/Gη§P>L©yˆ·7Τ5@ψ™gJΨ–»Λ΄ΡΘc ζŒζšΑιΟO‘W‘ό#dΚ—ΙΰHύXωvcυvπς¨Ι2\Z™`ΞΐP,’R„»(#)χΤ©₯₯eΈα*ΓyΚ0πG`œq»œ^ŠjΊfDΐ‡.¦ΪuΤ“ΰ”57'9ˆ%ξεaAΖHlv“ψ4¬AΠjδΟ{yP/ά5 ΈΥs4qο„1 ‚P‘7p1C;…HοUT~ž""UΦ‹l|ύ(hΉ:Pψ@˜τωhΌ±7γ"kwα©gˆ=αžΨkvsžEfjί΄VψFjφ uήwσλΖ0Eͺ²0΅7Ύ\>ŠOOY‰±ςEΌφΏA¦MB׍0ό'ό΅w½ώœ9L1w'SfΗ‘ΔtΆ­Ρ p-œ³Hυ‡‡τΘSίH-ˆcwp£Aއ†―†rZ›G‘1ίTΑTπ£G¦ Τ‘μόy'0MΣzHΧΥΉ9›ΗSΨ!Μis΅¨oP₯ݐ`Τ%Ω‘ {Σ^ˆh8Φ―PBpΌ΅vΤrF"^Λ’¬Jt5eOFjη΄σοŒ3ϋΰΌΠ€¨ΠEώ‡+wšPWDϊ‹SZψ:λ3ΐ\}~$>86₯KΆΈ•oΆwΗlΦΫ½Μ$žε,ΒΟ°±z%Ϋf0Ξ0σœ<Ψ¨KΛVšpκ)ΐ΁¬ ΥΫσ‡5ΞόQw0%mβΗΊο9έ΅–ο’ιGη_>ig­€’›ΠU‰’imb―­n—Ε:05θκ nMΟχιJTΑΈIΙ ·εΏΩε„ži₯P[ˆπV‘)=—AVuη8‚ζΛ–9ͺF²Ώο*σEz5v=ϊ4 ‘χ7T₯P{³ζϋ›…zo A'_ΊΫϋE²¬ΔΔΖΜP•s„ζ{κ§xfd΅ϋqLƒΫTΊ;hλ›eyϊK>«δ²,v‹5]υcΔΤ(¨:`ƒGΣΝ<­ˆœΥ㍉ŠPƒ ΖΖ +ύΑΕΘ*Ι²ό›HŸ=©ȜΣX<Θ.ωΧ]uήχ=@νΫ:}22ξΗmΏ=šΔΝ›ι…"ω·J +}(f?$όλΥ‚Ea…ώ/©€N£΄ρ0Θ¬ήχΦ‚„AιζͺK;Ο%Vs@Dπζ­ULƒŸΩβζŒ0,˜+~)5/ ψ«L³™τ²&$ψ½ρo-ιl@―μy•†G† ["ηLYexΫ¬‘8²­ΑtxF!3[‘ ž$Ϋ?O¨”‹zͺ–μώyW¦ϊ~T,a†HNŽQz“₯QBcΧ‘ŸͺZV–:•ΫIρ2ζ„[₯I ν~υ#η§2ΩΙςžrΫMLΝέπ`\Sۊεk~¨yΘ‰½ΏΆ{zP‘ƒO–ύ(Q³ς'y ΧΎ_Zθΰ”š”σUŒϊ ΰ0ηφM 3?«_@ΙΈ%HK½Ž€$ό\΅υn*hbΖV‰τ €Ι3D·4”Pφ§P«ΔΥ"―LQ˜…Ωv~ -9KΌ0Μz{"ζ±D“ͺŠMΤΈŠ:²ΉΑΦy ^!€'Jğ +Y$Ήχ'Gρ$Θ0ιhgY,Ž]MlΓσMρΘCκφŠQ³†Y/(B2ΫοBΪ€>ΓXΏ~ϊΪ 4žmy-³G”hgdέYgΘY…Pμ,ΡӊόeΡR;αƘ–’s&λœΘcnšώO‘jΡeκ”3<ς`x€pj‰¨OrJ€ύXVΟkΝ]²†€3bΑF†‹Nψa$*A·7&ΜΨθψΫΣχ™τϋ}UΌ4ͺsΉς“ΎŒ½gΈύάΐDŽς D₯Mά³φ‰#gT +‘žZEχθFYˆ~ιŸU,ώiΒm7Ÿ₯ 7ό"NGQβΌζν]Ι š ~m…©θ4Έ qAl’’šαCΰhchIJ2>π*B–g!Ι¬…rg°•ζ Eζκ-ͺώ@TiΟ3>žΛ]"1xŸΏΆHκώρ‘πή«₯>‡-‹ό–eΝ P‹―I7ν2j½Ϋ?μΕ +|f›€i~[U…lωϋ΄ξ#ο_―k«3{0© §­―5ϋ™¨OάoYx XU0ΈDcβ9δώΥΔυLι+ΝΩ(Y£±Šjέ΅,#aρΪ›ZTb\€ΖfΧ:‚ήγj6λ@½@Ψ[0V›ΌK+ς5ξ=ςOŠx³Tφ +€ω>Qτe^JυΘoY}Φ²{~ €ΒUϋ›Xΰ ΄΅€ΌB=T§η―1h’ΌκμŒ±†*™’υ՞Щͺqηί½˜iαμX:Ηj,iK ψb•ΗΪ`½ Nΰύ~«ibŸz<Γ–«€#ώϊ Aΰ/τ9œv½βτήFμꇏ²πα="U“F ώΝΒ΅}H˜ +ξŠ1ͺΜ0ωW’₯2L½ΟOήj Iθ"xyu3iJOλŒjυΘ…*jΤ’ 5·ν¦~T_ OŠ’€ygmώ"Ο+γgότͺΓTR©q1λYmqϋ­˜ΟτΉ]νφ,ΜpRΝΖXΌψ<$Ÿ^?™H‰ύπ­θ«τzΫ‡€––!Φφ}ΖΉ)<]αζΣGš£·uήςέΝ‹¦i{ΠΗ]δ›ε»+/Χ«o€²/΄»+½Μ·ηd!ΉšΥ?>ΣπS™ϋΆž·Α@—=>᳑v4!jΚΣγΰΊΛTY!‰[£ΒyπΣξ?€ΧQPΛΝuΓ±*'K‘~ρŽίkƒ„₯Ό­qΟ'Ρcϊo|Ϋ<žόΡθ^πO}ΔCrϋ°φb+’œjeνΏΨŽ@₯Έ`nž(pΡf4ω?4˜8Gfξ`—B£RAί¨ώvρφvδ3¬Μž΄―Δσ¨άΈΔ3›]<1\2(­‰Αˆ#πό>έθšiY/4Ο‘€¨ΘZ/κž˜Μ‚ΡPω%½=λ Ίšη0΅‘;x•ž:‚€7›BxΜSœξΕM—jEœNπ­γΪΥάSp„–„ !ΡιφMKεΎΦ€…y"α“ζοT BΖίx—“X€ hNξ“/Ϊ—6Δ³yσ‘’]ͺX"½Ύόο₯&ŽηΙ›(J&3&GUΛMN+ζυ‘έ.1ϋŠ¨Γ‡>·–"άs’@KͺCO€ŠŒg7šΊ aοί‘UώαM‚ΕsφΣyA3œ9` ꦧ“δ‰nπ}λ7μΣ܊OH3QάίZƒ³Θx”Σ<Α9r©<ΠE5ώ+όdƒ†SΣ.~2#†*ΡίF.γHŽƒαλ€Δ–S―Oc‡œϋΔvˆ‹I¨Ξ%˜6όӍ ΚΑU_…)\ζά―±·ΔΨ°ϋ‘^ΚlDάw=5‡p‡χFԞnΥ?5mνθT5//–ΣeaC,({gKcAGΡ Q©cς24€:>΄N©h¬δΫNΡή‚vxŽUB£‡θ–a₯Ÿ&7‹±[-v_r)ΐ­BYφ₯μ'ΆΝ9Ά.ιΑζΗNDή1ΌJOM²y6ƒξυlΥ`εΊ:ΔΆΓ—œ¨ιwϊ‘w N\7ŒεT?­ψUڞ .‘% u°Aλ²VΘ»χ κσͺΒ αΟνεϋ`a,qή;λαY¨΄ΊΕ9pŸ+`D₯e%­°&ošγΧ€œξYΞΜΨ ’λeο•ΐ½Œγ‡ι†ι?q9œ+>‹ί’°!*v€ΤΌΡ/ω—€ίέqMΞΟά#ΒC£v>°+UΏjJ$Zc†ΜX‘Ό ώΥmψ ’’‹sV«θ 8‰`΄8cτϋwΡΪotΘUΰια$.‰aLxα βOΧ|fτTσΠP·ί*` {Œz’DΆ«&F #θυ«Ž;‡Υ³8KXΣ‹ηUŒΣvΚφΣ)χOu€Ηbœ^γπ»ϋBΦΔΗ`/+ ψ7φ]<.v΅›I7ΠnkhP ΰτf_Μ/‹‡–’΄Wœ‰³<{›₯εGvŽΌιk{sΤx”š +%ο’Λθγz( ‘fλΊr–ΉhamcjΥ>gŒχπ±‰}ΆNCΕ—”=ڈζ.ιͺ΅ν»Ά#F+—0Φ0²-¦[›Ω˜k))Θjκ‘ρΙμηrmΓΞΤΏ•ΆYνΆj₯»%mžc_R=’Γ}Φa—ΈγU œu›'θ—x©Υ\Ί.x~u—q™IMα_37³o #&ύh`X$T4i‡ΥdZΎ4*–K½ΗŠ]皏ι~d`fz²ΤΛ B•_ ˜>ιL†Υ"E€ω“΅ρ«U‚hΏaŽψ“Όπ! +iεYR‡LMΙζjς°ΰΜεφVAόY5Z‰;ΧΏŒχG8l%#δ"”¬iG}‰ψ'„™@xξ»χεJΚΩωλΙδν4£0ρ"9’Δ©6s Π…βmώm€§Ή{βƒ$yB\Ÿ³ΝdΕgT>ΗƒΓ˜Ώ?ΒwΘ{Ο΅Φ;χ:λ―Ώύpψλe6…t}tŽΪτΆΜQ όή0UpΈˆΤΧΤ‹ψΦώ΄\QLhγX~RΜ?™JΟ„1["^_ΒΰΕ!%=εyΒ΅v9μΜ§`ۘξΫκ½±$Η–Ώψκ°3ψ9/ +­“τ·[@Ώ’ίΝFϋd%?ŽxΆώ¦u#VέΈΨ!ύ¬q=ΫΏYω¦€ΨˆΩί„λ<ΟαzF{ΚA£”!χqC(α‡σ3˜&Ω:~D¦P&₯_i±?–ΦpœpωόΡ@'Zq^ΉpϊY” eοtU ‘‰IFΞ#KSΡ€Κ?u?ΜΑ~Ή%Ε ,ϊ"―Δ½NOA(Ρwj³ά»TΆΒ&ΔUŠ ƒ£.Ϊ ΈΨ*ˆ§•a³Θβ²Ϊl{Μ&. Α«‡rU’ΰOΦτ­2ss(aΉν¬βh"d5>NLtό¬ Š+v-˜²ρH`\˜)#6ƏΒ›ΐE(δοΌƒ‘αΠ{Ό–}Žm όŠVόλω+Ζz˜α‘ƒRJ5•$fΨOYaϊJ=κƒΟ›`;Gc4:+P‚-L†DHψόSάυF€L9γf{ȻՁΰ?ΕAηε7o4|βΩ€ ‰J$™kt0j$όD†Εϋ@9ςόŽ,πέ;Γ3±pΚTZψΆ¦Ε]'K€±ψn~G‰z ζ%AΝƒ Œ2g\€V₯|±$œΠv@¨7ώ4;CœΠά='ŽΝΐŸ–Nτ7XUΒSt`fŒω§Ό!ψ‡ƒ^ν‡&g—γ$›d[£2Εζ* &κ€()PΆίφ«μΨ°‚•­,π%ΰ»ε‘€œχΞ˜OFVE™ϊ«ηUIH=αv€Κlύ)οΩ²m‘bMpsώΰ΄‚vϏRG¦ς΄ &<^ƒΜ¬x?2ΑiΧ₯F`©>Lώ@%ƒMΗb(7U3C U§ο~Žύ0„4_ ϋσΠΥΕ—θ˜ΓP‰ {ΧγCΣ9 +ΨΓοJθΰ˜Ο\ιΌg¨Ÿdmx{‚ηzbΎΥ­½_©ˆ™‡'F3i=ύdϊΝΗγpό}˜±2ΜCμLάάομdAσφE€K’”_²‘c :θ±Ή2Ώ”΅¨Τ‰ε4°3xE4„ΑQ‹oα¦Η‘ͺ°π"QΌ^*n\ŽŠ›”š? ή’Συ½ φΫ2ύ[€θ &NHZatdMAύτκΐOfG|ϋ˜Λω'5Ξ‘[|a»αΦ0β7"Gι$ύξ/91Β―κΝΐͺΟO5~“Β"p=;Sb• +τ"*‡;τe”&r‘b](…o9€?ƒ…Oηr Aϊ–aUμέΚP3ΐ±΅4ι3­˜KζCα)ΓlτΓ±šG8Β‘Φ%Ak§ΏΆmη ¦z¬εŒΆ}w_γωΖ;p9Υ™‹Ωε† „ +ρ£dΗbβ³γρ‘ζ‰AŽ„ΊφY2ΕN΄Όφ―)pœξœ 0c~Ή,ά/2Tˈ}ύ4ΌΦV0±ΉΫο•Μ³ψθά:%pΘςUΛA²b„¦Κ™[[Γ‘Όυg,Lμ#| ί%έςΈθδS(#šN΄hτƒφ5<`δ£ί RΧύ΄ν3uqΨB¬έ“=x8οT F4θE(Ψίa%ίύΣΔ²/»k΅Έ~ŽώθΞEΆέS’"?Gμ”²ΡλΨE[sƒH‹ΰvF-E_‘ tŒwv‚ 6Γ@&³Ε σsG;» ΫΏ¦ Π° ƒ2`­ƒ{{˜„\RX5¬]š‡ΊΌοl‹c€κeƒzιiTTlΈš~γŒ ΐ™-τ²χN”…}fναΡЏρ<$κW“!Φ -μRxŠΩφ -‰Δ£~•"9Ÿ^>-ζ!²γšSU8› Θͺϋ²Bν±Y^­Νρ©ς©J‹”Žjk^…‚Eάpr±ιi3*3Γ‰γWw+Βϊ^y'Š£τν₯—ψV€™μχ|βj‰Πυ5‰¬΄lKe}0«δ!b%σμ- x2£AdڌCΞυι•>kΞ[JΧ·N84ΏAζv’W„Θλp$yΆυ"£>«Βžιζψ^.l– ωφ7/3₯S`kUρ=Ύ$μ±&/τλ‚š6•ψ2>··0<ΜΛB΅ήΜSΈ–‰PŠΞ@Œ¨KλCwpφΡ*ŽΠιdδ+³ˆhΈφ$IœΟΝΨΗ£θΔZZ³‰±UϋS+‹=gρΉTKt˜l†(Iη §³aWΫnάΛ|)ΜTΌ9߁Ηθγ43„iυͺέNΉΊτ6ΝNŒ½΄ΡaP.ŠϊV?oλδΦΖ«ΡέώΓ•M»r7‰ϋ\=ˆύΨ«fξjZΡ&!=(ϊ»œ« ―ČΊ‡bΐ%γܐYpRρϋ΄Ίη`ί‰R†sϊnσlζsqΎΣ€X#R ΰζ΅4Evš+’”‹€,Ερά½½‘Ξ‹Ed¨άά†Φμ«ΗΫςΦΜΟͺΙΨf))tΘ m©n–¨βψšζ.Ϋ¨ +vιŒPH[ϊΘ―{Ε3Y\2ψί‘`Ηόwš{‰DΙ„m‚—%‡κ}\–+Αg‡ ­ςϋ<Ο΅6MQo‘υ–ͺβ#>ιnδ@0EΏνƒM% + +6šW²8όxT{h‘&¨m‚<Γ‹Cͺ(σΛE["c]αSf7ΰtςΫ6=Άηοεq`xά”G·LΏ\΅hόͺh©Έ‹© ‹]κΏBΜΧU’ΔΩΔQ―γŒΏ‡qχ“>f2wΒLΣA,•c+}fΫ8{)… )>θš‹ΎωΞwγ·»_ +ˆιΐ˜ Φ&³1J̝ +ΖδφΪ‘Dιt Zk ΨX=™rήZ…(7jAߊ7UφHDž7¬ά#RΕHGζ +5iΟJΏ―ΟΞ²H Εͺs{ιfUΚKoŽSŸ?”—ΫΥNύ£ύ·J~B°kο?YΚ© +M(bΝ»ός$‰ˆΘ΅ΉZ©>2ηWžή­T΄t*₯)Κ|NiPΧB¨œS +σ³jQηαyΚ<@ΕzCV₯»BNύΑ·Ό-Οέίv΄ΕςΦgΛΦ—mJζο΅ΦςOφˆθ7λ™SΙpβ…αZΡF]λεΙ$‘~£†ΧΫ¨‡σI+ܐyΊπή΅ά»q©Myθ;‘mKΨ;N6E€ εΥσۊΎ@!ΚL3#ΠΛφY4`ρUΝdεφξ—ΝC,ErηΟk­ΕΚψQ2φ^ blΆCoM};kΎg8)Q`Ζ›ͺ$MΎ­ςTG”OIx7ΟЏN‚Μ)ŠŒ¦£[ *θΪΒΰδΗrL Σ Dζa³ΕOΕ^D²ΊΩτq+Ψ‘q%Ν7‚Ύ)ΫJ²­οi%e‚zΑ@]/`΄―ΔauΊ’‘υ+hλ@ο ΅f;π̐Ѝ‘‰±›Τ3νΫΘψσ§‘3λ«Ε,΄οΟO}Ω‘Q aƎFΕ ΗaIς χίFί€Ί‡OΊΤ‘ƒ‘pώΰή<{k.°a\—₯€Ί’”² ;\»*ΓzΖ*L[ή0»\ΛΞ#Š‚‚˜ύ#*̈° cδ‰γ$α))Ν³ŒnΗ“J·,Χ"%\ΓbΦ$#σjFαΈλ"_^ηΠ‘ΟπLϊyݐe‹Β‚΄6Έά©žΪvχrΐΤ(‡!pτ»>­άπj+μv!‘ ί“ˆaο…ΐα7₯ΩI›*.dΠΫ€έW"L>=Ξμλ8}βφμ•‚žfΪφQ$k?s—:½ΐ’\N§FpυφηyΞψή€Σ5jΐΐR)Δ‘-₯‰π[thΓdk±Υ0°υ²ιd¨ψ…δ1²^>v½4R¬ΤO{΄ΜJˆβ³±LJΖ„ZΦWΖͺMwφ…cΌ>}0ΆΫ–EΟύ•ϊJΕ7 +›s4όEίΪb°ρZ‡Ρ‹Θ}mϊͺΦBτ―™ΟUΧqxž\ ”ζ{ΆνΒ%±Γ` Sζ€ΟΨόΞΐ΄*k‹­Χ•-/†θAbχjΎξΎέ_¦–­ΕuPo§bLVƒ‘x@-ϊ€C9[RY‚™#+UΏς’m#^π r”7ΌΉ:ž"Eo†§xς ;‘YhiΧ«ϊšβ4­†€ΈQ_υ­μJΏ xdν Χiς4©“§jυƒ=šh3…z `H~b3”κώC Θ7¨Dšbζo*2䃽¨*,δuͺ½ΎΤ—Q-Τ„›L–d dU©fT:{Γύ§Yό» ]νςE!]D@X7Ε¨7‰RάT—ͺΜ/–sΏρε㉻€Τ«Ίq{%’σΩΰώ¨Β“«DώΏΩM΅™ΖΚ(ji޽)šΰ“AΟrΦΙγέUΏΞ*œ’’ŸEόOŒ$#Y¨ 6εl¦Q%?=ΚΞν–V£Ε|]ōωΩ6ό^yμF™eΣ―SΒƒT›γ«jτζέΣLδΌm<<;C™_Ί˜ΑIΜΣ3{7ԍK\χf‡{ͺ;E&Άύ΅vsSS)raΙATΥP? ψzXγbϦ扬ΈΡdWοδ¨nΣψώjMηpW~π²Ή…Ελ£P›Έβxp]ΙNήΔffλzΠ*‹{Ώ–φ1Ζ‘†'‡hήΑS/1ω’³-Zmh&ΡΤ.1‘£27bBzΣuΆBϋ³f)YPτ5¦«hGMΧ Ίά\?Ioί €Ξ6ΦC>—Γ`7†fα£* ˆι η•ώ&ςF£œΗ‡@ϋ溞·―Γ€;τuxΑ+…ωτK6Ο9yς¬M¦lŽϋο|V\C\Ζ„9χ‰Π›δ άνKςΏΎ/†΄¬LδήσTϋ¨›•Gλ½οf±Ιζ€ώa:«πŽ–ΈL™1κ^³Ε)Οkά¨αN=Dη8%i-wλΨTU$aΥ±ΜΈΙ zbΨG.Τ„―·ΨT0‰RϊŸ=λρ”’Σϊη–tCe^ΐ‚;bλq{3s܌Ξέ†€X άΓzC4ΐUάψΜΎεΌ-.LC+†:ΪΥiϊΔfŸd²τ @s6ΐ'ύMt£U§ϋJςœ 'a5τΕPb‹,πw­Lr’D¦SSHΝτο“6._Φ€ϊRmc”]kf 'j›ΡyJ]ΙQιπ@Α·(Š>ŽΎΓ\Yˆ…/²~€rc f§κάQάb Ts,%=Μπί[ευ~!§EtιQκw_.½Νkš?07ΈίΔ―-8Τ«‘Ίmϋš%О‹»Tςΐ|.ˆ]Σ Υ}Jz:Ν€YTΧDΊΒή¨Ÿ963JλMΡH̍θ΁¨Š …œnζGMξ4R‚φBd†ΛωδYžΧaVŠ:κίBΰ:;€WG33ΫΟo”…²o ΚUtfώσ»Gb@Η@°oή™ ?S’zΑlUΌΆ Ξoίe”†‚YKά -AΤς…“ρzGό.78vwY‘/Σ"΄π―23ΡIš α"r7εˆ6{ φŽΔfδt,Ž*΅°ϊsd@²ͺΘzΎρNύ†?λ`εΟήAhχ£iαTρsn0‹α%WΤQ«ΐ‘‚Ή(kS,©iυΞRΫfΰσ2™σπ:“‹nœφ7 Αξ GΖ‡g⠎::]6’LΜ]wώ*ΐHΆ§ŸΆ˜WΝs²FΕ‚©zΙ 'ižrB1ΑλΗ‘„qρΔΝψX\Τ‰os΅…€^ΰ άόP%W{ή +λ€μωHHθ=κˆ)PΞ¬γTζWΏ»bνJI–suΟ‘TŒLTΙ€<ΕΒν²ΔVonq¨aν’šˆΦ°Γx:₯Ϋ"9ebφA’ι“Q±„6ΘwΔ£ψ$„:…֎α:ΜμmbA°θτ@ΧΆς§ΖkΉ©&ϊd|Ϊf‚’tj^ +…D8’ƒp›οQΊ•ˆνž²’εB‚¦Jΐ;I€χ«\.3Ωp­ΛR…ͺqŒu€l—SOπΖ@­ΧM=ϋa^·©AΣV-<&,Ϊ`‘Ν€“*M‚Ι‡?0F)¬²›}φΠ6eFI>§™O4fͺΐM)ΤdΗ6j|0†œχ€.ACγHuX[œšα²Β §ΝJyY―j쨝t,ώήηq~©Τ8.Όρ~ +™Υ$θ‘aΗ‚ νΒ•QB’]h/f.π:ΪW°ΌΑ*o<£2β(°!¬T‚˜ΫNƒB–ςxόΏι\0ΐwψ,4΄Ÿ?}VJλ7‚Π¬Ϊ[4€<ΗΛΎ±·ϊΟ¨"XQύu’ΣܞΔNλΩXαŒ¨X€ZQ=|ΐ‘υ=W{13°mΡ€RΠζR֞ƒZΆgτ{˜IγΆ=ΣΛFOψΐξ—§3©ξΊ6ύo^>aŸj³kΈ“‘'†ZΑŽ2ν”`θ*ώ# \©Ά_Ωt:=šœ˜V"CΚNΓUΛcΟΕ@γIζˆkc¦bΪ¬`Eμ\›mΎ ΅ˆ ψ5ͺ/LϊXžν‚1ΤβΉPC¦ΑΧ-“`—9 megΫδγ΄ΰsΖΪ} “―٘7š†]vξχHςΏΨΉΔ7W}ΈχD«Aΐ½g•2Ψ«Δ·ϊŠ^˜Ϋž3w[θ›Γ?/z —σ Ο―_-QδŠ<8£γΫ’†Pζξ9ewŠžJΘώιΆΧ£Πic8Z6ά@!JοͺW8mg—sΉRžΪdΛ°V[~%Ο1US«φ1Ωg†iιqckb^|¨΄B?t B6‹ξV°ƒΨ .Ϋ£0ΗΓΊ£e…3ξ‡ΠCΛXΡϋ<Κ虖‚šgτδ‹y>hΡα=x"‘Ρ ‘¨$zμώΛ•C uŠi·yτ9Τ΅h;%ZQrΎN™e[χ`τξφaχkκ“Β)ΑάΑŠ·δ5(ˆωqΦ 4λ¬Ήο6ύy(!Ψ€Ή¦M°5"Ή½H-Jξϋ¬ώqŸΑσSŠ ΥŸ‡v~`*ό–Ϊ¨±Νoάa{]”Μ—­Ο~.†‚ X+ψχ0κ^’γ…:|γi{ bQΈŒ†Σ¨Šڟl΅$O’΅έΡΜ›γ…έόkϊ;8)Ξγt  ε?φΗXI|%I2Θp―ͺp(al4¦'_qί[^<†Φ³=λ³|4H{Κτώdγ²(€²œ—mΫΆmΫΆmΫΆmΫΆmΫΖ^ΆrnR•ͺœΌΓLO˜ώϊtσ /UβΦ_«OΌHxό^-ξίbŒ€[Θvρη9ͺ θDTρΏj{³D§kN†‹.kδ1aοq›x.ρφυΧE£hŽZΧ•„ω‘Δ‚Ύςž8PφΫN0aΆ$mξ†SξγαKŒΥI%-i8υξ~ο“k¨Ω9qΊZθώώΰ\5€±ΒˆDΜέϊ5fŽμYάsθΕ`°•«Q?qež›σ² – +*μha/Χα.ΘάjΜόhݍ¨QcvΜ€Zfσ?΅θΈέΟΒrM QΚ‡PŸ1«ΉͺZˆ–Q<ŒgvAέ91ω—θ(΅%κUKΜ5'v_4―YΨχ[† +yŽΔ¦’G +’ˆ qΈa‘Ž +-¦lθIΊΎKΨ iύ؏ˆ¬C―7Λ¬Ψr2SŸ/Ί§ΗB “Hίώ°šLBU‹`³3Ω¬‘Ίή…‡έ_ LΫ$Eα―q^'hW1)u©P?rΎ@ν›΅°—'ŠΉΞΏα@wECPk˜ηάΤμτ9―zχSσ΄ŽTcΧΞ/ΔΗ§Ϊ…χV˜7ΩφύGšK;ΡHbΒ^ €K$Ή˜άNn¬ΣΗcί:Έ5>1­ΤΥ`΄ˆΈΦžυ_ο‡ο Ν +²©•±{ρΖOœΜΗυ’ΜΠ–PeΒHŒ*θ}]­“Ϋ• Ζτ[ΘΖμαφ(/8Ώ$··gq+HƒB!OE… +Zΰ2Y’ ‰ηMϋ!Δ52rζψ+ωΘ³ω ŠΤ…6 ½ΞY/ζΐŒλΤβ14ΫOƒ[ύ ­ΕηΣρj`Mu‚CŽU€… ₯£ΕΏ=κK«&LΠΖŚ/BΒώΑω«žT1do’₯t€Άœžqέd6¬{¦0U$œ|zσ΅DΏω(Ή'˜ω"n7…―§lΈA41ͺώj58Ω―ΨΓν/ΎΒ&Tξxλ΄œΠ$Λ’LύzΘt8?ω+½»1dŠ·Hhxj ΐΠlγ‘{Ρ’½XεςOωτΔ™/-τ\ύθΏRΩυ΄‘WήmbωΧg΅Ž‹œ§=J•»pAξ₯M€iO™ηΫ¬4"Ό"K9aZ£”š;s₯B‡Ν‚,’NξψO]Ό{=ρOw‘ό±‘ՊΌ£$ΌMƒΐίήΛ€β₯:Š'/φύۊ=ΒRϋυ©u6ύ‘ S‰˜ίχ›ΉmeYΩnώ=™Ω9Oετ’:χγΪ«Ÿ uύεό.dt}ΔΗ-…wός*«"³ խԝυ‘v)ΐ  „“Δε”0„_Τλ™υRϊ[ZXIΟ,lΙSu^B%Ζ€r€ΰΞ©·-!ΧdcήΡBΜ#F˜Ί„»* +BfΏ!1΅Laπ; ιDX¬γ)‘a^υh/Ν`=BV•άρ!>΅`B,F§Π,ΣZίmW$ΠΨάΔ—‘’ǁ@‘Χω:’½9> ω³wκΥ—5 §bΚO’‰ +3*Bj?˜F4§LU6mΉΣβ|’GΓ{ο͏£ˆ+ϊ˜’%r•uΩρεq;J·. ”5‚'ggl Y 9ΨόD1ΆEχ5έύ,Nΰp¨~ίΚΤ—ϊuοΟ™vΖ3¨ΆEXθΩ?ωc.°=xόjΓΫLάΡ Λ,a§ 0ƒ€.»Ίzwšπe…Τih‚KS‡&gοV©o£φ«,KMMζi:,ΝΣv·’°£]¦DηV"Έ_v8γ~9EŸ“eΛ‘κX"ovœΣ7»“ΏZAT‚Φ©Υ£(ϊC¬ζ1­sΧ$’J‘FE "(½@,ΒηsΏ*Gm84C7Qνr™¨t%”c¨x!ΡΗφ‡%Ι +₯ΐdΫwy΅Δΐ8όΗd<ά«ik7Aφ¬›ω(„ξ₯;|)„γΝοέl!έ/ƒ—SΫ1™Οž"h§UβΨ?_”Re Ÿ π”–| +λœY4Q-Σ6Ω&VTέφΜYΤγZJΫh δUάkω*„Ζ`;Djβc\Ώ^ΰfFqό;ώMΔΗ¬œΤ0¨OΰΈ§’leήQι}ΑTuNDвώ†‘ψ΅Fcβ³νρX ©§]^͞Ρζ„%έ'#μΜΚ’-•Φ?\ς –»ͺ­]N΄JΑ…ΐ^Γ$σΆς‘~ΘφβVΏ UZώ”TΙ σΠ.ŽYτ>TΠF§fvF((€²Pqp€Π”7¨ΫΏεβ‡sθμ–τψ­2²ΐΣ!ωΈ°Τ,qš2¬H.Κ/ΫpΨRΏ„a˜οF‹Ÿ~ψΔΑ+ΒYdnΐ‘T¬q’έ…Τ«S‚Ϊ–΄eaИ[‚ήiΤ8΄•yΣΦnΰ₯© Μʌ❑‚ŠξΥΫFΕ‹.}%1ψdS²ΆWΛν{nφ8΅νώ“eA_kπ‹2‹³XD/>‘Φtχz½Έ'©4~ΓΝcΒvσ£ΐ~,+KΦ9€A»"\-•αaΧ™„Ω.ΐα&{%EΫσΜΧ‡ρPΆ@|ώύΈ%iνVΎl‰π ‘ ε„rVε<Ά³Π%em rλΈ[’―ͺω“xυφ?Ώ n±=(m,˜?O(QΈPjdW€²gΪ/mn4Άΰ³™‘/šαx\uΧΗ#Ν8ώΕ‚yŠ?χ-Ώ‚@9οΆξMg+°x…¬Χ³Ÿ6ΰΑ8Δ»sΙ'Š‚CdxΈ Q†fNΒ™© ²ΰΏCα½Δ{σπ,π¨β;‚G«ͺ½³γŒΦε’‘Ω†ΩkΓβ±…χ7κ΄bΣvL"ν•j8Q³šΐ m1ΖfΆŸ—Js”οξΜ•ψζ9z²‚Qd·vρΗτΑβΒνσͺnp„Α3~_4 +ώh”κΌ-y,μwOM΄B[.¨ίŠqΊTϊ­kͺωZΞΤ3?+zή°…²„hR3GAΓRIfˆΕXXο•":›s’'}‚ˆ„Υ •΅QΖtƒΘʈΠi¬±(τUΤ¦ν¦6γnqVcϋΣhΔ°žQφrβI© Ω8ž—·ΗΛΠ³η9&4όK=Ί$¬~πL1ϋq"XΉ‰)\ ’Ε― ³XvCΕRtbn’?Χ–Tζ}%cΧ—yˆŒ)n_Ά›ΨΚτΚ…DόKμ†X! ξ”Ρƒ(΅–‡!7©ψΙωC°ή"Θf! Ξbδ5νΰ₯ƒ‘Ύόv 7UΒς’Ζ§UΠΘB9σ‰³G#m6λοEuΚΥŸͺηopn4˜B^θq«ν]“@?LΈρ[δR›ϊpΆ¨Ξ΄Β(ή› εΐυα ƒ·«ˆ65p%Λ|jΤͺΆΛ’±ό„]ύΛ) ς »:ΝθCJkiΎ‚πΨψ¨U¦J.S +slΆ…]’JΜωu›8―o΅(b¨!mnM*ŽΒƒƒ'œ"WμΞυ•kα!_]₯NhΊV„V)ˆbή΄Ή½2Iƒ)}'w$%nTBei“οςIΪ’όWςΫω­'³eάϊΥXΉEξΥ³!δ‰ςϋ»ψ φβΕΛ½aρ·4fϊΠE73Ξ”nμ‚)] η"P€­₯Έφiœ|upD‘„3{8ω˜ˆεƒΖ«;ΰ=‹ λˆu`yνjΌ6(Ž`dψΔξ‰Α`–Α™ZΓβ% –lV«1˜π +ίΙΆ€₯)$Ν΅=ώ‹r©Ζ©ώ«?L₯πίUύψΝΧ$ζ©«Y=8XΒYλ; cέ.ήͺͺ€ΎΈbΫ·ί ΅₯‹οVKαkE ©·“X—»«dŠΞK2Fξv6@œ6]ΪJ­v% }ί‘³‚dΛ…)² ΌU|ΩY6Ϋ)S0:‡gξ―2Ι K΅δ2.Nnπ«7…·!ήn·λmNι*NωžΑδKRZЦ,ˆ;ɍΩσQ­Gζ.Λρ4΅O.bGΐkD)w‘˜\υp+ααjι$3₯‘'θΞT}§‰6ϊ οHˆλ½ή!z₯P‘RR)όγωwΖ>νv"Qbγ¦%ΆͺΨDώotNJmΛ¨™šυ*Ϋ¦‚žΤL³΅ α£ΦΜίΛ9Z€ΜΩ.πφ†~g0νΑΔIpοχΓ―)v/ `ΥΪx1›lmBε†τΌŒΈœϋΥ~ό8KFp "|νύb€£xzΜsJνφ^M6¦3FΔσ„δ~~+i!Η$™ΘC«jn"EΠ½΄ε§β(ƚvMΐ9ƒ•«€‘Πβ§RUrθeυγ9Ύ…v2:^“Jμ°SbπW8Aκ/₯ύ1.‚‡/5 ŽΫ£Α…$8΅ίΓ"{‘ΪβΔ\€χϊpE_ͺ?ˆD9[‘-©L$α•D²έϋξ‡ΕxCά,5R/M˜`mRη΅OΆ5₯ξΨkvΞ •΅ηαΜβ­Λ΅™ή™δέ ΅ϊ^A"Ω6β-―’›'ٌ9AA•νŒ|θ‹xΓημu)4δθΘMρ)R)Q·Α<|Ur_ kž‰YΒ η—φΐ›θ_Ž0¦²yιNΎr—δO@·₯ίόaηUrηΕΏί‡πTC}k΄ή¬ίLƒrΟΚ0Rϋƒ-’= ΔFƒΆΈ·‚ϊ΄α₯©]Ό€ˆ ©eσx! +PZη=ΡEΞ·ΤQp,›ϊnΟΐ¨₯Σα7,ŠΩΗΞ¦Ωjψp΅Ωg H΄wσώe†QŽYiΚ4νݐ(Ο|Bκ`ο2θ«zŒfΊ–Ζ/Cl&œRqTdo­±’ & Η#ςJ"¨ω·ίΡ^₯9όOκelύ8ρ’nLeΘΏ φ:ΜyGεdϋQ0ίTqή΄ΰT2Ή—ƒƒΎtNΎέ'erEœWε Ή(ψ\έ^άs}Ν(ΜkGδN£ΖτTΚfΉ¨ί%ήχaVΗd€Χ,ΦGEϊ.(J±i+ιfΆΎŒN[―¦ ςŽέΗCΒ“ΘσŒh=Κ2Ϋ^°Ξ+}{’ X­EœεnθυW₯"ΑΚ„g06μT~αΣ”Σόβν…E3κŠΝΛχp_2ε£Όϋdɝ’+Ηf”ν:—fwισ3Μωy¬Υ9P‹«σ―(’ΗΘκψ·Άnθ―χ™ι½»;OψΧ; +Νά}Δm>{IWώ».ΚΙ‚G²ρ4=Ω#¬ˆ‡·!­ τœΨ āκΒDϋ2Λ(v@ηθ§-°sο‰aaDƒν E|Θ Πκ,ͺN©γ㓁ύNŸ*1„D ΣΧ0§Ο»2B%ζO«„F„β9ΝχΊOTͺ2ΗΌ;‹_ΥœΈQ‡ΐά_ΧU~…f?²]Λ―k½υ3ΎZcξVž}1NεYΛ‘’αυO?Q―Β²’ „'~\Ι ν£Q‘Π©#Χ€;bΝ:λA¬α`Ωe0βζžNθ§πψ΅”“ΓΠΝΐώωb2Š†ΛŒͺφc^zέ‚ € L&6$ƒ]‰Vrƒb^*‹ž²Β‹ +ύεžYΔŠ~K\’ŠκŽ™Ρ•¨ΚŽΣΊ}Έs_~Yψη12Ζ‘J‡K8tμΞξ²8Œ€ +Aΐ² oWˆzž¦¬e”(€šS½|«XρO”5sΊ+G₯>˜2Αt6>ΛCS”bΏυέ¬GG·–‡΄ύΒΧ7U6ϟ?«rΏ'’³?M?έ€–Γ05|€© Αw+vΆ?p-šΧ²pΎεœώύƒ6ζεx/^¬η™’’qό“Δ[ζK#"–d5nυ|ΝγοΊέόzgΙͺXb1^ͺΡE`₯¨Γ&η°σ8<γkΗ}Πo#„βן=¨ΟΆ}€Ύ3P%ΫHX°ΦiŽρ2–‡« +βPNΙψΙΣSNΚLvΟ !Upά¬=spΎΪ78p¬# ϊ~+:.£8&!-X&ˆE|.ˆoΆΘz˜NΰϊV©ΖT₯΅B4(–VΔ~κm2V‹\³Z‘?τίMςGτzp‡XLΦC›ΜM:œRΐ8u†χ”&| \ˆ%PΑΊΨ‹Q..ΖΟμ&₯υρθψΥυlRπRηάb yt ·ι•hέSκ£θ―$K#L;<ΰΑόΰΨωԜN!ςi4Χ²κ†LΤο“-Σb4’–K $p„x6Ϋ: ΣΒƒσοšμΞEgŠγU•rΝJŽ‘2 -œš³+όώœ~’T―!φ± Μ)b₯oΕΐεE©ž—εβ4ί}°Θe°ςXFΙ +‹‘Ώ ρ±Τ’Rrf$™&ΧeKR?‘*»γ’Ϋ&Ŋ?¦¬0FΠ)•KξͺηSΜ~r,ΐ.FgVk^΄†$2''άΒωω‡ 2 +ΊΕKy\ͺG0Œl8ιˆ/Jμ‚βwbί3­@(;ύo@ωJΒ6iΎΙ»OIγ .›cΙ’υ­λΉD@πΡlFε3ϋŽ ΅Ζ_έwΧ=0/{ŒvΤ* ’Κ„Εέ΄@ΩhΛ²ϋ2L²Χj|Γ)ΎoXLΪhqχΫκ‡ζ S~/’΄D‘N +g‘&ΐ$–ͺύΟ#UvκiΆ#–rαΣ9€[tΗ@˜Mμ^ΰ΄x~M5UI–hX£ΩŸ/Κ\ Jwzφ»[JHښ½πd€¦±LΕ›ήί½‘²Ή¨+ ŠZŒ²\<˜•*i d`κ~K]ά•\θLΧύ·2ά‘g+(ιQ‚ͺΘΛ3 –…E+.ŸHܚψ·‹oA(δ&_՞ƒN»bf=ͺ‚αΌ;Q»¨i|?σ'ŠΗ£u·Ύ²Ή° +bn–³sΉˆ…uΕnj³4εAvh9d*]i[RΡύ†§ΑΪ}Λu΅("ήδλ_Γ#Η[qόϊ―UXE~wŸQWH;0‹"έ’¨Ήa*b k60‘ο?ΧΉηΞ‘φέƒX[ΛΥiV\š5IO‘–ε|΄ΌΩV·:3Μ¬.vήΖΊŠ}$§Νϋ:@f½°ΛΌοΜ^³4 +]&ρΕ–xƒ•Χ3s1GΡ¦ Ύ\ |(ΆΪB‹tMχ΅Uꏀ ΤBV!‹0―ʞ%;ΉiΎύZ*πt`ΨΩ₯zϋ„ŽΝ€LΣyΝΓ@πpJ–ΔΤpλo«όfΟ bΐεSGGbΖUV[Β—loLγw>&’%lΛ*s™οʐ7Ν³p2ςΆΕQ<€¨Τ?8δζZf…Ž’ςR+θΜAD^2x(k>αΰ ώT*Ξ7€rφ΄xvhuΦ6βxCί€` `wGΡ™ή'˜β£ŸΒ€ Jξ—Ζ±μȊΚΩ ϋ0°ΪhH‡uΚ +;wqŒ–ZΜΦ3Ο?‡Y9‚°žΕηNς Μs«t>ΦR[s κΜ εq½Ν™ρ3.j +GX!-γ>λS‚1²πYlΧϊΎήcΪ‘όΥα5mKό₯ΉVKq”‚?°¨`…³†»'Hs‹9_C ΡP'œ²t&²« )΅Ί0N<„ŒC+£H‹·ͺe}ρϊ.•1ιh€ΊˆPΩγ\Μ{Ϊ6―΄‘/’­6Oμ#zEhβ J‡υP@Ηο%˜kηπ›ŒmΒα Ur;ώΘύMΠΥ}τvs«•Ž% :Υ w–—%ΐŠ«„γΕτ"‚ύsfΙώΪƒ‹ŠC&Sh&T1ιΝA’žŽΝ½Λβηv(c%9JΎΌ Ί¦BΡΡah'‰IΝ$Ψ‹FX;οZρΉIηχ…œ y£±ψνFΐΖ\μΔ!θE—€V έS­ΐž +ƒ„JσΫVιμ0ˆŸςˏ7ΞL»!υ„ΦΓe.•q3Τ‡­ώθΩVο ύp;έͺ΅•ΎΚŽ»x"₯ΠχΎe&2-OΞ+Ή +Pο έΙvγtΤtΔ£δ/?.ΤR½€‘.ή#^'γŽ„Gš#g>9t Θۍέ5Ϊυ·!j΅%€€θˆΙ\ο§1–z ŸŽΌ]šWsΌηUδ)ΓΏ:*0Ζn"†#2~ +Ÿn³σD€Wj‚y6Œ‚ƒ˜ϋ(`‰;“ŒΫ£(z„₯^&qμ–~δU[ΆyW/QςŽΉά«)ΞX"%ΆΜu8-΄°–;jgJ!#GξΓοPh”ϊ¬₯*™F` hKΪΒMΕ‹?$¨χ¬#Τ–ω+Χ‹–gξΟ\_ξ ξυt•J ίZ•›B&Ρ ΟŸόLkξHƒ(αΡϊˆb'DΎΆγΨ6?Όn>u2dΥΛΊΚωTz‹O g¦km‰Σ Ο†ώv©‰/v2MΡFϊ[ΫΙ’AšU¨ΟSΰΦ΄"–1‘h«Λ#5<VRToΞ)@@hθΤάεω^ζ²έΔΊŠΣΞRέ©€ν‹.…xZK&Œ€#ΩΑ?m|Χ +廬|Τa“l€ΧξS« ˜Υ\ bλΥΤ ‰’²ͺXަ£`mΝΈ₯ E<ŸbG LŽFbΨ’¬~]Δ£»Ό»1+aΜΙκΐDΆ‚Ά>‘]‚„œΗ« ’ƒ’ϊ9ηJ­žόα(“»΄ωi3eC`ύΎbeτJΒΟ‘ρίqvtΙ!Υ5a‘g{DΌί7Φ|N²Υψ·ΈI"1˜³ŒnΏ0ξφ†c"Ι ˆΤ"‰ξτ€ iœhσpb₯ ¨“(Wΐ‘ξƒS¨8zR«³8;,Β‹‹τzΧΪΔϊδΛ…871 +\kόa‘Λ—ΕAwΝ§κόή(<,Α.!]Ωυ‚ /)ΟU}G© ~„ΥGυLRϋ9δν«c‰)ψ$vΌM.V5t££L“Hc‘ΧBR¦j‡ύ€£₯Θ’W ύ<ςՐγR9#l‡EsγΖB¦6nΌ–mΡtϋΌ:+ZΓΣγ.pαuΨαψxq©Ίρ8’Σ&Ψ΅a /Ԑ‘ΟcNxˆ°Δm=¬Φ@Kmw\ΆΎPˆΖκΪοI‘ ΩΜη=κσϊ’t>φώΠTŠωY;Τ¬Θ"ΛIhKθW΄7Α,ώb³™9ο₯Κ.ΐ9h•γx2€ δd 6όKŸ‡W0i|‰Ή- +έ³Ά’rιά1©ΐζ#½χžΐΕΝΉžΝquωŸWΉΟ~·ΕΨύ³mςa[UžΟ>W£0ε <~Μυ1Φ©|)‡tͺnΡα₯½έٌΊ *s…ρΣΧ7M σS΄ET–Τ: q%”„tΎ.κ•œΛΠ@’₯ΞJλT©{hξ N€ψ…{Ϊ'Ώ(ήΕ loΉ[4†ΆβˆθžοR|©A'«QΠR™:’mwΡOR‚€E M1cŠΓ’*ώΆi Em`ŠξU†ΰ!Ι1Ν&ΐi'|1o1Ιώ}ͺ½«*ΥΊΠΪλ0™*™`6#ΊΟq' Œ%*ͺ^xΰbωΈ"?c0gέ44Ί”ο€&}_ώΌ¦αw4ꙍ°’γFτg‘bΗJsˆΥ˜΅όCΖ„NΞΚΈβ»Φ|‡YQ–ΆŽ”m !l΅‡ΊΆύ… BOξϋWXF|“πœ]vςαMyh©D“@y™¬!~‘œ-<¨$ΜqKu:uΦ—χσΛ₯ƒC“+ΈΉ~Τ‹TΡuϋΜΙdnL“Γφ]¦η¨}™*λΓsωdξΖΟΕΕ6³: φ"· OτΩHΆhž7Μό‘΄h°‰ΑrεψDθ’μg °"?_‘ΏΒ­$t¬5‚ 3Χ²Οv’ƒ Φwυτoτ―νφ!π΅Ϊ#.Βh€ήd$ ₯1]άnXυ)“ 5!qα»1Νκ1ψK8Γ8> «=D0··'΅9ϊxlΧ΄‹jζZe9\ΉV32ͺΛΆq·Qvγr υ‘\!½xdΫ+» Ϊχ¨t)uΕή(¬ΛέoЁ$΄j–šnf/Nι2€ά8?e™XsΑ₯6ιY­½˜ΆŸ;Œ³Ζ€ΕυΙ#«†‘«ΣφeοlnC?«0ΎΏ`ΛΓφͺ‘}Ϊΐχ<©\η©Φ2λΈz₯S'» r’/Ÿ¬^—ΘΈ}σœ ΑΎ—.ΩySΛμϊϋ±L¨Ψkœ+“V_Lgˆ‘TΪύ’½Ϋ~)b¦Ao*’‰WH6Y6b—έ«ΙHϋ3\‘αα“Ο].sόΑΥΗY~ξ—ψ€ΡΊ Ξιƒ Πίο¨qQήΈβV$†| ž?o†:‡ΪΫπTθ(…%nέ͘­ΒΆbpίΈkPdΰqk'γ₯ˆΥ8 ’‰ +.mS(­Ρ”Ψwύω!6EPν_q-žtOάμ\έ€Zτ Ύέζ…χ€gp_|°&ΊF”Bακƒ%²{ΥS££Εύ”»ΦN€Έ«ͺ­HεΛ»`χ9fοΫ›’>τ&χ©œ±¦}αθ3:GνΊk?ΠvΖφΈμέ,·kΟΑΤAo‡"ΰ§©8ιψ“!SRTFqFfNI’",©ž˜I”Οχl[eN\`¬π)lŠEΕΨ5¬ΉŽ)ίޝ< ΔN7Τ€žΣxَε7DV¦₯>‹γ^δeύnωMΆxη^†ΰ9Ήω‹†4αŽf ΟΤ'ρFΉWφl4=Œ3π%1Χlζω’`0)™όž1N Yυ"<δΎΠœ+iΟ)οiž} oωv©αiΐN0g€ο€6Oμ€:Ž“ΑGνaςd©Ξ‘U$έΗώt΅V ?qb²γ(άσΈσ_oi77Lω c7NΖ•₯Λ™βKΘE…εΌ\Ζ?ͺn²Ÿ…₯“˜3ΦzFœwZkΘJ¨’―—ύ C`·R{·¦Hς^ckIPαͺšŠI՟ΐ¬ΘinΚΕ3ώλfΤI1έ!ΗωQ\]5”θ›UΦw―.€rUV ”Χ,Hxaρd| +/ŽζƒΆΧ0,aΊgΝφt–ϋ- V6Ζ(žΙƒ#Ϊ]]4a’PΜ)q_ΪΕY—b§¨ά»΅νrλ@~¨ΘψC†uv΄!HλEΟΥ6σ§±ηX₯―††AcΏ] Ή(0PΚ»VΫ6Ύθ¦κ•Ν΅sςΩκΪ£΅”rX–xRpb’=IW4xc 'Ε§ΣeΏW~ίΨτσ{ό΄βBp.‚dΈ«>Œ$mn\Š&ŒόnHάλ ψ)όΔ AŒΣΐš½-‡\9KhПdbQSζΙ«G`ˆ/θtΟzŽ`ΤυΨψϋυνWK΄„Ό,«9yΜ{ ζ*Ζ2Ϋ^[μF„Υ[ ŠίΝiΆIaϊ0ζϋT6ΪΡΐάGuψΒΙ>Ξ<·Α[ +½Z κΨ₯κΠ‹[αι8‡S©ˆNUΗ¬doΆL,εϊ<Ξ0²αΐ΅j +1Αλ»7£N†=P#₯0ζΘfδ05YόΔVΰA {τžk ³³ψvwζλ%n€~ΎΤ€^RiΓY0*6άmVO h;ԌεBe@LΑ©o―/q1W”οΌχ*ΗΝpΐ΄l:f3;$ 6AM.GtDσŒkξMξqΫ°‘# ‡€Ζθs‘$Έ±@<;{ƒQ Γ[ψ„©Ώb pfΜ¦δΞ3½κQξdΑ@l1OΧΉ=»RXMήRΘD―<©»AΑ90¬ίΆ©ΤpŸH½ΨξFG―Vcw*φΨZςΎ +;μP ˜!Φ·c ­m₯^ΣUβ^Z[o/f~½INΊά/Ždv}\”oΈ)qΉΘ{gœEΒ1λ[π°CΏΉύœ6@— -KK±TZ±1‘κ‡aՏb:ωH€ςΧΏτŒΤΊΐΞ"jD拉…‘;ς{οȊ‘§R>©π !T)0#UR¨ΑΗϋ bœ)ό +K U}ύ‰ΡδκθJΣγ4νη9Ψ{Β‘=Ϋ,β^γrA n]uEΙΟ|Α +–·‡K,eΓϊX+Ÿ«0"WVΒ· +’@Kρ?₯βΉLK–gΑυΏeXZ£„dη¨sŒΛu‚ :]xˆv%s—>¦@σfd¦ιρA9Yά«Κχ19«]ΗS`'‘* Ζ +ώUΧY„68ρΫx~œWε—"΄š˜ΒϋHΒ ‘Oή‡]=,Ύϋ)Dά9«„bIΔ!oψ•”Lν˜ΜmΕOσO:pžXΐŽU" R$VY€`s,αAσϊΣ+η€!½eΨ²bΨα[΄Oδ{žŸZ4» |a`Ρ’˜―­䈰½ι8ρ«ŒοX ₯£ΏŸ$…Ώ„l[‘wΆNΕτ S·l)/Ι="- “8§T?‹θͺ™Žƒ£ +dΫQ³vܐΗμδ^ΜΌ‘I‡’j€TͺΌL†Γ)Μ‘³Φy‰sΫ{ˆ¦ΧgE ^‰ω]οΊ….oΠ!ΈΗv-h½«:Ύζ Ι8JΤIΠ”j}|V‡«”U…Ή ˆb¦ςͺΓ’oΖ  Άs +©πmƒaψ€Xrίaέχ† νuΝ³oŠxύωμ썱₯† ΟAΕ α*φΉι*Ÿ“>0Δ4o‘™2uδqω‚|m1€|Yαγΰ”ΆˆFŒ(ž6?]ΤU5ΎVOξΚ­”ύ5Δ.ύ ,ŽŒΠPΏΣΡΆ„ψΠΚ―²%`γαεnφΞv6ΦΒ“ ΰ†ϊl–ߝΉ[\L$·BGν’$ι ϋ9€xHΩ Ÿœ +™‚*ΖώΤη―–Φρυ~zLΥπζ@Η@†λ±LpF‹7xήφ+­ΰ¦ΗΉœΨψIh{jzgή`͝E{}=ISΌ:ΰε·'ξ²Έ‡0ˆ±JΞΜϋ @n™6έ1†–Έ#όΣ½Εβ4ηΒχ4GήΡlm Ϊτ•~°©Nλς=)φK’ΕΗx,3©Vιˆςߜ­“₯ΙΩ²«£rΰΠ{oMΟνΣ?ωθmΊ‚i++ŠJZύ—ΙκMΏ6“σ‘―υ4ζ‘€z4|)ΞZ\³€©~ΦζU°5ϊˆ<εaζζ²Β»;LτΦY(₯Žσ3!<πήF8Pν!Α­bAΔš3}YŠυL{ϋşe6aOέnλ žt›η½ηאψ lΛωgΆP~d%$ΪEyΚyA[‘¬ƒ,αžΑΑ?ό/uJ‘»’‚gۚ.Ά9Α:N—rC΄ι°΄Ψ >ΉFj]ΪέπQ„9BnuΈ'”±`Νъ™‘+—zF§Kx/ ο„HΆ}£ωŠγSΎ΄σOF8Aή:q¬)Ž7©υf +χγίΫΌύG*Ί8=|±7ψΑ₯š~8Cζ}K"iσyΰ}ΗPτ$Υ/Ÿϊ†_ΈrΛ„°HΛή…dJ»|N€ΊΤδ@ι$‡%IT2q~ΊΨ·PΝ=†ψ°ΐΚΟάΊ(‘ΐΛϋœ?-^Oh*Ο³;Κ+ώ$aϊŒ αλ`c3œόΙ6p«φ#}bλεΎόVl°3Θέ"IΙΗbμž Ϋ&3BͺΜ$JΌlη-P{©Z‰aύρ`ΕZ°R•”`§ΚαΠ…ωi.S’KΉφΈŸ±Φw +η χν$–diΊ™θθΤοΖQ³s¦­YsΈjk£VB…š)0[²Β„nέKΦαdϊCηkΪUL»{wφ₯έ£„!Ιΰ ŒŸω―P¨¨†Ό|4_•ή"$ρ *±ˆΆ²9Dρ?έλΈφpIeΰΒSΕο‚™ψ™)—'’ύN œρT¦qC΅-‘”‘s.²™Œό<₯†hΣΆ›RF<#¨ +ݟŽ0}%Ϊx5]OαΔ²…Ύ«PžgŽ^nu Ϋ&fοžϊωlax\2ΗΘJ˜Μ {Έ ½Δτ(ΪΔφœ†Pv‹΄λ-ΣB‡DŽ2™‚ν³6>4ŠŒ7DΦχΪόsbbΪTλ&z7–Κ‡¬CΓΤ½“ώ +|3†[gγΰVqΣRΗ†!&;&"wb³†)Ηnπγ23œdεPOEdλΣ%}– +Zςυ‘½‰lρD؎ύ1•Θ–k𨌑0\Oy‘κΘgίF“ωWΏ[ϋHΠ™9Ε–E2•θ·γ3ξ!Ϊ¦±VΝH.§#ύcυυY‘𝈬Ά;›ψςЁ―9\©'&Ύyxαώ,Ν 4Œ; †6΅xν²Τυ'ηφ―s˜Œ· +X…ΥΜ{ό ρ~…‘ž‘3HΞ” 7νh\ΊZΓ8F[lψ<‘;?/|₯BY³½9ίԞ°Μ#υK³<Εδκ · ™u)np~Cκ₯±Β‹!ΏU>O”h^ Ψι +ΪUΨ ΊΒgsκφ6ΛθrΝH$2‘₯(’Θ>3OΟ½r6l‰¬ποOζ +u'Μ—t#.θaνnΐοΫώ䕍ŽΊΒk±9jDεl{r2MoCtH…Θ–ΚφπχŽΗ—ΰΉ.Νό„:Ξ’ω0ί‹<]—>₯ι•κe8X³ΕЁ›rΌ’Uχ£?¨δC„2ΕΒ°―dωδρ'δuα‡wγ"χcΕΔχν°v`ˆ΄­dX:Šdj·ϋ,)ˆ˜-kŠΈ―§A±Α·iaT±#­Υ[¬δpΑκχͺG ίnbΙʊK¦‘αTPRLμΓ‚Bα1+θ~¨OτSœΑgV§J}HLXHJΧ£τ’jͺh‚.¦œΦRς4‘‚ΟξΧώΠ¨ΓK»=ηυRsπΡ'@o ³s'±«€”ΊEέ?ζ[~@I%jΠ›ϋΎ¦¨AmnδFχwρJ”Μ.ͺμ-»ΤΐϋY§Ό΅ž Σΰ +©œ"ώΎ€ΕΖ›Od–χμ#ωŸwoχaHμΙ²†!«€ΦχΕk„ ˜sœ† •Έtγ|J‚i§5Ά ‘ Ρ+³OζdΐδsRL‡…?W¬)„Q:έύ^ψΘα&Ή΅ζΌ―'M1[˜(« ]_μ9ο₯lΒIΞ|Zι΄.»-ؘͧύ±•!’ΝΦuŽ‚3*_ύΝ]ξAΑ¬έ5Έ†JwαvA}7θΧύ>€:οΑρ™>˜ΗΠζk¨“}ήԍΩg΅9'ΙΥ +ΙuΚΎ’΄‚v@%§C·”]ί˜/"†γίΚΪ'βς­ƒ…XͺΠήΘ +ΔsŸMϊΧJp`OΆeh0τι$:΅YΥyϊΰωΰ¦[ο–ψλFβ(.uσλŽΣ^s‰Ϊ€ΗΊ•Eΐ.XΜ!ζ2κnκr¦vΛ«œŽΞΏybžΪ>Ό}€ %μ΅C3½Š=έ•hΈc7–_†KU;[i±ΚΑ1 ¦ε&VŸzb2².b©s°*Rďڴ7­ji­R§­<Ϋ»ιύrύKΈίέV:ΎΏ«mο ?謕j­ωŸMCψˆ‹ώ›Š!L 8@|μ/΄HΥd»――ž3Ε$; ŸJίΟ ƒηβšΦH*Η5:­ϊ’!½%ψ@²G˜ήξrQRϋ•χ[x)Όvcai\γMAψOΪψ\s-΄Α̞κρs1œ/$ϋPπQBMv"HŽηCS)yœ”6—j[0ΰ"L#φ5§E΄+ΌΑΥα5σKTΌHgΡ/λ7MΞ#+³ inΛσΎ6ΕΪς#ε0$ΧCC]iW.νW"«'LΑ©ι’ώ]"η»τ|5 ϊάAW2€Ηγ’VΓG#Ё‹FNΛ"c6¬ͺ0ΈΏ­]ԌθiNIτέ&6j#0|ϊb'5dŠ(U&ƒŒΨDWr―<·‘N!Π€:—>ΰ eRg0†“!NmͺPo|Υά~Ο9Β¦|ƒχ:ηΩ7όηp‹τΏ}¨:6"ӈ|•RuŸŸ†yΩύ ›€Υ~kšGG4Μ’k«ΐ Ώž9ν§nD‘?6*L–E;G 3ΩKM0οu α¨*…<,vΪρ<ΪB§Ά[Ž!qwk‚b³§¦y­ς‰š?έϋC₯¬ϊ‰ $β†4Μ&ΐXΖπΟ7κnCox¬,Ν%ƒžd€Q™3”:(8_εPxΫzρxμ₯DE}ζo !ZΪΝe€‹sρŽΓ.έυYBNTαIJiψΨ|yΒΘi Ή<Ο)h+…‚ΟΒι›A`2ΞΤc6½A¦«;’H]lΩ½”#A EMvβ¬1Ό0xΰf„˜OgHFτ£[iθ©φ6κlΝ³>――’5C¨!{<)·ςMZ½zšΙ_σ Ξ¬θώDϋ)•OςdΜ%KΠ\5O 6Ι¬¦ξΝψM7€™?<·BΝΐ­„•'G·:•§)Xή,‰α¦―je])j ‹WCQK&Cv”ρCHβCΓŽΈΥ…$­`Eγk,  ΆΊFz(|mλ^ή†ΛΑ_±!ύa‡x0|”ž7[ +Έ?_«”Ο²’›3E5ΒΛiβu{Sγw]|ΫϊΪͺU¬ δ( +WjKδ‰gsεόZaOEc6ͺ"²Ω_Γ™JΞέΘΨ0ηΩ^šE4M"ΌYαة΍„8θ Ζ¦ηβ›šΑuƒŒΣΥNλ«Ρ°‡w¨ϊjwŒΒt<ΛΡ‹D +3Ε<Esz`Δ7-+ˆœΑy„³ΑNΊΒζšt&ΜIa>5eSωœοΠ«6<A&; Ζ—yΏ Ζ[ΓjŠoΩ ‡—λhœ=Œk€–[(3χžrΠΑ €b3₯Ήc‰ΠxγiўΑ'ΟπX£”ΦYΗy#JjFΔ™§‘NG»§²­ϋΜ£μrxˆue‘p0μΰ«Kώ{•¦½]τΦ½ΌΦ‡\ΤCς oΘΤφ²σj š=P†"• Qp‘zΘεe7·ΔXߐΏΊ#€£έ6L"ŠόΔЧ±'EFpάΖ‘’ 3€ί­jNΙ―†£i&ybε[Β5d`ήsμPψ^Sœ·α8%Μd’fnpjΡJ¬ΨyKΰY Z6ƒλΡ'[—α¨\Ϋ«\―. 0– ‹q©Ε™LΊη/Q½μ›0[„nh>o +¦§θ=‘48ΐ˜Ά‘ΡημpΒΩ·1°$u~ͺϋ̎«Ζc!šζ¨`άΡ › *z3?ΎΏx;ΒCoCΈ3=YKα^ΈΊ"…±γ\Έ%"?ϊΚ½#Šχœœ~p©Ίe|1j›Ν3χ[4a‚„ZR¦’½οΎΒ—©( GπT:¦ώυY9Τ‡ΓΊe†-Ÿ#_”O?UΒpžί ¦”iΡS€dρήQ‚JDu»ίχιγ†+QαΪΥr;@pTЎ +Χρά.χ–!₯+Ε’,]!@Σa(satΡDŒv=μn€W4Λπρ€μ>ΉψtL¬ξΌi2ίD₯ŸΪΏŽώWαn:‡ζϊxSΊΖ΅μ+B„Ξ<λ·rΈ~Λςxυ8^ožŒήNӎD†yhΧ8«ίΐ‘Φbύ§jΔ&¬VΣ]Όbd›—Μν©)λcΌš3yκc ^χ`dφώLž°Ι6.ΜΧϊι7GU|΅•ΐP1Dq긜 [ќ‡šcΤ름$ŽKϊ耊 „σ=AθΛOξ‚cώ)·p5¨‰c[_•³”¬΄„b j½iοξ¨&`ήΝd +@V“¦Ϋ_ϊ³μ*!NV§φπΒ―ϊβ7ί +“s|[ir…Φ[V+φGWWo‰qc­Υee7:Φ§G΅θe)Q[5IsΞ! €ZO_lEG|£UǐPŽJέ:—έ€šΞΎnd~bͺιu†Dύ‰wδύQW,9B8έEœe‹;‘όόˁYwάΪ―ς&“΄ν™­»μ%Ί§[GsΖǎοί1uχΕΎZ,– πά„ ϊžΦˆ%r€ΗƒΗJ<ό­˜Σ§i3ΗΛ&‚£ΪzΝ:½αΛΊϋˆΘχ¦”ΧΆ§kΚ΅½U τˆ p$Π»ΞΨ±ϊή·ώςAΧi…šμYSΝu™W†ΙϊS_?αnœRo‹₯ΐΐ΅_†?£Œ§ζ&iδΖν:ok3Uiα±Ž·ύNW9ο’(εͺΔNΪ\[. ‹w7\0Θ u@}ΣΣΡ2½T³Ύ[k%J”΅KθΤΥ‹Θ°φbH œ―ι¨ΜkΥxΨP†ƒAJήŠΐΘ0ς,_ˆ,oώ€ψ^ @ΐ[‡ »‚YΧΙ;υ‰Ψ·JέΞ ΜΙϊδD³­uοȟѸ{Q0ΗQ7 Uήάƒyxσ/Θε&ψA#—άγHŸ2Za’’"Y¦ξw†²NΟΎ!qΠη2T…–όwΉ[γ­γb/Χ‘’}o‹»OΑΘ‘:_¨+ˆλΝΖCIί—.³‰g0οyρ“5,‡ψ±…@šs!{nXϋ“ΎNžVΒψc½ +*I!WsΐEΎzΓ{)$ΥΌ0ϋ6¨έέv?D―ΎΩΔΌφυDβD!‹μΩ€˜³;{IΨœ2‡‘]ΰsμœθκφΌΥΝ·`π3ΖO”$ͺΘ=ΙaZ’§@” P}οεzΒt¬™5ž|MTb—’ο¨uΛ ΐϋΏ·΄³v@ςzΧ=‡ƒ­@¦Φ)W‰)Ζϊœ"wntή‚ŸjΛ"π―„‘–©‹SΠκ‰Š₯αώшΪUg¬Gτ+‡φƒ&[Ε\λP6φΙYΘΖΗ,}ͺU[6ˆxϊάικ).Ξvή―ψHOφ‡~1«θˆ£oΌJ +Ί£ρΐdjxΡ\Έ +α}ΝŽ₯G†{ˆmœ4·ά«D‰³[ΞΛ”ΰ%Πr·cwΛ ηφUΠΆRΤM Ως΄΅vCά6Ώθa]ς* ο— ήγύ?Νϋ‘―œ¨M`€rAcΓυ>u³9oΫZ>X@~;ΪΖ€^ΆΎ υφ<ύŒF(Κ’Η/ΔνŸNΔ’@N}CmχFE‘Ξ ’cb?λ5#NΈ/Vς§¦f­~ Ω“$ŒΣ›N2₯’©y'„r‚‰αγN_3otz» ;ƒqσiPύμ$°~+ήžLφP\ΨΥ0¦ˆξεή&w 5f4FDδ‘B?=αoΩ@¨Εh^|2ΥViŠMWμΞzi„έΡzgόe’»6r?`€3‰Τ€ΰR}²/ς!$d˜„΅›Ÿ˜d':WΚα[EρW€‹χŸ—ΗQqώMrz<‰QLtάη‚ΰΪ‚ηΠn/i(!Š(­b“*«dψ<•]ψο(ާ χAS_ΆΨ3ΞލΏ–!eZEAF(υΒkg4Ώχ‹‘ά„R:s‰pǎ½Νϋέβ³ολμΜΩ)1I&οG·δί’‘ΰA‹£|œšˆο”dχQ/Ή|8·…Χȍ _ΊέLίλ2kiψSFo₯~| +/χδ€Ϋ…¦Œ1O°΄,ŸΫ6 Ν£ Σv…φΖγ„…=S¦›—˜‚D©MSΗ‚%urq§ί“L{=Ϊ™,Ž£dβώn™O•g—‰­‚ξΛ2(„œsοbœ˜W“ά/ήG_ ΕYΒόΖHη«&Υw:pΒ Κ!£θn[XΘ5xΝ£Sαiš F$ίlΕ—7 ΎIΪαF6 ο`’ΠEœVςα»›vΝ]‘ζΚvΏΚΠ‚‰Γ&^8ŒμΫ Ξ΄žϋ’ŸΧFM΅Δ ΰ_sqξΰΉGwΎΉhQCb$ݞfϋΨvo:Ο‚o‘FhgΔ€‘iοVΪ€2Œ―˜Ό·h&αΛΆe_ξ +Α――ν^1 Β|³λ © ŒΥ}^f‘0pœΘ^šΗ\ζRl ¬©4šμ8'.³Λ0ΚΚ„1‘šψ''/·lž%€)Δ<χΗ­·ο&|¨Β i)KήAj“σΚ“ίRΡ$Υz…ϊΙsPKœ›3ΖXΈώ€3%o v?ΩqgΌνd?Œ'E΄ΩΧD!ΩΚ―ΛAβI¬ηξT΅¬έ€ΨΘ―ulύ₯φAH©)ΦHŒFΜ™Ζš°Ζ{’•ΰ»ΧŸθ&2rRθ₯7¦‘gLxΰ§Αΐ9 ΘW@!Ζ;`θ¨L{ΎvΑ:Ψα›ORΟAσZιΙ9³=*U―£©ZΤeέc}ˆžΝ•‰ θΞ‚ͺθ­,rL§Ro†Άj%’N€GM›ΐ@ΰmε'r{–oμλΑΗ†ra·l³I8WSρ‚Χ²€„μӈl½όz2ΘBύάgv¦‹˜k@ϋΖ=–‰”<+Α uΐ@lυ„|;œ]Κ}?ŽwMΛ^‹ΒϋίσšωΆZaσȝ±Tν”4}f¨hŒBΠS5Β1$έη%zzΛ<Χcΐ:†dή0ϋλ‡GΎΥέNΞΓΆ œC>m&ωεξ/$xTΉβ*F’ `2ύHQ:OΚξ’ωŽάΘηa§?ϋFy`·+‚β3}αγτΔΞ5aGώΟ«@gΔδH˜‹sώšΉJόΕρvΚhύΎ‘ VͺާΌΕηλžƒτHαS¦»Ώn†bσρš³!ŠXΚ?θˆ¨y…D:ΟRhΝXΑ —Τω*iα9ŸΣk(–€SΤdΐ«Ζsι4β₯RMߏu>ΌwΜ·C”$’„ϊ’μνJ‚~sIZ™C‡ψΑν κ0»σ{εrΒ«y0^{ω[efzμ:―σχ–ωr$™κFˆΠqΪ_ξΒχ °ΡξœL>§>›ΰ3U₯9!{©ŠDj Bλ,Ζo‚χDλbmdΑ©ƒmΨ¬Υmr$Άr΄FEs‹Ά.~Šaƒ%΅ιΈ₯„|U'Tτ/·ΖaiπhΌ‘γH.`ߞ8 PΈW™―ˆθγ©'υŠ’ηζ‹|ςέΎkjJ +φ8ρcLΐ²Μ„6?ιŸΘͺ“ήK²—ˆG‚fB\UΨ*MVsVΰ?Yψe5ς.L&&΅˜€ϊ­wbzBήΓ’°σYάπaœΛzΟe:WjΞςέ†―θ—ΐ·Ή6σM.šςΈΓO1™«ς.?­–λ—θ¨Ž@Ÿ “θcnjψΊ_{τΗψ―­%Jh‘¨T~ϋ ΐ&W(ž•ςζΔYƒŽ_˜‡ςγ~ΒΏ]][Όt!§Šε±Ο7+›ΦΞ₯¨–ΛLQ΄ ¨φΈtέ{ά–¦A½uv±Sη°--žγ€"o j‘°ΚȐC|Nνƒ Ώ΅Θ0“DΦLE)FΐVˆD‚PX£{&{r»thρ{‹˜1A£₯Š4ήYύΒΛ~9Ӛή₯•₯·,[J/Ζ”ψ%4ώžO«χ₯™8 τίπ2°ΜΪw^ύχ£Ίμ(pƒ—ͺΝmŸ–ψ1=‡}΅π› €Η€±ή1-Ÿ˜ίn3sζvηr WΔχΡcχ*½…/<θΤJΔ9Ϋt뽋Ή^Ž3<{=ΣzrTQh\E }΅Q•ͺƒ>SΨ Β₯鞿ͺθΫAΒp±¦wŽΩχΞ5«X‘0¨6Z]γ}!` +WΣ\λ{7¨³π»XθW₯’’6_86Ql†αό5#m[(XŠ•4Y4t…₯Š˜?½$YMυΏk7·Μσί&v‹†¨=€Τt+·₯ΥyόεζAόΝώs~ΛΕi)ό‘ύ>ίφό†Λ²•v1YΆ–Ζcœ™\γv†~„gGSρϊ0Σ|6χ2T’΅k€FqŸ‘‚RhΉΨy£fUKρ²J.fz.ƒύ‚βVLΰϊυΔΉ›²5…ο–ˆZγΐ8ͺ+ΧΤϊς,ΥαΡyͺ,?8RύpεΠV4Ÿ=ΆΗππt³ΆmΎθέρFΊΚ΄–Χ@πIa δ@‘+φqΩ―ΗP(4Σ™s9=}Qˆ~mjΐΟSOΰŠ—0 ²y…εHF鹏!†vΫ·ΓΕ­Zώ&ͺ)χžϊΗ”Ο‘4gho σεχT‰Χ;|fβΘ•ŸfYΜAN‹"μ&―¬φ_R6ΦΗΘkZ3aΦνσ‚ή„θΌ +endstream +endobj +599 0 obj +<< /Filter /FlateDecode /Length1 1388 /Length2 6325 /Length3 0 /Length 7213 >> +stream +xΪ}TuXΣύΧ–ξnF7ŒξξF:„mΐ`Τ "‘Hww£4H+ +"νh)ABήωτσόλ½φΟ>χ9η>χ©/‹‘‰€2ΨΓ’ααŽΚώήήQA >‡)ƒόαs˜CΰήPw™?Œͺpˆ=υV³G |L}&O€°@XLFDZˆ…₯tτ€Λτ‘ g{ `βlχ„ΰs¨zxΐ‘NΞΐάΏ¨ωšFzψMˆ;ŽβΏτixΐ Ώ‘₯€ΒngΒSFHΘerόeτvt‡ „xP9ΥέΑͺnnw„7Ύ0†‚ˆΤ_θ•i€' CλΫ#ΰP€ PˆbώϊύυοJ>ΨΓ𷻁½ €«§ad‘Ιχ{Γώ²©¨xψ‚Δ$bb)Qq€”¨ δί4FφΠ?dŽΥvwτό)μγω§dί?ΊΓjΰίD(ΰώoλ§αƒύ¦›ϋ7ΑcΆwƒΒώ Θ―IΈυ!`¨Ϋ„›ό³m„= +Rvw‚ύA½5 ώ°r8ΪΓΌ!Ώγfξ`u‡yxC­@@όΝΤ +ru‡x{Δ7AάΑ ξςCݝ"β{8ά>ˆš°ˆΈ8 HE1ω ώ¨άB‚ξTΐΣ@-ώ―FK‰„L~AΏ½DEB(?Δρ7н8ΤΫΥΝαό. ”9ΨΓ βΰφ ȿ⅁’ΐΏ-ύίΈ”δοΈ+ρο)©Ώ GP…όνœ  AuK₯αΧόI£07OD€χ?‚ΕQ + ώPTuξ¨=μ/ό§Σ―σ…ΐ!^>0H£*D-0jη  ί¦φW*% 5©»£Όέ|`¨'j\W‡*ΒΣqOΡΐ)…’ηA~εE}ώ΄ˆ‘Ίδ ‡’6χ―tΏ(ΰ(o‡Φ)Œϊ< ΑνQχυqΒΏͺχ†Ω{=RiT6χΦ…Ϊͺ_Θ―Kό#τ_ϋς£#~;\Τrώωv„’.ρ‡€πηf=@²‘.M‘]g Κτ~_ΗDĈή>=Ζ΅XW§υέΌ‘*w~$Z‡Tξήˆ;_%Ϋ©Έmu΅€ΊΪgΖΙPΝ½κΒrtXγ'“ΏΨρΦσΩΠνι±~(κv’Ϊo3φ;Všβ‘ζχϊρu&gKS޳­ξ΄Φy‡«eW},!‡FJJ”=²[9§»N₯Qyjϊ΅6»χ‘Ψ^2£ΖΦyζЈˆψ—7σUόhΠΟ™τΫΫDžΊΗ9Ϋ +ίϊ%θtަ8ΦϋΥi.ŸsGwE†s;xš%ON-έχe1¬ΕΜ5­Άόt6έZ3SMœ©ΑR<•ΠMFLΫp’gΰ%Θ†ifMxώς;œE˜1 ’ΰρ…©Hœδwφ:°Ψ=P#§ˆYV[l'Jρύλ Ž—†χ£O†b +χ―_] qιq™t²΄‰Σ;±N»@l‘^rQlVφͺx‘Nξ}Έ'…γƒόΕq―ςh¦l@γGO%γmnΧ₯TΓω?p_π'ΚΑηά 8—kί'[hυ/CΚV0ƒ[θ―χm5-ήˆPϊ‡EO2qπΎΒ&S”|«IR—αΝ4zwά‹'sD―.Zmξ/A³c”χΣφΓ·Q˜‘?ΖΕ›μSΣ0]Ω ΦοsΖ›VlΟHw^+0clζΙ…KεΎ™•ˆFcβΏ΄―Ηδ5zόN¨ώΆζ(žr4j*ήiσ΄NuΰώhΓZo―/ΛP₯‘Α“uNΌδ?ψ' ΎΟd^<;ckυ£+±UQ:kŽ‚Nzϋt ΅ΜŠ Ϋ#8έ²ί°I*ωioΉ8πŸΫΑ†θω_ήqΊϊdNυ©ν £›œΐ<ˆc’X–ΛΪW΄—WR'ž]l€OΟΘ– ΧŽ*―Yσem8αUΦ­wΛw’¦HROS[:lCίc˜(ήεuΌ«€ςyνΑwνΠ‡'―·”l31/0³R«vοΑ†—¦Υ‚VϋI} Ρί½ζzqΧΔ o*Θ’qΧ>\–5g–{ΏˆμΝ»ςΣχκo"ΩﭝͺΡλWηΑϋVKšhΡCθyήμΞG…μ{ήc8mNe(άοΡλ<ίθ'»\|+―*Ψ„ΖΖχŠϊ-;±Σ±:‡οͺ ­JΫ£™₯υ,Ž?λTj€”NΦUyά/:Œ0œΩ€Πεq<šηd²]z°]4 K‰ςΔ–εaτ―ΎΏ:SςγΌ.ρWD·V­0ϊ₯‹΅ŽŸ±9ΙVPbf!οΥ¨‘Ϊ\lWέ°Γꆆy₯kςfΆžρ¨ϊΌM©ν¬ZνΈ³M»#φ°ΞO4—™wΜέδ‡z~’ζy }#.Ι/;ςT|›EŽΟn‘ΥΈο|Έ]^OS2œ‡ηͺlMj^pr’Λ²δ™ΎΨέͺ9ηƒ$ωoήτY½†P~I²’J――πŒ»‰΅SR_ƒSC˜1F[ΰΕ·dΗG^R>ΰCύMτTG¨ ζ:¦t1†δ°Ψ.,‰_χταώ£ιφ+ΰε)?ΰZ ΌDχ, =Ϋ‰Ž6YδΟN/ό*΅ΝΝ—~{2—Q…F/ŒΐΆΆ³~έbΜKUEf†&"‘uό 1³‰XRγα¨72lhˆΓ9$€f΅άg>>0ΙI^86 +?Ρ΄2z'y8qΎβνΙܘBηKaz>IVphη;Mq¦Τq…θŒΤ’Hν›]`ϋ“-–Δ…‘f₯ϊ½gΒΆζxo—―!šΙΐgdz^TaΐŠΰ±‹ζXτ|YAΒ +Μƒ“+e>oTη1εΒ-fθB.‘ύαΛtZν²yJϊιZ_m¬°yβI*κ–#^Pl‡άj:Γ¬ȘC¬χgωŸ>Σ$ώ±Θ‘χξ*˜ζ.‘mζΓrΓvό˜ψ ₯»ΝZ™ˆ'Ωξ ΈΣύ1P·£†*έΩY¬¬£έ}υηNd ™•zƒ­8A1׊’!œΎ•"Ι'!Pξ†'¦φκd§ƒ5΄ΘSΐΒoχy_Ύ5Σξ†@γτ{•S“ΕZ£b}φ»όΦ§“κ‰3ώΤ)AM’aˆ°"š&ƒΌ»ΰXy³ςŽςEμ…Βr χΩ°F&‹Θg—³z7τΖfiεΪχΟ0–¨>sˆ‡CœΗu—―#ŸeζK\9<ďυ”ίzŸΎO;ο±E³I ?`D ae$ψAΖΘ 3&;ΩdΊα;ŒΞΚJ—N$œPΏΟmνxΩш™μΰ[‹@ 2΄r–ΆOK5#(ζ2ή'KΜpμ8Bnh&–Ήέe‚ΎN Β έ¬cUd°O M„Ό%&·O&šσ Fd1TαR šξΝ3—ΤRš[Y1³,bjiKήη]σMYt`±ΑΘ»BΟτ˜;r`¦ΦiτΑm֊,/₯:ϊF$¦k2GYνυδΆk­žœ#_†‹/0ΓIίΦ !–ήVvΎ0Η Γ€εΞ2o"ck)€,N„D„μ΅1œwί‘ΤBΜ-–”oΜ^Υ¨ΘDέ©)πώLΪmc΄€π˜ηΛ΄j’ΏΝΌθG―½Γχ±B©¦ωsτ2[΅Y„υΘ–©©ΰ7?ή;Σ1Tβm™„‘i—8HŠ9’9™†Α¬ ξOqΗΡaRΊΓU†­mλD9Όgτœj†`l*ή"ͺΜό’Ππd—;ΧΑ£―φβz° ²ΕMφτΛ_MŽς…tΗ«€Ψοτ―*'Ό0Eš‘H?ρqU‘ύVΡx4k¦Τκ,ΉwθSj©h—©Œ5uNν”²œHΤφΎ‰pσ½)Έΰ³7”#―υ4<νyΩ§^ρ#Œ‚μ©"’»Ζιμ!³ahΆ[yTϊΫκζX<ζ c΄jϋZˆόŒ…Ο³\&@½+ύΩΤE80<²D{ή¦pά δ;λ­`΄υ88ΓJ˜ί—ΚΏΗف&nΌ  ήέD*)JΞζ'{βpΊ¦ί―Ÿδ:xJ­rΉUδόIύ=¬g―α~WΕ°VΪ|ϋ²ηZ¨Ew–π],^£q―ŸiμΦj$ ξM }ψχOε[<žŒοZ―(O^½ΔS~Μ“?Š1RyK|γϊ…!{Α[ζl ίΔΓΛe„ιΫΉq.ρhωΔZώΊ’ύ–mͺ»-ΠR‘[ήm‘>U½-ΥγρΤ¦ ¬ό➩9;ΊAν‘ΐυι‹oΔ[ϊ‡u[όΑOž·j0yυΫκ6χuEV˜Δ#žΟμZ―Ν +ίB8iϊEοIL=ru‹©sΥ’ιo$:œ=?ά– ^ΐΫjo Ύσ\²λjƒ{Jδ`κOzͺYQ߈cTΒΈλ72†Ή€''ΝτIΛL΄υΈA€C_ͺ©^·œ;θΤΩ©CΪ?G2χΈo$Ω„$₯&ι|6Ώ&ΗsΖι-F;§τΥρ›)ŠΫvίΏΫόψŽ+ΩnΈΝ= ,ΰΕΆhδΎκΨ¨*ΎΣε|Qό‘ž ”m~oAE,$ Ά“Υ\7‰Iϋθσ©έŒΰηF) +./ΘBμΡΫ"ύ>–ƒŠͺμηΔ–\lWИ<<6½Μ Θ‚²ΞΧΰϋR4»τ!ͺ΅³49ZΐWΜ¦d Ζ £lΨΊβ+κnΫ¦Cπe¬Gζ–ΉlΌ3{σΟς€n…θόͺVN$h/uTxγν~HαΆι…Ύ θ+|§qdΨσ᳞ΙΔCέκƘςΛ‡ͺ—I’—”Ce»μcaΚ§ΓZ@ΤηΖo»Θcϋ`Ύ‡b”)#x­―½ΌRkϋxΰ]:γψρ•#»#νcΒ†Ρώ‚~¬½bšϋψ/^ίI›²΅Ββͺ΄—N-{Ϋ€y;Œεθ@9]šΉ0°υση³NσΦS°§ˆΝΙc'βπβPg½7O’-XΦ―²&ψϊLΑΠ4ms¨[˜h - %μ—Šͺ 3ΆΓ/NSRσ%ŘR½ϋ€ +«}»`κ/b…Η£ + %° ςΡΏ²χ‰8 βvž›”ΝTl^τ5pRnˆΤ'œδ^}X:,ξ1όX8h&m:eρ“ηYσ™ΔN$g+–IN:'|Όy ¬V“:­vρΈCDJδh³žΏ·wVμ +HάͺξUΖtχNъtQςΪ?ςŒΌ4Fλγ —ΒyΨΠƒ³Ρ—ζ„~v`›?­Œ’5€£g˜Pεy’Λ«Ά΅³FΈIγ9υ΅²/ ο“άu#ΧlA‹·›C―I'ŽΣtνQΛlϊρΚQ 3εΝ +erNΙLφŸ£^Βλ­›‘½ά-ΥΙ>¬₯€fEίϊ>χ9ϊIͺ­‘Γ%}d¨xη¦ξ}Ά # Y’LΑRΛ=Σηs‚Yƒ•όμHb9ΉZήψωtfδ:ψ¬}ρ…ν' [d‡]’λƒς ΌΉ /φ3β&'Κνhžδ<㑝tƒ‘γ»u#½ZRπBΤΤMšπ£n„T―ςΘk¨ξ„γn‰Ε6βθκΪΫ­ΙηEηJiΖ­hΦD‡)λuΧ'%ΐ6£pV1šΚA Ό™Ά¬`-§$JJ%Ž’ŘagŠ yu1’3­―#­—BΈ?Ώk₯t~kΎd +q@Ί ω-Πέ»AΩΆΞ]Ρχ*ΦρΜ3υο½λ4ΞΈ gΣŽΔ‘€»€ˆ™œœ±8:χ€ϋdeϋΚαύ<δ5"όΤΙ(ͺpu‘zKΐ$ΩΟΨΒyξ‘³η.ΫΙ½š±B;‡¦φ­pD8IνΙWΕvzέyl +ΞωώOK'8Σν%ΔQyŒdHΖK%ΓΜQςd |qMŽ,(Žββ‘½:)@¬βγZ#λ +£7WΎ š…ŸΰΙKΉ€ώœήlΧσƒέςΘΊΝ‡qxΎ\$я‚jƒ²ΝΫWRSE|;Ε^l¬±c—BOK?ΥΤ:EΡ„Vπ Ρ›yΠKTτΎό‰Άφ€ χ.w| ”R…n1Ρu+‹ΎΦHLΧEιQ­ˆ.dϋ}ϋϋƒe\>g ͺ Ї“NοΨ?ΞΞFT1ϊ˜”&’ωSδ84‹AΙ±ƒΨI|•ŸΠΛtdπη/¬s!FŒ“Ϋ,˜^Y…΅³/gΑω2€½,ΌΞd·.ZS~m0ψ΅>[’GΊO–™άIœ*urU•©Π€α8‰dž©ό?0α +endstream +endobj +600 0 obj +<< /Filter /FlateDecode /Length1 1873 /Length2 10572 /Length3 0 /Length 11556 >> +stream +xΪ₯—P\ιΦq'Hpm<oάέέ‚4ξάέέ‚{pA‚‚»[pηgΞd&ηΜΉ­Ίu««vχ~Ύ%ο’½«š‚DA™NΠΨΦ$fkγDΗDΟȐ1798™Ϋ€d œΜd%ŒŒ―˜‘‚BΕάΙ +τ_j Gs[3v8½2§Wu1@d`b01r9ΉX@F&ΞΏ mΈ²ζFf +€²™ƒBΨΦΞΝΑάΤΜιίL^Uώ>Ό3’(˜™[™Ϋl­¬ώΓIΦΦΨάΔάθ_* ψ½ζg§ϋCν₯ώσŒ‰‰ξU,ΰ__υ»š;™ώθ’˜­ƒ)πΞΜΙɎ‹αE&ϊΧ υ«―8Θδπ/C·³###ΣΑΡΡ„ήδΔπκL!jc,lkm ²qrD`b›9 A¦ζ6 „Rq³˜Ζ “?ο_ΰ`ώ ΝψΪu&γŸΏιΎκ6Ά΅±rϋm.g` 0(ˆjͺΚ‰ΠόsΜ[ Ω~xΠ1³t@fŽΧ‰½Žƒ ΰυŸ Μ)bόν,icb ψKΉ±³έ_κ]ώ\ΐ»_+B ψΟ`rΆNζF―]ύ_G­ΓΘΚhτzaϊχλ02osώντΏNϋB(ώ–νcΕĜ­¬ώ5Ž«ψA$ ¬ΜώΩ1ks+·8όaOCuΠŸΩ•@¦ΞVGωίφg.AS+€Ž ψ'4w32V0w22˜X9‚ώδͺ6Ζ «Χ΄ +ΆŽζTόκΒΘψ33s#K£#€υΟ#ρ?$ˆΪ½ΆΝΖde{홃γλNYYLσΧH ―Ήθml^]vΞN^―Γp@ψcŸ˜X8 F―ψΑόβ0؁Μmf@ €Α™I„…™ε7bω…Ψ~#Φ_ˆύ7bϋ…8~#Ž_HπoΔό+Φλ{μoτ+ ΣoτΛ‘ε·fΞ_ˆυoΔΒό qώF…‘ε―πBΏΡ_αEF―]bύ­λuωDΏu±ώjΞλχίθ—ˆΧοΏΡ―N°ώξ믌¬Ώ›Γϊ« ΦίκΩώρ[=Ϋ_"~«gϋK„πoτ—‘ίθW'XΧΘφ«¬bΏΡ/©lΏΛfc…~—ΝφK=Ϋο²Ω~©gϋ]6ϋ/©lNθ?–ΧΘΩΑαυέό―—Ϋλfuobώϊ@AF?fmΈ-jΫn«ρ]ιΆΗΨy‡χΈ>α/n¦X¦Ώ0έ/}”ω‘ΗCι—.iΨ&ψA>Œ,½GŸΟσ#†‹1v*―¦Ο–.Ϊ4Ιεώβύ+φ—G’HΝ—MK›o7%±˜ 9aά?˜ΰB)«MϋΎΥχjήΤkϊα¬d£. ΤΘ§†μ“gRˆ³B]“”Ÿ< jrΤΰε,2b,w’Δ“?LΞ·@%Ϋΐσ΄@fΣφ`‘Aν»RSά¨SΉ£˜2gC_ugv≝[mD3SΛk[€Jmˆ―^·ˆΙγ©h_p‘)?€΄λŠύΎξν²@Ϊ\m˜ΗfrŸd#w<*έΑ‘3₯€\kWQ«ΕΚ79 ½AœL#ID²Jά M› T-ΪόσΌ0[Ρ$Sο―€φd‘igSΌςΗΆβ·…Yk-ν<ιXSΪŸα‹Vk +κ”DŽ“‹/ΟΎΙœΚΏœšσδU”ή!ϊΫΡ‹rγZGϋšλrΑ>?Šσν5άb΅ΌΑλ5=αχ KXώ ›‹“ΙZM2U„Υ“SjΖ±φ5aΩj$Ώή!½ λΑo{tΕon +Ӎΐt0I܍ʒηgfΘTΔurs―€ wšκ D/Š?^%ζαDπTΌ!/e0›Μ·­άΫςΚΒμΜ£ΦΜΦ£7ΐΚc¬hλ₯Σa' Τε—1΄Ζl‡p’υCBΑzϋGΉΨU4; ώϊŠu/ΐ—τΫ3»(Ε™ψΗ•οΆωΩΕ©hϊΑΟωΡi)· βjJfR§gΓOv0λ?HΐσŽΆΣŽCo»¨σεbt‡c0'Ή4m\ଔΜύˆΣy²bγs7O1’iqwΕαΠ簈‚φ +³Α έM΄_κŸ)E}Γχ°ΧπIθX[T–τ:pΧψΙN-Z~Ίjψ3€agq‰¬}Ž +·4K1”ΊI·ί +ψΨ|d9ΐ³½Z‹lΧΣͺG_ΌΙΘ©ί€½ΰ?γ’ά9wž»Xi@LΒOΉσM ΎGζ¬·Š‘„z₯Έ§ŠΗΡ|ˆΧ1>I!A}„ BΓλ>KγζJVBuΪβΊGʚ7Έ±„³iιλfrzw©lŒjιW« +]rWΉr;Š5―‚+‚κ#ÜWVv5σZ‡КηΥΏG+ΒκXύΚξψ¬QkοΥά\1}‚λί}?tŒ6ω„EFν(ΑΗD»πΥ*Ηφkξ9aμ^B=†Έ‰d ϊ‘γ:qœqXϋŽΥχD~W AŒοWυ[FŒ™{p|㈰wyˆφ±οΛΏ!٘Ωp7ΰ°d;&Ι†(­νŽ—ω±O$GiN/&»5Κη2Ȝr“ώ€e<ΛΎ˜ οܝG™mΘΪ74‘Ν5%ΉA—ηέ«ž9ΰ3Γ}!£Ζ”\ΥďEυΑmjG†΄₯NOg›…#(Χώζo09ΝζβΑXMioJ5ΰgm%^>Ω##‹Ω@»”Σ]ή–,_»Ύ½CxΘ9IE;|r €ΦΓΕtWω|)NͺŽuÇЍκŒm%/ϋ–¨Ώ£–[;Εύςη:@™tόΗ₯ξ „hΘΜmωV"₯‘gΰ"3t›ͺΦ0αΦ±sO}€GB!«—½ώ=γsK?¦"]Q>YΧCj¦ς‘”V·œF r—πw²Q–Z_ˆ%o§n’xZ]ξπZ½$B‡½4βι3w &·Cή·π“βœΝ]5όϊΊ‘PRΡAd~P4ιY0‹…ϊ•njlžCœQIΏ[SμπaJiͺ]Άθ BΨ[8C½οv#–:"…1οD&Τ«ι "MΑ‰jT³*¨61+֊Δ>±aΞΊ5FJίE΅θE)ΚS3n'·[ΥJζθ“~Z’υΰKg„Ÿ½(σ‚τVψ€[PͺB¦₯~v•dΦΪAΝpρ¦εZE"M(. ξμR²i?ΥΦ‹U !:Ά+šΤψYΣξ~α‚Ϋ\™e3‚΅m³ΝDiy$%/χ4e»~«_ωα’–8ΕJ¬γG,Αε§Ίε[Εζ Ra…A]'lQσσKΓ8ΑοΞΆεT #~SΥΥ­?Ύc°ΟΦ°­nΡ`¨QŽN†ΔfΛΕ`±§ύ|tU3cψ°]ψ™Ec™ΤΫΈu3Ί…Ž;΄>(οΓ’_•ΓV> = εμJd,RΞς„Rn7ή#ς4Ιμ6άθ™ΦŸe>ΰE»¬‚Η(•Ÿ;²ρ΅9#ζvζ'ˆΘξB7Ύ…Φ’έš’Θ6€ƒLŒΓ|^3ΕΒSh:Βy55V)»•—„eΌ'm·Χ!ƒϋφκΊ_LΦ{™ g”™#£zeρ Η²ͺZˆζ|#ζ֞©&I”GTΛ`žE˜2v™‹²|^τΜ„ν9ψαΦβρt$όŸŠω!7!R•·‰η{œπEn«'’bΆΩΪ–;yΜΣ\&!Ψg—4ΰ ‹:Άc(eϋ"εΪ­ƒΌ·PεZk˜”« +ϊαΗΚdΪΖ•*jΈIƒ•πάNί‡z§PƒX½ΒB¦±Υ1­Π οd…2ςήΣ1 €T«‘—Μt««Ζλt!ζ6Π©(Α-ΣqΦ5Ρ4•QOšΛ$†r΄wξΧ$²½^“NΈE‹ΜM9σRΝΩ’iΌ‚£Ύ°β¦\—ΰ&ΰΕ<ͺjΞP’¬žΡ~*¬2°ΠΨcΣΟκ>κτ&3ΒΙ ΉφδQ΄W#Y9'Łθ/λ‚q蕝%žΫk#"Θ›β¨T-  |L\†Τφb·Šˆ[rΈVΕg–£ +΅‰ΒΨ-NϋœοOπυzi—»¨ϊ?χaΠZισ9(Γ`{…‘†σΖς4cNέΡάΣk’-―Φb“ŸD /$βZ»νOuLfτq(ŒJ;MΚΛF­MŸA™Σ7ΰ ΥΒςqJqΒŸ­ED₯¦θ"*Ο£{γψLRΥ)~­4Η΄ι‘oα₯ο3b“›ΛξBΈ W+›dόO„~6}&ΊΒ6HΗqκΈψψIέΘψ¦§ύΨ +θydΑά©wE ΓοΨw,Ο%hv\ γ^šœώ£Υ”_iFπeΠD oσ•CB‰4~»&Όπιi4hΚ°†ΠωΆF4WΏlLΞ§‚Xz°•±GΈžΩΦΩ !ƒHΝ<ΰΝφEžθN–‚JςVτxGq cNk%tΆ²ΥΓπ06,?.¦e&kHa*q‹ŠAΣu`Ζί’ŒzίnΦI|;uΥ4ρ.υPνψ•Š©Αες³ΊƒN3w:]`Yό5dτΞ^±Μ»άbΜ DΝsΕΙπ§sΦ⌷`S kδα}o‘K64ˆΛ²[χk/ŽΠ.ˆ”j‘=ίΤNΓΕεΥ’ώFᡜ[e"ϋζ1ψ]c°FVp6ā‹›ΠΖtΩzχͺ5Ν‹ "*’ˆWlS Dά‘‘€Χ'«^θ™—qmώEL +wx±X²AΔΠ~’Ρ{o·YΞߐ)KjU ISρŽλρβΙx‘V}xΣ1Ηξ,ˆ‘'¨vΥ†ϊ˜ά9Υn™νxΠ¬‰œPBΦΛΏwޱε― θwλΕΤ©nh_‘1T3–ψ!ξοcΥ6J&ΑG΅%Σ^•QW]αΜp Μ·³€*­1±a€~ή%\w°τ1ΈΰΎ`.YnυρBν7ζ?MΔυxΐs!Υ?ώXυP»K=Ξ₯ή'€ό’Ύ†6#rέA’%c―¨QΩΧ%cb‰˜:.L,ώ³D[8yπapvΗΨ’¬₯Τςœ:Š\OσΝa*?γ›ύμ)ψDδL-όU8bY{φΜmϊ•Δ1Α™ξW[§ΈaOμήIbΓ€ž,sΖ( +Xϋ{ΟαχΌAΦψςδ¦ωζΉDSXίρΊ©ε‚^OΓ+υίdrTϊΙΧvaD‹Η„Ρΰκϊ—1J)™τYϊ{σζ₯s b/Ÿ\Ε«Α₯Γψ; #O6K ΘΩ{υΠΩ •ͺέ…J =ΩiΆ‘-2BuNRοδnP¨T­eEη§6ŒΘω²½:Θx7iόσΓ3ͺΔΎιxe“N3':„"0Ζ™O%Α;Ejͺδ¬fž>πΦIeCӌ1Ο<­ +<[“*θυ]iŠ,μx{«μXβȟ¬βλ§cψYfΞa`ν<Ÿ‘#iΊπM!ΰ;―:I BΗΝX+ίμv0 r΄Zi8`Τ ή³ΰu›<ιΕkέӘ₯εθp’–•Ψͺt‹ε+«~2@ΛιΠτ•0©„_§zΪΚ·ω‹!°jSΌ$ΐK„ο}§ C˜g―cΝΤ₯ρ%ψξKCίbΌI5Tc«”biቡUέπo½>©ο€πΫ’J’ηKΙ)%lfΒ:ϊZΚι]΅m”}t"Ί£7ην‚5°=fOVR΅Κ:λ;‹Ό"G’Ω +²«JSΚ7qh₯PΟ_Z‘*6ΔΒΝ$τ’ΡYρVςΪφw=Ά&7P¨ΒF€š]J'ž`N |π8Ι0odΥϊJ©ςvΑ°ηΖόy +“ΓΧ ”dΏ‰ϊ€3"e·:ΑcHv?XΑDιkΚΪ)ΘΌρˆT—…‹kΧuΆGΨqΉ§}žβzΠE£4A_’ΣΙη•Κ¬A]*€f5Žuε)αΗ₯C° ­X¬ά(ΆvOς€ϋΪΞ(άΗ>cξoΐ£1ΖΘ+rλλtUΔq‡έΒψ)0Σ&J™πΜαιΉgŠΓVΐ©mά¦σ8(* A™#Ψ‰’‘σ‰FJqŸ‹Wα–*t}Δ» qޜԒ‡šΊΟΉby£‚Ue‰ˆŽI~‡”%:; +R ‘πΩόά^ήC½,p”Ύe‡ŽΛ₯ϋ ‚‘]|’Fd +’cά”2ύ’­μΚΝ4kxυΓ}πFω0·X}ΣtΑWZ­%A2΄OμΓRRvd#³•ΏΗ­/HσΒς}D|Σ %₯ +jC‡φ£WLGuο2=xΚ=>Oο˜•b-ƒZΑΣτW]’QΪ‰pλσ‘ ,„ϋ-θ†s|’WzΣc¬zκUΤOͺK/γ©‘η΅ΟKΚ –>ž±μΫFzXŸF²¨9©nΐͺΌuyYtŠ’dα—g*Pyt„ΙίΚ¦\ώ4Ιqθ!ξ³E±A”ΓJLυF{ρΥ΅bΛ“UΧ*ͺμ%Ύ\GIΧ;Κs㻏θΫ0Ά K‘fΦΩό}ΏPJbD<Ξ^½lUΛ4Ύ,yΜγb)(ιP~¬ί¨υ\¬v/­z€Θ>\5f·«–υ±+R9ŠŒεέϋ’[‰ †ύpΙΣη•f‘37i(lβ7π —π•αLΞ6UbΚhρ‘t&ž¨άΠΰϋˆF[₯“Στ.€βό b +–2 ƒΏ+ΕJH•α±>―K0χŠ[¦'η–+ΥoŽYF/ΌK@ +`Ϊ…ηrf7v½°`λUΆ’Ι³“/nξεΤμ&ψ!6ΟΗΘH΄_PJ0rͺ―ΑH₯‘)Γmδ ³τ5Ο,ήxwAΘ胋…(·:ς„Ι›‹₯ΧEΰYΝEc"ιΐwKyNŽtΔθ%²ΐ­#ΑXvލ +ΣΌι+%Ϊ %=Ξό"S‘%η¦k hξς˜/YšUπρ{ζuŒlϊ +Λ³€« _ΕΠΌ7€2O­YT°MΉΫk,;*q£s;mWδΠξύΜ¦€{#}mΧΥχͺ… vh›/ΠίέNΟ½w‚27•κYt«μΟκ«RΞbΨ( ιLΦԟumTˆ`ήٜγaœFHUω€4JJGg&¬ψ`f6ρ10/^ρ;S σbΉŸΡsfήΰbsΖΰΐγkα#pφ(C•ŠΎ-’Z°­9•™L,˜μρŽ&Ώ\‘ξ3VD³ΧΜg]OnψsΆΨKWΊhϋΤ/_ΈΘG‰οπ3ƒn; ŠΡuVπPχ}>ΜbY1υPhωygπGsΆzσλ βš13»AiξZeHˆ9inψ΅ϊ5ψ›Έ–ίσΐ€>ΐτ + ψ”Β–ža²‰‚!‰·Z›ιΩPΐH&oX +S½½ΊΨ·α’Ι9Ήψy‡°‰?`Ψ²τƒ5to!…σR•­₯ΦVK§΄Α|πΑx“΅P•o‹{6sTO‹μ\£y@:#ΊƒŒ?υΓf9‘wr‚ώWRCwΘ·ίΨ–λΥζc3κ§ζβ.p|ψΉΫ!Α¬ +Άφρ>UqΞ ŠJζX†α<ޱΌ9ˆΣίdΣώμΧ―δν«¦^~GΐΪF­YΫR[ϋΤo2‚Ρoj΅»¬e˜Ιϋ-ψ¬˜ΦkΙIόA@ζΌrRΫ€σ’«79rw‘ŸtηY·S}Ύ!½;}mΔϊ{4(Řέ!d―ΛD“D4Θ\>1b3]¨€Ώϊžϊ}]±iψΊρ;4…4RηL€¬€C38E<‚Ρq₯’x4`½£1|£Ω’Αεψόt`]qΉ“‘΅p΅~ŸΜ$ΨΏq΄2ΉŒ£ _0ͺή6ƒΪ§8y~<Ί‘ΣΘ°γσ†΅6Bf[—<ΧΙΫν,ΣΊΚΐyΝ¦ Ÿ&ΰP4Έ½w|ΒΦ…·ϊG ‚΅N«ήΓΉ>Έ7ζ W#Q{Υχl°ϊ'g²™L>9Βήͺ•ήcΕ Έ«τέ„ΚmΦσ\§gž­5:ρ™βϋΨr{λκόcΜsΨΥωͺα&«PτA*“+Wγ`±Τ#›9@}qψ]…’•λΥΦθα‚œΜ‰γSπνΛ’‹εUs8Rφ;Β\½žN Τϊ¦VΨ’=N°3m.+Υƒλ\Βs΄ΤξέΫδΗhΊηβc¬ζEH*¨½§žΖtΩώΫtΛcm‘ΙQae§[ ₯1.©pfρ&ξQ'~―ˆΕϊΰBΪηyΉ—=•‹8™FK£ΦωWŸθ3H έXF‘£šΈR}Κ7pή†„4ΑL<š|oX0σΙξΤNή§W½$ŸΕ{λ9fλcŒ“!zNΏͺj·KHν-.œΫΒΟ5©ΓH‚%9iP;›+όŠΦS|9κ…nat? β‘Θ"νxf`u]΄‘λήzWmλښ:ΚπΛ…€ΞOΤΪdR³ΐΔ€[š‘‰2sξ€^φ|Λdα‘F½― +μ€2•N!2―ά κ)Ί|_ΣώυSh'jϋ³Bθc˜qΝsΩdΨΙμϋ˜%€!¨o ”\9μ uίrbA Φ†<η6Ζ'ΖΕM!ΗΈ?έ–³Ž₯@!²hώbyυΈ³Aϋ©»œ£!Β²Λϊ,9;ωέ&oκt4!™£${}Μ$³τtΉ3g‘Ε [adrρϊQ+]žF‚ΝKΟ!œΧΟ4˜£λΗοφΘy­Š«J5wυOHONΌgϊΔZ<υζ₯L›8Χ]Š|ΎP;wn€e:λK™Ι„‘’ΦJ“’&†ΛL1ζΉ£σγε*d°I΅Ύ~j-j΄ςξDΥwέ…]@@Ί—ΞεΤ©- +DΊ¦]ή|©3£w·ϋρEW‘"n ;/Ή‘€q»#-v[‡ˆΩΓ‰½eχMΜ0°Ρ?~3„—ΚΙ^_Šυ°™hΒΐQsQv]ά”Βu}GŒύ΄:«Z¬ΝŒ‹€9ύυb&–TΉj:sίhš»Sc(υΡ‚‘§Ω «2ϊΔTγˆesFz¬ͺι*Ε*ϊΦΆhΤzt1˜΄Ί¨[[–έ΅ π£.”JŸκs$ίΟQŠθΊ'`°Λ„‘œI6_¨α+5ιέεΔq\†B9Ž πΦ +vbUΨju\υDΌΟgΎ·φ£y‘Q‰°όρλπ[x€:iω²–bΰ˜_ήηΕͺ#ΆXΕΥβ©±β:£?ι=¨ŠSν©!{.z ΅σ¦’όRβB‹T4π…q₯­χ<'δ0-;¬kά%ηΜνm'„«Ωτ½υ^ Στ1xΖ ²vιΨxμ$%Z@Qδ"ŠAˆι§₯ +2$|΄Έw½-@'·^ΚίW‰υQ.Φ¦Β“μvvΒ‘oJ²9ί‚Œψqϊ\]k’δfv€?Dθ“ € eCF,R±6/―œaad₯Ζy$|έŽξw–s@yc(~:M±δV@ΠέX­€r& +Βτ 9x¦σΚ ν,Sp6Ξx@ΛΖΎΛ‡w‚Φ ƒ΄L›Lo'γQAaЇ0mtUX*–*'‹_7•#ό5kYqY Λu\ ϊφcή~šξΘ0¬HχZΉκ{#g!cšοΐG.ξ%(n3ΰζ%»σ:θΥOk“’‘tΉmtΫJžιp3•΄υ»“wΖ}r†ΚΔyŽΧŒβΑU¨¬˜\ͺ»Gͺθs^ Χ%Ω6^AβCP ²D0jΪσ–Υ opέh­ΦσB\1??βTΉ†­Ο„G’$#ŽsCaι–ƒ‰oGPYΛCO‘αUδμΕ“~³l*`0Τ‰-ŽQ•ΰ4ZήXfέEΫ€±?Z]b»’“EΔΒC-%rŠ_‡Κθ:fΨ!we€O₯νž8ΚνŽ#q —ή!έiuΟ‹e™ΖΕδή’› +ω+Ί‘±R Ν •]jyŠΐΦh<Δ³μέ@΄°΄0ΰ<©πu'UΆš‰6e_ ²`gτh`‘ΉN +‚8‚`ΦxηDρΤTαSπΛ{™*»ςO¦ρέ“iδ|£6f¬„τqμ<Ξ» ½:tc>’]ώ( Ρ=φΜρo€cjmηaΩlIΫ₯£»ΧΰC‘V|pγμc\‚fγΨΉϊ?l+ν΄†ΗTΖ%"˜Γ%U12tžθήΟϋΤiš(έγh9ΡN‡ ΊGτΞDՊ +ω+u‰ΓHF!|xZ‹X,Λ)ΫD³όi2ύP,ΤΉ²‚ώR νΩV‰*iθ…“΅ Κίe"0{ +ΕWA‰Ωͺ˜} +³₯ŒΨhM$*ά-uφ`žΐνyžŒΐ'6κη΅"s’UΈj{` Β]xZμΙΉ8KμwQI•˜rέΫ”*Œναv΄@0F: +Ύƒb)Ό"dwӁ– ΤmΊζE)Ωσ-Β#\οΐ€'@™5}O~έlA@ύ¦8€ k`I(μPšNl‹qw=χ)ω€%=‰¦[cnJ…J2δ€ϋΜm‘}eιτ‘O^ύ˜ϋŠiτΰΪ»ζG€" zjΐ]λτδNg(™υΣςf‰" ΌΆpR6 hΧ§έΏ°ι­lzζzκΉUŸι†€S ²]΅ΩυŠW›ΐε}$R²₯&Α +R6ϋΝ7GφnkΗβ?€8S²aXϊΪƒuoB€―Ζ7„)λυ6κΟ0fΞ+l4ΡM‰+g²ύμ³ί_œΓ{kΎΩ(A·ώΖΫΕfΛ<€œΊj­eΦ°?9υpc¨κWT$„{‘idO–hJa'y1™g”ύI¦ σO-‘Q₯ο|ΙVœgΰΛ•ύΚd²Ύ§›Δgς°ΜΔΧ „£˜5˜EŒΎ΄ΙΌ­© œ :X^¦d ^ήΜy‰κ6’ι5ͺ°*{­8ΌWΑQ2-ΐgSK—S•4±!Vf°mHΚ.L%οŠέRς¦‹qΚΖ}ΫȘzuQo­5α––_ΰΓσcβχχkή,Ζ„XZyI‡ž Tρw~?Ρ…ΫΘΐnžς“siΔλͺ#=7'gbyƒ† Χ ζSzϋναΘΫ?ϋcŒaL μƒ·f’«K±rηKέλUZ€ύiσ'Ρδ1εUˎ%’ΫΈ`Αw#Ÿ;π)šDρ280PφΙtPS:Ί5ŸΒ§B9πκ‚:Ÿ >—εš%β}bΕΪ+€G΄fvιž•5‰έ$™ΎζξŽC°Μ―;yŒμέΔ'dW³™£Μ}Ζ°?e>0[„#½9ύ’[ft›ρD“7’qΩϋέ†π3?ςόZh;Aˆ# Γ}ό›Ό|ΫΣ‡―ŸςPxQ² +Pϊ +Ye¨¨Κδ©ΊΟ&‹ž7lΠ.'²Ο\U’<2.Λ5qi—ΠP <Ψ€O[ΓΊ•Α»ΏΘŠμΟεJeΩlk ݏΎθΖ…μœθ³ͺόΩοεδνΔEIZάHeΣΉŽΌ @ΤΧ.hέM«βζΌ )gκ‘€` y!CZ_•δ:č½Ο†ΣS;™.9f|ΌA'l γž2cyhθEHqεθ.ΚαωάκƒAόœ+8UX“Ϋ˜Z³;†ΤώΎΤ} βŠ‰…X|΅4<­υt’Ψ`‘֜γυ}†Οd»ε¬ξΎΪ[2 C%Οκ ²`›γ~κ†*(γ΅}Έ0±‚σ(°βν­θM{]κLd‰De(΅ztYΥΗœ)iLΨ›Φφ—ηy†Μ ~½b`Ορx`¬IΉ ψv‘ͺΦ;ΉBΝv†3»CΟKΦp™οsl2>DηŽ:±U' nτCάm6ώ,ΏnΠa`mtΦ ΟyίΤJ +†V¨00£ρΦ"i +‡?™μ“mA‰Ί|·nνγK΅’]·δΚΜRΩπPήΖO· ¨ˆ6 +Ξ›ΣαΙυŒiΙKήψ’/ϋμΙ .κ1ΓέK"ϋΔ‹b}*‘7$\qσTEdγˆŸΑύM06Ωή™ί–Gΐc˜Τ]9tιΌertξ†ΆŒηTmͺόB?1^%¬ΠύΑΞP‰0F!ηw˜7 ξ½Κ% 6~α)ι1S-u=ιFš F‘’™£޽8ιΎw žvΊ“R€ήhw₯γώΆκ Γόώ;^sά*LRj0Ά·Ό8\ψV\@Λ ‚ι—½Ί«t%©0B‹MΜržJ·v’³Φk―Y έΦz#ψ…σ…w£5nh`^lWƒώvHaof„…†']CδϋφΠεα<Τ]&Κ/Ž"Β©u7τ‡θWγ“k‹‰έωH$ +L‚C½Aͺζ2*M)g­G*ΛΛ•J€s†I½&}ωWτ*x3ž€κ6–δ lAΫ*Ν?ίΆsΈ<ΚoΗfˆ £†Α3Ϋ͐Ύ!‘¦e¦XήAœ0rOω\7Ή=ύlPό&}Λ.VVΧWψ!Ύ[Xe‘y* qsβαTΑcjέKg\–B'“―ogΛ…dTΧθΔΎ2ηŠw ΰ‰ŽE‘έiP†\u嚈:-vJρlBύl ΆηX}ΛC™Ι¨°―``₯}ϋΉ75ά‹Ύν%ΟRW} *`±]QZ΅‰2Ί“ώηδ;΅:Ž^χήJM(νή£-qυΗET#­šeaXχ…ŸΗΩ;Λδ±e •rΟψwc]&Q­¬ΜjEˆ½`kV-}žœ”¬β™οΦm™cΘΌœͺUηB«ƒρۘφcM k2OM΄|I—`°ΌέΒΌt―ή7 GΡε'yΘsΔ”ΈΊvkd@‰,τX±αPΫΎwζBj0΄ω’0\₯:]ПKŒ‰,Α4ι‚|+T₯’ybdί“Εͺa>ƒ~"gΥ‰hˆΚ*½ΐAV‡Cz6Γ”Τ2V#T«₯*(ό&»/¦eNω†*’³ΠD»¦Ž<έςγ~Ζrιύ.me!477 tΎΆ£BΩΩR2#$ŽξΜ ’j#Ηϊv(©ΜhΓ2³¨νωοm W ‘Πΰ;ˆςνkΎ3³ƒΕ)q₯ž=Τύχ˜/ίΚ0‘sΠmuΨ)μ¬^(σ*D^:‰kdHδŸ3 ρƒoχX–δEͺ ϊ’%& _'ωμmξ”z»ωYΓ0kf&rΫf«?jž9~τ…8«ήI½Ϊωp§F‘)™|Zzθcθ\QRΧTtΗΏ"QœψΕύ{cU©q“)h‘Ίl~DPƒ€8ƒuα©₯Ι£ƒα`Ρr$–@/ΙΆ„6ύ‹)hXz~BΔ…k?`ޝ՝?ΝoΡα[Aρέ»ι=8—¬Ή +―θpγύ’/*£ύŸŸ.₯λBšΘΛε?Ή³έΫ'μι€ξ–Μ1}DΎΑ-9I hT_«γS h<9ΈKSw"Γεʎ­’© έ‚ί¦ξ슰ށ ζΙςG2<όΔe (§φ1©ό}-πe<Κ“ +endstream +endobj +601 0 obj +<< /Filter /FlateDecode /Length1 909 /Length2 63390 /Length3 0 /Length 63746 >> +stream +xΪtΊt&]·5§c'?±mΫΆ'ΆΩ±Υ1:ΆmΫΆνŽνάχϋΞ=ηΏηϋΗ5Ζ΅ΧZsΪkUνͺQUdDŠ*tB¦φΖ@q{;:&zFn€¬₯¬₯1ΠΙΕ¨*`£g’g‡ε˜ZšΈŒζ–v° +–7²TEΔUEδiώƒc +4ϋ― );3{ΐΛ5uuψoΎΠΙΩή@ωoq*€ΠΘΤήΞΖσΏˆςφ.–&@ε?’€Ρ{-,m,’τE{ZX ΰ_™v¦@'€*ΠΙΦ`o0ϋΗmοnigw*φf.ξFNΐδL€vΞ@gnX EY€.₯ΠθddPt5Ά±4ωo·.ΐέΕβ•ͺτ0:ΈόλxμL +β’ύŸΌΓ ‡ωg.@Σ’ώΛ)nοdό'ΪΒΕŁ›α_jf2Ρ;›ΡΫ]t©`UώQ032²Πώ YlFφ#ΗΏ‘σίΘυ/dbό72ΡώΜY9qW›χεͺ*@ΚΕθŸ ώ_ #[KΟΞk-Ν-\”ΒφφΦι³t·τš*ZΊ˜όS6#gΰΩ+­Ή @ΗΔό_F΅υΚζŸŠφΞ–,ηΈT-,M¬ν€ΞΞVΖ»€η³3±7ύW‹U\ώιŒ‘“ιώνV4²΄sQυtΟωο1ΣΛΉ8Yztι™ώ όgϋο=½H§ββdo Τ°4ύ§»Eaa{€7; +€ŽεŸ²3q08ΉX|7έΔΥΙ hηςοK៩όχΨΜςŸΒ@Ψ΅e{ž`«΄?ι.΅X9Γ“’š­Db,ξC£VαŽg–ΤšAΖϊΛΔ°χtΏα„³°‹ΘέΌήd|6ΫΣEŒe~‡ΑRΥμ›ΐr”y²*‰P²;a_-Jγ [ά-Ύo ‡žΞ‘Υ ικcΐκi1ž•:1gπ%zy- Εa{noΓnwόΩJ‚ΏSe5‹~€u^nσ]τ0jfύ*΄ eTtsͺ”A#Ÿ‚exεiLΩM¦1˜'VΛΫ΄π ,υόυ>&Άΐ!χχgB€Œ²nψΝpΰ©'c=τεUwœEPΟδ4σ-k!OυwΐF€Š›Uu5,­ˆ«‰Χ •ΟϋόΒ+:·6 Eq ŠC[*ΜΉeλπθ΄…]ό‘ZˆΣΪr<, +ίΩdΛ†ή|<ž»J`QΧPsθωώAs=¬Ÿ0R¨Λg=k­3fΈyέY‹Π9ΎΓgcΆIXhlΡ<ŸMαΠ +k9m™=Ї…¨ ΄ιh/ώ΅ΦϋX£ͺ4bσξΦ¬Œςα1; Ϋ²ieπυ¨Κ³#ʍKb"ύd s‡Β‰†}·0RΎ§š Ο“€mΠ9Yρe5{vΫΌlΈ»‘ΑΎ!’Λ;–“{/J ›ήͺSSo+Ί~ΖP +§χΑš©8šμΉΠ ΄QΤn0όPΡε—εd ϋaσr}D™θΰ+Nθ ,H@QΗωAΝΆΫ<ΎΤe·(Ν„μŸqή[ΏQ0^6o1»Ύ Ώ ξηKŸξ¬7›ϋσ|½"Όoœχκ χλˆ][D`<ΚΕ³κ°΅ +±dω—'§•Χ€ΨΎ#’š˜άˆγzq0Ά?œ…† 4-‚«»†v‡Ε•ΚσG-¬JuAΓς†‰|=™.χ†Q·‡΅sͺπωΑ ]{ G‘Έ‹uΡ>W~μqGi‘έkͺ΄G/'{Z ιou}²*±+x Ή€’sHϊƒ 2>ΡOΊƒ©Fs΄ —*Sρ5Βιq7ϊ—Œšϋš—:‘φΌ=Οΰ_‹½`-ς!†ιBI3cEyρv’ΏΡy*xΠŠ€υΈρ™H£ΣΏΗE 0DŸθ‘0_dyφ•ΣœΆΫ›ΌβJϘν€qA¨^>‹¦P4;¬Ν,ά‡…μΔF—ΊU՞ΟrόΊYΎύ}qE§*‘[ϋB:3sš”ώSΆo,AθZ˜Ί@Ά…ͺ2s’λ‘5¬H2‡Πΐ^T°πx»Ι~—ii‘x9σNΥ8δfr9ξ2ωrώ7ΗT•.ώ6%Κ$Έ™Κǟ£·e²)»VπDαZ„"ΈœψΣJS ώκ8I[mΖΓ§7*¨=iόa +>[³–ςφ’…ε7ΣΟ,ˆΛΧλ=Χ½‹kt]1Ρδ>ͺΏ>Ηο·”ϊΗασψξ06:Χ_N"»tφ( +Ψw— ς\"ΉhπΘ]η΅ϊ!·΅Ν­ςR†JK—ΟX°»Ν2.z¨/ 2lλMi’Υκ“J +΄805ο%‡»€ΛJ’ίπ}u&hκΕENΐqNuΗ„²Ζ"α3₯e#OL–2špΊqKΔjέXv-j9.'ΏFψΖ•L²‹νڏ;­^ς³°›”šπ/ΥFsDDš+ε&φ'>ƒ©ύ6M’Z»`£hЊ»'Ό)vuh μΟΨlQΐΑ£₯/XR?χ ΉVŸΉLuŠΓχ/― μ3Ύπι3ϊΠΫNwωγ €€c:X½c-$rμ §'r¦©Dβ]λŽγ²g ϊ!λφ~MϊŒ&›ŸW‰Α²Ω0νm² !•΄S.0 A ΄ΰφ+~*ΎΜ“5^2‚ϊ eΚΨ½<Υ™Wυ͏.Ύ³Πw0Ί£Έ7«ωŠ-qˆΈ-Qi‡ΓœA:ΐ> VCΤΉ½&α^AθhD½GDμ˜srιIŒJΪΏ’wjΗ!νλτ.\~ͺ―‹}Υ=€-ψ€δ"…“όοϊŽXŒCίϋVΝΙΝԊ‡­Υm-–€cΛέ‘Ξ].Τ°1ŠβšgD£”ΪlΔ’₯€"GώΗΫQΐεΒι;β]7.s αιΘμΕω%•βξ "·ŸΣ‘IΩ-ar‡Ρr:_·‡NΓq™ξ]nOΓ΅½υRΒ{°<φDlƒ”–Τ”HΨw„ψœΨ!‘q‚Μ„•AΣ#9ξςΙ  +.$UC¬η„lΪ=E–wΤΜN…φ1LΗ}²π o‚P^b L'’Š,tΑμbλ)}huΎOΒΎb‹•vΊ™Τ₯Τ-X4E,²R”aΆΈ’₯؊α#œ­‰Υ²'~ͺκ#Ϋΰ ’κΗο‡R/J *α6…'?ε~Ϊ pWŒ₯NTϊΤέΨβqšfGš(ΞΓ?`Ϊα’FΕ΄ήΕ\κκ°αbQβsΊ f±»Ω§\F2§8noυ‰Y‚PŠσ4ŸU8z¦-ΰ§hΆΔςi@Έz!F`FwΪ[³—g±ζ'Υ΄@ΈΎF’p!=ν_+­τ‡GΚΔ LM;š/ΑUθΤ+Ω7~΄w6ήΟ/u!pα‚}ΩQσFψp›όΜΧΔS°8ΙގηόΆ~@.¨Φa0Œ“ν«(‘ »πη9έΕήώ8_ΔNΫ"€kmΈ?Ο ~«κ ₯o.[gζΏΨJADγε܁œ γaΈ«€‹ςη[wΏΥ³†ύξ$κZ'.ΓΞΞΘΥ”AΛγΓΈΩΜ{φ½UΏΜδn9Β[_γσEƒ #ΣBό8ε‰tεΖ™ϊ•‹ +ΪΕ• š υŒ)Α%³Ή~(S‡yˆGμNέΓ=;{μgΤΤ\ά¨ueΕΨP«kτ`F%ύΏΜ*¬šš₯<ΡΤΓνΨnXΝvšΐ₯γΰξ«‘~¬1QœzΊΟαπ ο9mUhΟn§_PηR¬^,{ ’ͺΓE©μAρςqEίΊ¬fU"•Νf™^ŽW}L$,κL!šγ1VΌ=5U―²Σ^κgdΘό$έΡ']†0ŽpηŽϋήύ­Ύ¦&#άf(υ£ωΌ3x8ۏn0γYδΥw!7_PšSEhθ„1δ‰γŒ2žΣάΝΡΏ7ρ(γEi2ύ€q0_Μ¬S{‚›y`ξ*w«αξp0τΨ /Τ‡Μ2&aF–;Η]\ΦU#*˜C +Y:ΧV1Hj%ϊίϋlΤ>– ϋΆ+&εG‡Μ€ΈsκYΗβH$ˆΌ ΗΑƒCkΘŸΐ'ͺMiqƒqa½ –=&ξ.HσvgnIΏπ΄8·Ο,πΣπ‹ΨάμΡ2\Dίββ+’Ό¦θΊG?φ4/B F›Κδ}ϋί¨xŽΈεIβ›–έ'ΈœIJλB»Ϊƒ@΅ +œLδL8qΦΛεoΑ/e‡2€NFΪEηf.TΈ ͺΐ)Σ8Lͺ<₯q(e a¦›‡ώϋsιηΜσςu"χ§O–Ξ/ΔlΥ?-Ϊγpg₯2uštεNGƒΜσ0ξWN¨Ή›ζ΅ΏμUώ²Rf6ί©ΜD(φ>IΐV•ύN‘ Aτσ"Rρ07HA”Q¬φΙεαθ§%―ηUί ·φ #™Ζ )χ ”[ȞYρα–1Ωη|Ό]δ§ΰ“$5v-^‘R0&ŸŸi‘£FŽ‚ωJ―R%ΚFέ™d +@Τ6£ƒ&”&"7[/υώŽ%M’νΛ—J}ψ׈Ύ,GΡ€ΪυQeLJŒƒsT†pu£V rX}βΎσύ4’³„xHΓ DόvΞΰ’>AςOqNά΅‹z0€γ%oh²ˆVΚWZƒΠ1‹Mš—’έΕ¦i§Π‘cτ»΄s3‘Ώ6²ΝŸs%}ε?“5F΅‰»©ΏaM!/asXΊzΨ‡αΠά·π…‚Oœδ<&=]³WΛζ;7χλςγέv1~t/‘¬ρ:Ή0θΆΑ…›/~]mΩ5…oαGΧ’λ’ThtI“†ƒMR@φΡN&H±^”«ώό«3>‘A—.γ/nθWQ­ιX2ΒϋHKͺŽ«uζκšίΰFŜ5ΰ=oΎς>Kμ~fXΙ!žu·σξoI›fTΟTŠ΅D^W~¨Β9TSC8RΡJΧJ5¨dyS ŒΥ;Ύ«\žPͺΜמλΞ^ezœO(ύΪjι§η™ςΙιΕdR1ρ&ίlŸαΚd΅YΜOάΝHΏH±ύΙiŒσnΡ{Μ- ’€ ¬ώ WΚϊήa桐ˆ]dζ΄8ΗМ-«χ2όάi-ύφύΫ¨ΪΏ¨n·€όJ5ΊKσf ΅Χ/gƒΕωφP’θ{Λ——]κFο·aVΝΥι ©wψ΅Ο<4w¦γZvτΈ}ͺ8½Œή€»fΎ:ρ[?.T»Μ %)›6bIŸΩFΕs˜K?Zξq;=šχ«Σ‡$‰i€"8Žάπoœ~~ι|Ym7’βYW¬ƒε띹2­p[’Λ‚Νhύ†'%ΔuΠ²ι6CCCh K!m½πfc*ύbχⰞ‚Ή@»»|ώOϊ[‡W˜DXδ +FΙtιή―gy8o Ηb1Έ«²+ZσIΊ—§ύΕΡϊ*‹έΒ$ΜΧ?Ί~ύΗPKή’‹έAo΄…BϊN'μc<ΙΝ•@ΘΥlβZͺ Λ!μ7+ˆoψ"/±έ":˜ŠφεUΚ–œΐ' ΚoSŽυ!ϋ§-m‰“Πί:μ³θ›.ξώŸό +32μ?#!0’―2χ!‡ψο*ά„8Pω³‰ηnŠ/3Ρ‚o'9t!!K4~}έ΅ζ‘:Œ[ώIΠΫU"E`@N/X†λJDEίiAR-룈θ|0ψhK:ί{ ξv žο‹ωd„lšη ΉΈ₯#_zΘ°xΪ ϊΪ…κ˜»eόbͺŠΎΝς3c­ιμQηͺ+yž―8V@1ή+{={žξή579–ψ=Ε~¨„Β/ ζ^q܏±8³ ΜA\ ±› Ά‘™A½ω†Sž‘Δ΅Γ‘oϋ&M;¨ΏΨϋΧŒ7ϋJυyο~Ιz@ρνšμ£¬θmR业D3VŒ*ΜΥΞτΔ)Ί‡qΙΒrΙ1…*―Yτ}/Ό3ΈŸD[ρlBΰlWib,Œϋz@ψŽo,cΰ":—ΫpE‘7ϊƒKδzo™ΣR ©ήϊΘ#t_žB¦Ι•₯'Κίaϊ…*Q^P©ΐψBXŒEiΩΎήηΫ;„–œ­?έώ₯°Ξ’ίπœκ¦}Z™žλx“hgkΫ>7s4Q½{ιO*„“ΤΩΧΉΒ‡qm–Χ{(׌ΪΑΐFM|X—žΆ‘ητεΰξ£»A‹ΈΥ™ +ΖΎ='ŽΦΪpŒ:»Xra¬ŠΨ—΍hw¨ΪŸV6Νl,π;x½"<—[>Ύ^Ί. +'’cΕΌ ˜˜Ϋ±l)°Βcˆ–?c +Œ$ŸYt(ƒ3Mh8‘P7M`jIμΞ€vŽ“ΠYM +Gΰ0Βμ/ϋ6”ύψ5Q2OΔS +Šu¬5?k§Ί©W;’D CΏ—SΐkΚ~|τƒϋ<Η”DΛΖMΣ½σν;gμθάdVΑΥCv>n½†~,8τξŽ@u‹ΩrE0Vp¬γSxƒ\X/9‡Ζz ~,0šž”|΄,Άψ<{ΝiWΆΑΡځ.ς@WkΗ¨ƒίsύώFδo=Xο7δΑΌ63Θ‡Œ–ΐ%tW:tΎμ}z{x+ΎA·ο΅ βnpgΣοΈGkŽ}%’a[Χ&s³3·))»Gr<ίΤε΅2ϊ4Hά=¦©ι+h3Q=H™»΄–qβΐkΉ¦™g‹ŽO +›œ£΅1˜›»:/Z''ρ<96HζΏ:¨ηt fdΧ~,vάϋYδoΌ]cLΝOzΘ=@…±Ώ%ΦΜ©Ήιg\0v|œg/,»δϊ—4ϊWΜkε NQs—Zξ‹b»_’©! `φ§Ώœ#}b+ΰβδΏπ`κΚgʚ_–¦Sw &‚ ΗUX6£nπZ‰3V?Š"υΉχZκ‹O‘εtΟΕ<§ςόώ•Pxηο}„g3›ε˜.Ώηʉq uvƒ96ε2ρ+χ|7/Mf,PŸ‘މTΥ\Σ‘ϊWQη―ƒ/ftͺŒTκ6™!‘L’ˆαΩΐ|P}š2-gC;lnUF$…ΛkΧrΤ­ӞΏY'«}ρ‘ντρ·&¨,Φ=σ€ Ρ׎'ύr£ΏμΪ§Λ{k»:t΄½Ο’‰¦ΨθφL tΗIKIfΉΘv?KUμ mμΚΚ·?‡Ώu’v™°+l( +fv^xT’Τ²φύH@Xί°œ 4««ŠΠκΚ¬ΰ{³ζŠpJ©“‰³δ«i^’όο!Αω+PΤ±ͺi+;^Kμ?؈ψξ/ͺ(ά0Ί OWkQqˆ|ΘSΥγKS±»F(ΟlGη7Z!Ε1ΈΧ›10Š~ϋΘ˜7Β8P/Β»t¦Ζ[1lLν_2‡&λ Γny\ϊ² ΏΣkŽp͚wN’Ι…LΚ΄DρY’΅ΜmW-‚’θ•Uέ‘Χιi§”KOLΔ` nάϋλΞ™²GPΉZph!BΪώΔK ΜDϋΥΩ³!χz‚ΑάΌΤiŒΜΧ.ΤΡιΘE¦Ψ\D©Ep—#σ7b*ρΈ> Όόσ&Ÿ{ο  aEΡ}ΪGπ”?ŽˆJ›[“Έ‰PΣΫ‡<ξŽUUΏ²1½ ήέ+«Π>θF¦ ΉΤ@ ρμκƒy" 8ΑΖQ΄˜οώώPbΫNsΈt…ΪΏ'ΆkQžŒ[„‰dr.ιΕ%yo€Χ4>kžα‘τνškΕβΖε+Bξu|Ϋ~B 8yG4_©—OΈΖ Ξ»X›q2}žΊu.JηΤΈD—j‹f°!xν 3iωΏqCYK(jΠp]­At3J_8Ϊpτ:ΙkΞ†'(z =΄ΌHc9ΐλ]ςχPc:/uΒbΖ,β¦LfQ1`K’²9Šd%¨yOYνhufE°YΣ&€ς‹x_φ1Dϋ™ «ε!fJ·Ήδ@εjSW—hν=Υc`Ο5-Žoσs“N0e“b#…_šΆ±·±]€r·•¬Oρ57|™h’+ˆQΗή‚©œGϋδξΎή¨<½# +Gε22«A§EΛƒΊmΚ­PU¬s"ΆΚXρφΏR?#ΰΝλΗ0ŒAiΆvτϋ0=ZJ@£ΉχNJ—ύ +4Παn1JφŒΊΦ €4ΚΓ€ͺ²j …'CΠ~ZIΔ ˆέϊΰe™`R΄œƒ†‘H ϊ +IbΈΟbŽˆ iΧLηnIœγ¦υRΞ-©r΅—}Ζ1ˆm*TM§œb2J‘…KίΤm•ΗvȐ˜RO„}eχIΩΘU,,Xϊ[…LυށA•!».μ(Ξ¬(vw|¨x³.°#ΨJΓQεyn¬yy4‰Ρd”ΫGk—νΖϋΓ[]H:°&V*»κΉ ?ύ₯‹Ήί‰T(ΰp!|€ΛΣV7’¦λ_6SMΒGΪςοχ't΄οMΤ†‚&—Κ“««»zγj±"†ςœ€G²G'8V~($|edΙI―€6»D <6’‘Τ†μyΖ³†—D;ε!λψΨΤ₯Šp²Υ₯§‡™cn‡Ξ>0Ϊ %+±}h瘌%Ή›HΧeΈ–ΌZβΠ+TιΗΎg’!αψS§Κx†όn(©›ψ‚|ƒOERA,BHnΑ΄%έά¦Χtμ‘eŽήΠζ sεΑ|R«ƒu£΅3,Š… +ŠIc8§i:.t_͐IΦIΘr;Γόρ½΄λ7εΖcx“¨\2εκHŒZC·£ͺTΠ?&―- κ'D§_Œ5Υ§Ό‘Z‚ŽΗκyPψΗZTΐ/buς0P »τ ”J.ΐͺΆΞ\qΟμΡΐDœΕ€ 5«χ’-Ί²gΥφŠ»`JΌ‹k‘ΐfμη;α(axΏΌΌΰ…‡·κnΛ6Ί¬ΛC’Ο$~‰.5Εhψ‰η½€Ξή‚ž©?†,ή:­ΰu9«—Γ€νT+΅χΗr AΪ—Ϊ­x‘†8ΞDCz³@[xVΘ%vLδ±VΎͺβNq3€Φ μ υ‡ί"‚χχ½©ΟΕ"YœοV-λ›?̊ΝlνCΠKU„ρ/.¬”άΪύCŽfΗγT§@ώC’KηŠ₯ΏKLœŒsR4`;cΰΥ/ΤΕZS“\χ#|6ό4ΕRΨ!«υ―Ί™:ΐε€~`έFu³Ο―Ξc³°Kΐνvηνd7š«ΐΦΤ iιͺ‘/*%šω”Ÿησ½™'±VΫ€X`\έ@17½ͺ£WεVB†Ϋ€Θ£j$6Odž]Rυ~­κhα·ΑcλEΐ8γ ~/ζ™ΰzKšπB7ΜE%ώξ™.—[™θG$‘pSpƒΜβεŒ9ωκ)<1Ÿ 8χφ/²_q†hΊZΛΠG+L·Eέση³Ζβ3*άhŠή.o!m*Nΐε ——΄rucΓήη…>‘δzΐ!Δ$npέ΄v—WΑΐ©m5u/…Άτ.ρk* Uάlϋ\Λ‰ωga[\3Υέ¬<_4σΞΏ#X-ƒΠΠΒιψzακφ?H4k¬§'”nΔꛍτLPΡ ϋ5Έτ°Ÿ¬θ-¬‘―AΞΈΡΥŌ~Τ[ ¦£[:aO€ζcίMδΚ ¬ϊUAgΰΐMρ;³‹φŒ5u¬Ίl‰R/Ί$ AŸ‹-Ί{~Hήqf‰ΎΦ`>dqΉ.Ω%—1°xގ@?γ}½Ι{ά~mδ,qCύšr“ΝΙώˆ#NΣρ`ϋ]]xaϊctJ8Ξ’θ$,αB@ΉkοŠL. AZ XΡ“yN— Α6―Τ‹&¬ΈΘˆϋšφ€Ώ„τω(Ι•ψ4\{X”rΉ“q<ωMՎ;bΛΰ >&YuG’yUšΈ.›ZΌ"Ρ3#20\*²φx φʐ,ζΦDό]…†υGx^„©—{…rώZxdϊRω L|Γη”•·Α£΅‘ν»η–¨Z•Α™mhή€0 +V\]‰%ΖΘξ΅Bχh8‘€ψυήsM1 ΏΉ²ή΄w΄zγx†p„Ρ‹χIΙ+†΄5Ϋ{tWάF“ο«₯ψwE›τ΅n•­d†zνšhς± ρ‚Γ\C4ο₯hσηέOYEΔEΎš9Ύ„)ί(–°>ω0PΞ3ͺr•°Ιέ„βψ6;ώbl—ζTΣ@Α9ΓΈφ»|ιVΝΕ­¨kφšΞNmu4ιχv03―Hβ δΓ-œΗe γ=«ΈJ?‚Ρ‡ψ]ž·upΆ¦UΆν %Œηž΅¨γ©rΛXΕvX¨1(Œa›Ά*αA£{… έΜ—Q?ΞΞyΔ‚E²HΩ‰νΤϊFU'7’Xc}`…TΑ-Ϊs<‡ό8ήά~ηV}ΏHeŠΎnοΓ]rδ±)ΈŸ€PXhk'^1Kύ9IΏ€ΌΡβ%XΧ–Šπ{u…Κ[3<’G˜₯ΩFω‹°ƒΉΤΉMG΄ίί=K€ν#¬΄Β]αΰΪjΏLδy[WυqΑ€$γXŽ•J±ΐ~υ°]νWΣΝzDy³TwπbIΫAνdΎώζοŒ’ln„™•\)gJœF“~"#"φ΄n‚οDχϊn1£ Ί8œQαRέX SτφK”A²ιωό1Ξ€S\Β}υžnž=Ω Ϊώa #ώ—W+Cp'ώ;€s _΄κλ—η8Θ§Η†ΔAHeHœ‹π‘ΕXBϋGΏ΄I6TΟfξ7’ώώœ/\ΞƒΛK©ωεχAΊx—΄α%θ§τγ-…rεφ‘³εuk\6•›™h+ψ’ž<υ“₯YΫ:둌€ΚH^ βΒ•·s4z(-–0¨9> _|Ύ-­qΑSYͺΩ/Ca‘ƒ_PΝOξΜ#AζΰΚn–eθ=α=™R―ΤY+_νψ•9·ΛAxΨ΄”hŠΪzΦ;ήΟθ€5%G/"Χ|ζ|œ·}Ψ°OΈ.G|Ζ$8s㉨۩)Δ<=ŸbλtW*χ νᨻβ3uφί“„h€.ΙπΤ_ε ωΒ‰2šΣ·ςOTSϊμ€’μοI{_•ο P +$ώ ΐAΗ…υx‘DώΦ—ϊ9Πqsύπh«+Εe=ƒ“T“υAVT349Τ0m+ϋβΐ‹_οՁο/ Με=$ƒα} ³ΰΤP€‚ )±9οDHΦB²€€SΜK=ϋ ν•%ώNξyŽbΚ­,žb†‹3ΖiŽ»²–Τ”p«ΈΑhδAwˆΧlmλz}³ UXJrj]Ξ#›έ–~y#hNδ0`σ‘b+ξ7Χ] FάΑ y<θΪJ ΆP]”Kμ~sλLŠ˜Άδ”•e冦γ―Υ΅ΣΥύ‡2šZ½‘)e>¨ΈwSq‘ςΙ?vϋ―Χθύ½•Wΰή±S•Ζ›ΞYήτϋαa_‰JΞD‘Π¬>ζ Δ)²j„χέl+©τ΅Hό'ΥQvοhχΰκkΩ «ΥQ‘˜I±aUd?‹p‡W–²―ψ!ΟdΠπσ«yεωθA ΘvΓ°™;<βGr-%2%#¦–ω6bΓτd»Ž: Nί&H\SίΚ…Υ{h8#Χρς+Z‰‰)>>©ƒ%œ +³c©„ΒMϋPΉžˆ™”η-Δ —½E—+5¨dω­?ΕΒ¨¦δ¬ϋΪηZiSΏ0:F‘1™9Ν ±ιόΓ{aΑ¬ηκωά°},βfΥZA8ΪC.₯V­tL‰5WΐλΨ’Dφ‡(sιΡ +₯ΞHI―‡gΰy’V*q{IΓnsΩφmFςdΐόdε—€΄JiBK7Ϋ—h’Ήpœ*ασΪΓ΄±;₯g”Vqχ‰’ΝlŽ`ϋξΝΜ&ω±©1§ŠγΚ;=I©ς'™μΎΉ.]έPΉ +UG΄ψ($NpΖ‡'_Š-qd οηšŽδΧ第‡’Ί.ΦΪe7zΛΉπΜ²6nΤfe;–ϊ6τSΰΜΡΎHiΨΣyό­°ϋώˆ-€ΏxPm’WΑp!˜©vgŠϊˆO–ζό„]Έž0ϊΕΜφ2AώβΣ’ Η›;©νRΊz`N3k–¨ !δa) ⸏έ;,RP}Χ/Γ6T”Έ§ŽΠŚz'’Mέ?f¨’Ώ†Ηϊq₯‹„VgΤΪΉΑ7ΧP( œLPΔ)LΚ—gβ!δT’ΔΕ‡Ρ«@‡Κρ£ƒΫ9Dς;…sΛιπσφ#ύΉ(ΝjY/Ž―Afκ.Yπ O?>qŽT""4ΔYΪ0¦oO„έ–Νπ’νEΎl* +ƒ8ιΈΓνAδ%ΛΒ²€Ώ§s<‡ίŒ)ΐνm PS0ΎΌ΅fuΙ9αΒb@Ϋ#S~θβ§>Hλ*¬U{N‚ωΜlŒΰD +ρ38žr«Θi“Ž iG~—΅NάVψDw•ηŒ“`Ψ¦ή‚ΖpkΚΏ₯YB.¦δͺΜρΥr€2¨αΠ#­ΆίΛ—%],δΨ.œtΜκBV M6cΤ`Φ\1‡fΈΗ¬ϋ₯Ό7λ£ηεCΆ΄ CεPΫ«NՐβI^;θ1rqΕzͺsΊ|6›¦n―-i΄·ZΒ ζ™1΄…οϋ`ˆpL^ΌΤ'[1Χ%–‰ηνΎΜχ^©κ)‹p_ob —γ'΄` Nΰ–Α’`{λ1Lή•nΫρ©gΫΒIiaθm†ΫΧς=Sλν΅'1[r[ 8ω»Χ²¬‹n—m5½Σg0‚’b„ΛζςOΛ3N…„9έχ4&’~4yNάqšιΦκΝ˜Ul“§”ΏΊ’(±ώU "cγ»sδΙZΓyΑ„αJ6K¦p-ςΞC·AoΥRάMοΗlxξΪtΫ 0y’JwnΔg‰ΫΒAk–ΏΨsςΝIπr― σΫΖ” +Δ€›Γ\²L‹XœΜ'm~]51ͺdƒύΤW° MέΕμ·V +hαίq΄ +C­ύ[σ₯d+b<…–Ea%Ϋ}ά_‘œΥΑΉ‘‚# πAΣωκ΅]°’€`Uw5σλŽDClj5KΪQδTY⏣“Ή(‘\=†h₯ΚκΉ†eθFΎΫ(εˆOΟα(°ξHfω‚(œkdJlšJ,,3,Άή¬,]wτj}|sZBU“;MMgΕΨ Κ$]η™ςYŒ‹HFΒΘΰο[Φ&-?n ΡΝψΦ6…Wi‘›~gt~W~ˆŸ²1ό ’έω˜ ’Œ0Υ /₯υ_[ƒύ―y&΄†Eο"bg[&βεŽΪŠM­Ρ؟μǚώ,»R!bΰ ¬Γq’‚Ϋ"Ύτ“ΒΊ?li.Ω‘Ϋk‹QυΫ-/σL|γXΛΐ˜M† +‘±!’d|ŸLΜ‹Z‘OΛjέ%h­όš¬BϞγ0E΄ύΑΘdVΖzύ;ΓG•ΡχB\€-Σ΄Κυύ„pΖiž0 *ΓΚ―τˆ6δόΙ)°Ώ'x¨JŒ”‹TΞΰCŠ›& H +Μ¬Α²†?~x ?±ήΎ:Β‹ˆΉͺ4”WiγUο!”:σΈ*ο}ίΌ όβΗΚ&Μ”+«x14aΨ€%Ρgΰ¦ΦΊύ†Ίf Ω3I3€Un YXά΄_0σD6όQWΑ ι#CNF₯q^σωKΠTgAΆΫ–Uυγ`γΧ3a…qυ9κ‹’ΣpfL0²7Οi‘S5G‹ήώμLk9WυΈ +«v‚η(s—ˆš’ΠΨ+FΖιVWi Λ³δ’έΊ5}ιqνοX ASτZΑ(Ζ}c—ΜEωD₯ZuhΩ―α§6S·c;§V$Νty*ςoGIΪ[suΦΕWΉΖUκήp‡Vύ^βΉ( +λΣΖΦEqΣΙ}widV5IΓHpΓΧ3Ϊ£2„YΕm•΄η`Ξη‘βζŸ6$NtοW’ρ9Η§oa^‘aΟt{‘~½‡'Zy@N₯;Ν^CYxRМ©›ψΣ°ΐ½"«„Ίά­:QBμδ }Φίn3«3bΒЁ[`lΝP Yfx σΆ»JτβΖb*@£’2S³Ψ;‘3;b¬7Ώ#žq'GΌ±bW“a§ιΞ±0½πτΈ•ΓΉn<·d”ŠHˆ ρΈο`ΆiŸοΛQΔ{‹N…Kg‚A}‘ΕxPΓτ-ύί·΅γI!ΈΟžg”p 8K$1Χέ―½Φ”uί* ]ώϊΣδZ„sHHXΕδ(e——³°ρ‘7ΨDδΚεψΎ||έΨΎ ΊΎ―Έ4Ν#K/nŸ6-*φ±Ή&κίΏ¨]7W‘‰cΌκ̈%wϊ^'5έΆ:Ά.J‹ˆxύ;›†D4†ωqΚ¦Γ$<…Ο‡γϋγΚΈήxg rΗ$λ»%N ςΞ_2ϋ±JAΘm…Β₯/ΉWšŒb’5Oμ3 χ—ψκ£ξtΡ<τμΩCΘsΎš\-1ΞΉgGθ κbΜZθ¦T½4‘Tmz€Μ˜sTίΩέΣύVš~{]Ÿ‹:6YΆ2ΪΙ‹[ψPt†dάϋ>8Œ=$`Fkv–ύ 9Ή’Σ ήEqΆx Γ»/c©άm»Hžδ(Ÿ―Ή<ΪΨζϋψνΔd}₯©Ηv†[˜όœ2–‰W *νVB΄μΦQλ„Ϊ7MO«’c|.‡]KTB6sύ˜"λŠoΧώμi^η­ώ~E‘ω[–iϊΆ,…'·[©—;ΉΐiοNέΎΟCχKν·Ύώ!­:I($Œ ΈΏ +‰:cx%’ͺσ² ‡«^‘κ"f. ‘‘ΒXεi‡¨$Ιt΅~θ"ωή6«•ηwͺ +ο²ςθC%ΘωΠ,σ…τxΫ¬OΪh šΆHςν(n¨"ξ=5$‘f”@*Υ'A+{ΜοW…OMߌĺ5€²9lσo9 +ŠΟ&Aκηρ+ΟlŸΤ7Ε +Τύ₯Am^N3sŒ“†β67‹οΟ¬jΔψαΊξή‚ωμ(›[1qL3Μ&BΡQ*ΞΝ6u€Χ εΐ#­Β΄iζ€uύUŒΘ€\Ÿo('Οψσ “φzύήςͺv»V›έ‡ δΓφšώξaξoΥ„―sȈŒθνeγμ„«j³:8κY>42ξβ2θΗη@‘–VΟΡΈο―dχεΰπν9/Ό„1W^ §Σ›]#¨|›¦Rbƒ A3ζžΙΘJ±1q†(ΎΜϊΎFθO²œίρέχα ώΨfg})fΨL'ΔmΜtM=Ϋ)Ί. +–σ}H`zBδΖθd’Έaι‹Β2χœuΩjΏ\-Ÿ1€MxΩ3³ˆ œ?›Λ,me­@*«=ΰΊΕŸΌŸΕOuρ—…YΎ‹«mxf,›ωΉ“ "pΩΛ_ž‚‡ΈΟl +THCχŒΞΰΔΡ3ξ€.QV5ŠπεΑαΉ±–# Ι7gˆ"ήΝ€Ξ»ΌŒΫ‡]˜8žΠγάι‚λμؐώd±ŠΗ€Σhͺyγε½§Σ5Ο΅ž‹mžΥÏ{ψ ₯ΕΕrŒ0wS€Β=λΦEl½ζζ^vΝήΠωM‡:x―ή“(ƘhΰyΓί“:C›θΐη' +ΡΚ:CνΐC[¨Œ!ιψӞ ΟΦε&“ΏL‡Ϊ%†3/ΪΤ2ΗΞNΣH+ψ6`3½ΆώβγωΑt³pfc€‚„­jΕ²’}'ΗW»v%+plχ5>`‹L4ͺ—šŠW §œ6vΡ1Ν­  ΩIdΣ%\ΑœΜ:τ-{μ {+#%zη&WΜ1Ɏ3ǁ3N™Ÿ|šά2―΅Σ>©nBbΓΐjuI,­ΠΤ‰)Ϊ(ξ~°θΓ\šŸ\\rύΘRTAΈNOf¬jρy&Η§qΠζy=2C¦ZΓl»=»ά=0ΝWΥτŠπa<01J: εε[•Q Ψ6•‹Ψ}tn{=†…+%#M 5[β^ 8~χΣ”Γ~uΊό₯‰ξ’-a!σ[^ψ X,Σ%›*Ώ'¨Α…·=ΆΑN|qΣ S >₯ί_ζϊPΌόαϋπ>—τ‚FXρ‚­JT$ΙPω?»z(šΉp’γόαΓήθ—¦μŽl"z8T0•ͺ 8Ρ₯ΦΫcεW=©½yΎ‘JΉ‘,ΐ“Σ‹°ͺΑ€ia…ήΉΙ₯ΰo_­žλͺτ{¦itxJΪL<†΅ͺ›ODO£.ϊFΖφΒ½χ[jιΐ2¨ϊA/Ιη‡b€Μ‘1g?og •γάΎf‘ΡŒω+W]₯ΒJβχ~ΣG%8υ±ΠνΥΨVί!CκfAvWEΡ3η”σ-ΞΟuΗ­’Α‡ε#Dmr7m.¬ˆΛiΡΉ6p +J$±Λχ½ΊPΤdSΓ.A5Sγ²;Έ6—RΖΧk•―ΟMŠ·4;ύ:ρΖR¦9εθψζΙ,P°ƒ[YΠ*A[#o„Ρε3ž$Ω³άφγ( ¨ƒ$̚(HŽ]Ηά>ϊ,rˆΕζ"ΐ +«ΟΖΏšOZψΡζ$“+“yq ¬HΡi¦¬½Γώ²P1¬E$SOj‹g‚AQ'd Aξ™Whψ KYqQƒ*―v§šfύeΞδΐ$ N$W‹ŸϊΫΑe0tB­‡\θ€MͺUM΅ΜͺΟdΐ‡ˆμΔΚΣΔZϋf¦.wι‘;cC#„ϋpAΨWΙ#πWΘΈŽο9¬°]―ͺ#Ψ2eό~SΚ½dB$š|“gϊ +FJ ˆŽά ΗΠZϊαpϊ˜ϋ\Œ˜ΘΥα8{αjwn3§ΰ¦ΛHF„±ΜVR`zΉvΔ8v‚.=΄μ“θ Τλ΄οέέO›Fβ `TvΕϊ]οώ3uwΒ“Φρΰ`/ΜεΉtˆ•φΰ‘>˜ιύuŽΑ-:£λΤ_χώkAˆ^;ΟθSHΕΉŽpω4`υμ:¬ι›Α’Ψ3λGSΐΊ?Υ8=cQ+^Υ?;] Vs1*ΚTNƒΘeοσO‘_;‘μh̏rsZ1†,Ά ” χ 5’»©/ZΥ"νbΝcLΏOuπΆ7Ά-|“V—ί,‘ {§Σ ¦Sb M1§­Λ •lA)ΖƒΫ •σœ©CμSF#Έ¦-V;ŽrΒ m +ν’,Ξ³£~ίUά$±φΪΙ=τ‰ά@™Ÿ@^-™’0–Fs2Bό` ‘NXέNsF"χοܘΉυΖs ½QVsŸ·Μ€β©σZΛ₯Μ6€·QΖBY␁AΌΙξΫ FƒΖ¦ζ>ΎΪ™š@Lˆ/ψ +KΒΧΟΠχ…šςΆι…]aιόΌηT‘¬owΉJ‘všs‘˜tΓΊ7ΨVBΛŸ¨μίΠ΄γΔ|}ΡεeύQ”tš=Θ°š?—ς<ωλ$V'EΒ¨@‘˜oπ²K2Ξ©;X±ΝΫΆmΫΆ“ΫvήΨΆ±cΫΆmΫɜs7ίΜ?θ‹Z½zUU?‘[Τ₯Uλ’Γ >jn\!κ|JͺΎOυŠ˜c“΅°SΙχζr<§»oŠΏVώuiMFDL₯‘?9t1’ͺɟα ΰQ:ΰ£\›ͺbLZsΫ—“™νιιΛ}ܜ±cΐšΩΆšI! +Ώύ £ΐυθ#!hύέ–ΩH¦˜<¦ϋόJφΗ]3ŒRτ…s{›Λ»bFΚ[-:*ηΔΈ+όSΓ统OΘΆ¦`‡άόΘmζ]Δ4¦uΗOΎ|Ψδ&ΊQJ%Ρι α.ΪU·ώπΩORRA%Ξ’­w;J ΟΔF:Κ€LCΑ¬FM΄Ιχ΅ηH IjD_ΨωŸKgr‡Ρ%εq3qψΔ7ψίΙž=Χ—Β)cxθ2΅ΰv¦ "Κ λDÏζmL9b(VσzΡϋ!Νρ΅α^“πλΝ―}ٜ‹ +'Fί:ŸΧtιγΜΣi ±‹?‚9dxο "ž;˜θ'c£ƒκJ¬>kNά*v*―ŒΚΑdΔKμOυΎX Ν«‘‘ι$ΡYγψ‘ ζ6΄UΤ$ £,3bξ9cΕ§Υ W'dΕ°ή_ΥΝjh tΥΫοΏ†―XβZpjΝ‹ZΙέRœ*sv¦ͺΥ&δV z-)ΫφΙχ%ήΛi\.m-βyξ°ΌάΎ·ψΰ>²*tw-·‹hZ$ΌΐYdzη<³M> \"ͺΔ.D^Θl=šTjKΝΦI¦οΪ9όύ$C­%œY!\ΕZnΟ:hŽήˆ1P„Ιΐ”σΑ8SπΦ–)I›Κϋ–E°Γκ‹Ψۦ˚uΤΘX%ΔbnTΚ’α·ΪxR%uPΔjjΰ<κ,bΣ»·±#άUYzŸ1χωŠώ d ~ψΪ οω‰ν u#Λ„ϋ9€Φρ•-†u[dΘifΣfΚ9)ΐƒξUΊΧ€R ΒQΠIδƍ=™γ%„oΔeΡΫΰ•ΪŽΎ2ͺ„~cƒ“Ηά·²ΌM—Ιc3‘Έ=©ldaŒ$z¦hλ―²TŒH ½4˜{σ[5”Q΄Ι=5»Δ,Nήΐ^؜(gΡΠ52Rωπv2lδ‹-N<Ρ~m&kPI]ΤΙ§ώκχv·“vΰδ~qBσ²α.΅ii\mχVΞ6ΧeŠF$zoY#dQƒIΝƒψΦ¦Ϊͺκ΅Χύχ#Pψ- ΡήΘ MΩπύΜώΰ&ρ +(3ΝΕ†½U«pM^SeΌ”%’ζ3ΞR”p>B΄z|$.gYέDθclΠlmΏ>0α6ΪΨΡ7UηG7 ˜cž0­Ζ¬·ά¬€pH½ £΅>ΤαKn7‰5’όs¬ nλΙbJNΓΚ»ΫΘΡ΅{ηVΤ3―𾝠Φ~ƒ•zΏ?ΊΏŽ Ρ“‡6F†b0RV«ΦΨΪ.Γ ³YTΐW0χψSΊξI‘}č}} =uμ‹c3ΥΓCθ Ιφ‘ϊό΅%!ЍS-Ό@b‡‚S0Έx“š.Ά;ό·@AF ―Z8#›x²Ώξ3ρ”yp͈8²ύ…‡ΒΞ¦ω½&Τηρ•AΊγ]ρΨ„FΑ[ZRΘ²$Ζ^ΞHϊό{_9ν!8‡E»[€δ#r.vλΰΟcΈœ1FοDWΗ}΄Δb]€kΊ€=ωZ±η‚˜2֜=ΎGs9{ς(yk­ΔνQ΄af…Η 5ΛIzΎfΪΓ·VO#q1π]BlΗΆ±vΎ©Λ—%”£¬7bhI#7•‹Kψβξ βγ¨L3oΠ~Ύ ”΅i%€—ςzεœ~l0«Ω;ERΤέίν³gm:¬Žv_›ιnΙΥ5Ήμ쀁iςϊίF„Ž ε+Jδ2ͺΘSNVnέ„PHρ =υόσι7ώuΥ Ύ¬v”ΟLΟ₯«ΊX―/ϋF7ΰΜψt$Cόΐž QθΊ…yΝaK™έιbμTKuT-μ4oTcΌ'πΙTd)+™+θBW4~!₯@Ω·όœ\ρEΠ―N~—`ΧεcŽΤ’wm2Ύo€8΄ έ4;ΒvΤ θ .z‘bΆύϋ>’ŠΛ₯Α„=ψpΣ+ΩcS²—³―3 [tl¨εΑ +HFχ}ΟΨ&>'N8©vyXδŒ›::½-xΝΤiξψ—Ώ Jώ ίΉπ“ζ3”šρ]έ²H9Ά/€pΕ!θ+*­}φΗb·rgiΕ5xφπ¨qή²"Ξ  +Rbγ›wΚΜ^Ϊx‘ o(Ϊτ¬ϋιΛE¬ΝM5yг3ΡlšY± +M πˆgY‘ Sώ­]a›Ή& «ƒβίw qFΘί—yx 4ΓΑ—WQ2WWnλZ6Ι·ΒE5 JHΑ +ωΤ‘ϊ˜Šu@^Ό)–T–’ΡΙ;BP˜’K<Xβ5Ne&"J9B=bS$g!™ΟΩLα ό2™―BKλ₯gςvγ‚f3N{·3ΚΙx7ρθ”“άuλp,"ΫβZ1- ΤΠ– \”ma·[oή,ύ„νyαVμ§\žΟ}²σ}ˆΖŸw΅EaΑ~ˆMΚbτ£\@†μ#€IΠΓΉnς=š[·G½.α_oRΪ^]λΫƒςΥ_eίΏ Q€šΫyRpVσΕP]ƒh¬{ {ŒΡ'Re1EM!ώ6$£)(GυΌω;·¨θ Χ•λΙŒΨtPgε°‚ͺtΌσ&Ά˜_Lg£Ύ¬ΐKΤK£ζše½Νέ”΄σWΣtͺάοWa,)jPsΒ8RQοFΤγ?δβτΌ³ζΓΠkg뻨­‰Ή*:»%(υ%θL%ΉR„α‘ΜΘ|2 .yŸπήͺ(ύi{οΨΑ,Υ¬ψ€?™cΓT[ΝΗ +Šjšεη֐υ†=«/τڝσλElμ£LΖG›‡ν»ΤΝΘ`n˜Κ…σg~³‰½σRœο6₯Λμ6œSΓΊΡω΄{ '1ο4A"Qν›―’…=ά(κΒJσœSdΧΘhmΠψU9ŒΙGzΐΒΪΙνΛD™ΙΖ—Ι…Ζώς©qΆγ#6XjN”‰ ·AJ =^{²pE³ΩΝu‡¨=k-¦Z°-Εs$Π œ4¦ˆϊ1c°·jƒ†…„Ή‚ˆqy­BQ M­.βϋvRŠσšOΞnU[δΒGw-ήBΠVJΥt)Fω°ΞUά"nM€ΙT%rθCˆπ·τœΤΠΤύ/Α,0B¦qa[£―™μΦύM¨»Y θLͺθkΈMΆΙ‘‘"Α|ϊw— ω"ΦΒas5Ρ]^νΏ;Έ†Š‰/ˆΈœ^ΑlIΨ… ιeΐΝk; U.<κΝ€»#Κ― yVUΊΑ ›6“vϋwuΛ€ ―€Ηz“Ρpdh-Σf%+£NSέ$υ΅ΤDψr¨3r9b.ΤΫά(°yΠΪΧΐ·Ψ‘fl€ gb͏—Βͺ~½X5= ΄kφ"ΩPεaǚΡΜv-%ΦZΥΞόΒac<τ^0NςTy.‰#X~˜έW\iH«=έ‰xqT)μy›V–σKlξδf -sMβFgΦ›ΒθkRže-jαΡ=£P ¬-§μa΅:ΛΘ.„₯‘>H^8Ά\ŸξŒή¦e;TA]έfM< “νΧycAt}hW}ΈMԊίΣΠ!Ε™[ΙdSzž˜3Π·αKuLΙ? ΧνaZsg):1…΄τΥϊr|Ν˜yΗq%ιΘΪŽriΨσ^xHΙΦZŒ‡·Nόφsγj)³μ3aΧƒΰi,›Ά_±t»ΤΦ"ΑB +‘oΖ σoMƒΨ'λςγg•eEΏϋfG\ζ}OΚωŸXjEΏZ[L,†αΟΘb'<šo›O+ϊ¨‡Ukνooμώ_ΪΫ;ZζΆvάF@κ6ήΜ‘°‹Šώ±Oθ\ξ²ΆζΈŒΝ•RΐρIš:£»iŒ»fZŒΑ‚)Ύj_Τ’Ϊ΄4Ι-ΆΞνX~·ΫΕpujχνλΠ·pwdCΖoY ›‘Ϊ²›εΓ±KΌϋlΐ-’ρχžΐΧ"y2ϊΝ₯²z4Ι-·FIΆ΄Ώ6sύϊž²Μ˜]Ώ tΛQΥj‰Άο»NιtF₯8-Ϋ€06]Ε¨Yc΅δ:(ΎΩΐκΖ·;θuŒ°ZŸι~›άω,ϊp‘ŠϊX αIΜ,­7ΆΝqz±δ8}>½ωεŸΠΞ68ιaamvg?ϋf_pΔV3ΈlŸSH Ž(χ‰οŠ&YΏδ2θwκ>cTx#ܘs”—vGwkΆ;–€ζVUλ©Θ,ρ.aJ’uΜϋ—ό΅΅„ˆG +L‹mΥhΖO1i›EΉ1N/Šb7y+ͺ©Μ‰%ΧυPρrŠ@*]K=ρ©Q»­jΟΜ«–ΊΌ6δ&4Kέ΄ˆ9―ίGδό4½kΰ«ΗΕβ4яΈΠα60Mg€σsͺ6((#I{Ϊ¨+šξT9Qƒb.½ϋΞ+TΨθαΓμΕΛ₯>c`ΚO{7Δ@‡μxΙ²\&§y8$ii#ΝπUˆ!»·*λ?q₯G˜}–ZΜWήΡΑΡY$P Δ"“‘§l¨nž`Ίκ!ΨΨ²KRg9"2 χ JC]jΘυΠΐσΜ$πΆ*9κ’ς 3¨Ώ΅^͊¦—ŸeQήΰ»²μΙρΫΗΞ‡:fƒΌm€εkΐlŽ­φκy6*‹ΚgλZw€bδ€@ +­™χ P9HLΆ_λΩ#ψK/‘PΨβΜ₯PΣ2ωjeb©MΚό!«.pJ λ.#±¨r”―Θ‘Κ"’€ΥϊEΑ₯gθό 6dd¨%ϊ,+΅χ Μ{DO2žW³„«kΨΑzΚHγ |δ6A£vy υ +Q!lΈα?.pΰ柀Z ²D9’°D~ζ½πΉ›Ÿ« z‰ŠDlωΰ8εœ'7 Ν0ΟyxfTΒv©ϋ˜ΜΕ*MΤΫμx0P°πw‘bqn‹b_έsω`φNσjΎz’₯γ,še _·ΘCY~ fΥ– Ωt#MΈ€eυ +;XUdλ?΅«9€ΎΡ ξ#€ιω Ž- _‰1έQiDώdΧλΥ/›@ή|ΈΝƒγΘ  d·Mυςe?λJ}ΠPŸΨŒΏzυ°5'iΥΔ{@Ht„GTHq،Αή#χ…ίj―–sD”³‚χΙ½; }αˆηΟψσ^ν8¨¨ Ιy+rWo XΑ0htZ“(‘ .Σ ΗJL£! ­tΊu΄rΡΗ9’³FU–β΄ρL™‘N0• έ“Lݘ€½ίžoB—°ζ>ξFπ7]ozό}Ϊ›)NuΝ{’<Ÿ`™Oš½Δ8HL{b}„cη*Ο ³β‹ΩΎ5'Λgw*žΆ<’IZŠp@{―mξb‘ Ψί]mS ΞKNZΰΨυhͺ”ΘŠ6uwmϊ½gC4—8y…ŽzϞ_M…•ς8ήaΙ±GŸ„i ω–gσ‰TΥΜφόΈ €ΡΤ#Πθœ*αCΪ¬αdΞfκΒW$+ϊΤ·Χ⡟€Ρ$ "‘e“:ζOuΉ€Nd…μSέ’‘ΞXόΜΛ§ΰΛu  ŒΪz(Pɘ‡HΧyJšX6sή??3αχ8ώrώ΄υe]‘2zœΈχ½ Ε7uΏΣωzp4²πΝSιLΆΙ>2κα$^ΰ2.4ΓμsTΌΐbΤ³‹ύ2θ™4Σ₯Sφj2Σ·6ΛUApšΘ{ΓThΏηWΝ9ΏΩΓ|± xΐθL ”Μqq½ΰ²ι\§@Έ!t FB†εXL XΐA 'wmκgβy@€oS™ζΫΞϊψNFžŽTšσo8WKΝZ‡rψΥ”ί2RŒ’qTY»aœJƒ—~iю¨ͺΈtzκŸΧθFKη{”ι;dΫΎάڝIkυ|ΖΌ_q ˆ*ώχ~β_Κβ]¬βΫρp~’sN9 ό<ΚΊΤ(1:žΑό XBΏΌUH½9ι9€oςRΞΨB“–„Fl7_²mΡμk{ˆ‹Ιυ… [2Α’ˆlΓ\{>·θšςΞ]-_ωN­X}~ΐrς· ™Zφ©mλ‚₯yΎ―ς„=38{Zpί΄SβΜ6.;ΛωΑc’Vγfύ05± +Ε*&υι―w$ž±¦ΟΝtޚΘΪtφ[νρ>¬|-l˜ηw Plˆž@β‹:Δ­?>μˆ?!Ζ.Uγ,π„ΐέgςv˜5&°fN’\/φ K_Ν¬Q*™V_£F,υPζ?πlY|άυσΦD¦Ίςm;Ώαcφ›Χ*·+Ύθ‚P7^ ‰ž +δ«yιAjFσΰIΆgB 1I9ζΉut~Š·†Ω`ϋB“ά4xkOͺα[σ4••+coω6ašΉ]Ϊyͺ$ˆκMQ¨Bl‘Β½”“λ„™ΰcnS…ϊ#΅RA7ά½―Δ9]«»Θ˜ό ΫΈ±›œ»q1―GRW ±^φ1ν–+۟cΤDΌσ¦XVYm)Η¬)R yΦώΤφΓ@?Ϊ}‚a{;μήΰ 2Q°yi#ΕSA!ερlN i°ΌΠ εCϊ΅!ςQ-,+dψ·|Ά·γώ#ocΧW‘t{”»Ζ;Ÿ|CY|&†Ξ²"ζ…Lυ%uRHγ:οuηjήΛΗJ{?gnϋΖwaNΔ,τ»™χ Ύ4½‚Ψδ«j8ΣМ»BhΤ†’Γ§Πn»H+Or…ήK’U*„ΜΒΏqΝGˆKʚ•αΰ©μ©α€ i€ωq<Χ,%pj˜±(x¨5K±„°e3ς‹Ι„"Α£"l΄ ερχ#°ΝΈœδό—‚βυΞ@ΣχŒζ Ν?tΓ ίΑ½εήx [†'ώΑPιζ‹1Δ+nc―ο)Zά?U:Ν0’ί™ΈυβΞ"Ζ„ρε6G4βΓ+ŒιδO$:φ΅GͺM–]υϋ’L ο{hΌRl)ΕΛ‹8‘5*TŠ3M²cŠF¨ ‘wα‘`σ€†fS<½SrδmW²ρ;Cι’ΫQΜPl5<'›uβΛΗk+»žv‘O…νFBT:ͺZ8’( 4ζI7w[$€:$5َ*0μy Π‚ι,ΣΔ²ύΆ΄s“_.-ˆ!Ξ+η–Έ\Vεέ¨,IβŸ$™―ψΜ)ZzoZ‰;x*y―ό‘γκLם1ς-6ΨcρζόσβΡ(΅i™χρKT±ΖGZ2.έ΅ΑtΧ§΄{u\”α5CJΕ.ί[5r‰‚ΡάcΪ©uv 'ΛPόίψQΪςbγšYϊ{iβ¦M₯ ωaaεš&¬‰ZΚΊΨέ”-ΦΧψίκ½ώξ&ΐΔΞͺ2’–Ι“AΒIdŽ:­Y>K<ΊΪR“ͺΨϋΘEt?ž]ΎT)†Χύ&•ΖcNΕ•eΊ΅ŽΑ.›…{,/•:;P\Ž6g9œψ;ΕŸPΉ΅Θ€Τς!T„F―Ζ•ϊRj©ΆDπΙ§‚™ϋM@¨Έ›ϋΫΞ_βσtέΘϋιΎίYγΔߊA+αƒcšνγKzŸ΅F­ό ŠmΙ譝ΏV2mGϊŒρg”¨ΙΡδητ(z–o?uBš‘>@‡kοpΒDa,…{eΰIδΑ©Αt³ώ9π£<΄Θ?Œ7>𚑹’P»Hg8δτˆ²Yr€$b΄£m5@Α‘τA±:y–'b)"Λ­ύsΆRYn½Ψd}MYožΨ'eα{‚†Πγ€δ/˜fC…IόŸγ +„nπ.χ1d:Δ*w.†XΝsΓ₯‰<Ο/QΛ{Ι1ψa NφyώŸ­Ža¨KκΕx uVNΦtί/hόŠ<3Κ A}7,VE­”vι};Ηβ΅ΜΙaιrΰA6ίδh²w$ͺ,a²Ρ‘εΌ‘©ΕΓΘGf΅{ύΠμΔ©ΠελΫΙΐx΅σCnXσγΓD πζV‘ΜIΎ]ニ‹Ψβteμθ4?WŠ]χbάΰsχο‡~ΔQ9YώδH’(ϋA0ŽζŠή[όΝ–§Αζ&|ӈJt%BwΒ¬½j±Rœ©8#.¨2b +ΐυ/™_`E‘Ή·ˆηeή„€ϊFνώΓx6³oάW7ΗX»˜’σEjΞό&4–νDIθXœ…ϋo(φΩ<Ύμ/_•ΚΌφ.%.gΙ,Ν’αkςžαΤHϊZ§‹6½βh/ΨΞT¨uΙΌ.ΥΒύE{|[Τΰώ;x³Εθ5ΊΗΡϊ6»mΉ‰ƒ­Λϋp›)«\ύζ°€ QϋtΣ/nΈu“‹X-ΕΨpFgi2‹Ρ2£ηͺ}Β!Τz s+6ΐ tΒa~γxQw6βlψƒ‰zGš Β5Н=^J*ψmI³j©S5ύ“%”Kœ,υςζκw§λ1—Ψύˆ 3¬ƒΦΆWtέ*ρ-€Π?'λn—τΨ2ΙΘδ[ nIvι>ζžαH[Ξ¬Ÿ―ύέΊK°yήΘΆ΅j¨hvΈJm%\«NΚdΏ^-ϊͺ¦;Η{1 +—Υq~Ή+ >Ηά«Xr»Ϋβ&σΖ‘κττωLf*yΉ8‘I?Ψş}ζ{FqγΡZoI`3;όœnK^ƒΐ‘Ν5š'σ±a€Ψ$ν=ξύšq•:Ύξ©oσ7@2Ν2ξή^p•Ηΐ"MDΟkŠF}Bω θ₯εYθΛ΄^{t=J˜”sK„`«cιΡ¬ œ₯9™™»ρκ½kπζ»-΄)[VtΖ*κUΰΓεξΰί©˜ ΅WaΑͺ8JvMuŒυλMr`„_Ξ¨l*a4₯Μ”άσNγ1μIλΑٚΕkEΔ +G° ΨΡoΧ +ꞁ‚ Άυ`χε ^φμPδTK*ΗCΉ 3ƒiNΧΪuΕu?ξΜ~σCs ]ϊ}ΜΝΞφvΥ|‰)#`ΩΔ;€Γbΐi€adͺΘξ'ο:[`'΅’Έq˜oBTΝΌΈžQIZ€*ju+ ‡{F‡@w—^(aγZP«lϋSl˜ΐaΐ#qΕ€φyMٜ*΅C\‹ύjj£Λ£ΐΜO‘ΚΤ’4ͺώΞ JΊG€v€‡Œΐ£yΫΓΈΌfΔΣC$zϋαT†ΠΈZwώz8ώ²Φ•β’cŸι`£pε#’ίie+£[˜™+lGĈ²ύJαw8?¨KP»i– {t«νž}·ŽX6λ/κΔ΅Gϊz quΜM₯žtμ$б ³[šο\π†@X{4ΛJo Μχ-Ό`X14N”D£₯(Zό‹μΫ€ώp₯B±Gl¬ϊYžώ²ζŽšη1B £ωΦY=–—F΅όΕώCE8Φsυ£‡„°χ+ψφΩύw·`<4ΑW†Y=w€??†h… ’Ρ§n_4 +ΨN’ι7{·ΘέΨώ4m_ηΤ4©6=™¬TΘCͺ‰Aώi+ύχ +τςL +1VΥ4RgΕϋΧΞP˜~.”s“ΟYS†lε{[Ž=χν˜ +\Ί9 wΎTY½ρ΄„£$η>Ϋ8£jnΥή‰ΑΎΙP*΄θ·΅΄ςN³.€= ϊnΊ1'Σ@`œnfΙgdάΝ~@)ŠW³€™ K +‘gζ))θΝrμΥΐCXc(=Oͺ,=ΰ €ύJ›‚ΨςtτrΞ "Ο­tή–ΘZ‘.§Ϊ^:WlΒJ„wϋ[ƒ ΐ- +Η†γoσžej‰€ο–Π_#Š'0zuMF@•[ρqW wg QiξεΚΆ»Ψx’BΨͺΩqX$hh’ΣσΒ ŽΡϊΧψ7p’3 +ύw+%JxΦζ‘εg ’n’WΉ₯ϋo“7?[R¨qŸr(² β±™3yQ…΄Ϋ.rΏσεŸžωq&£ωoβ,‘£Zfπ•ηLΰg%œcΈ0ΕΐX½£D_ ;ύtΤ!jΝ‘υG˜iŠ;υϊ,Žœ“N2˜Ό‹š ™6Θ4eGnx1”7ς­ΗΕ›μQ7ΣK Q*ω$X8dj#Ψ[/β•SD½—R­Η€rΏ[Ώω¨a3‹ξσ +9έmUΑpV͏σdϊͺώΪ.¨ΎΕΏGX9δ§ΫΨUΆΔ_ινΠΆ‘Έ«rΌmΦe`zKΜsό†ΨK|n>‚˜ΌΆŒ¨‹qŸ8ΞgŽBRJbίe}Gn’ξ/Μ±Γ6·Y›ΫCs|ΛΚ9FΩCͺφ7RTΘ\«BΆ`™F»!_k΅A$ν ƒ Q]ˆjœ't$ς€οψΏ–Δ)Ή)υ΄ΰ§1ψΒεX‘ωΖxΧ² 6¨xDΣ‹ϊ1^:KpWΆ•Y—su>ύλδΛI™¬‘δΕWΚΔ¬3ΌΜTΎ›“ΫΏ>Ναo+Π‘l|$ŽV7Κݍ0j€uυ,1‚?wŸ°ˆfΙχ€EψΘpΞCa5Ρq@Ρƒ[αΪΈdΚήΊ5}μΑ~YΟΐ`ςν+ηnš’.šVrOL|֍Π%HnfΥβLŒ! +ρΊ{±…U$|6¬^€•ΔjSμήΕKސ3€<΅DŸ>εχœ—t“*AԐT•Υ4!η:cPh„Σ(‰Ψδ—κ΄"&{Ζ΅ƒΒfz§WŸΦjοIjμΦΞΆεώfdNIΨu;θKΝP„ΠκDŒ;„,ΜWΊT³<›¬Υ6ώ<2΅ ŠhΞ@Γέc:/RPρiΕUΗαZ'ΝαΉQά4VΏδ]ΟσIDXiΫlňJ¨ΈρΔ#£·΄gθ{ ’ΖΤ ψvϊ£³#~ΊΘηφ–Ϋ‚oK”ψΗμΕίύc، +οLRν*Mς+¨ŸMη^Iυν³K3υΝ렟ΑŽ7…£'zση;κšYˆ€ΌΎI5͐·ω6VχŽ+νz†jβξy–Dp$”|Ϊ¨ξzoH²[γWΤ‘ρwΞτƒ yο%šΨ—,.ΪΉUΥόζU(αŸ:ΐMθμ&…­j[m<ΣύΥž¦ r!ΐ¦gΛ¨‰~>ΗΑW°AϋFΐ{ aέΤ–Όu`’2DΩ +°υσ@σVΐ)DΞ#,ιˆmk‘ϋΛͺ&ΎΕφΘ^ισv#±n"‘Δφ/;I•|/Π6u­™d½υȝΉ¬#ςN6£Δ„…ϋWCχ}…ΆιΖ”Ζ΅.§pεΚ'ηΐLL +zVUŒ-`=θ_³€?R’±OώτjrCν’rΟε(²χρ_P^K•„“¦%ΪJςΝ¦(κψμοʘ!ξVΟΑJ%J₯σfTs«uό εψχ#Άη1w/β†RgΘ-²γΫΩ[zΡσ™βΌα°*>"½^k}ΕασΑ 4ψψΊ¬ΜΦ b―j¦Eν-O]sΑΝΘ=='¦Ϊ€―ε RΪ(u,΄`±~Λ„~)VΌ·p,΅ ِ¦ζfV“Uƒ5η‚™UY„\΅φ™·9Ρ©WοC ˜ItšΑΦm€ +ή0ΩrV΅@Ά.{ +©*;θ€ζλΠΪκΑκ9·AΆOFι²KίGχΘΨ‰2ΕΐxTE^θ$γς £:ΰ¨Α{’;ΣlΤlQSθlΠ%Ή§1uΰBeŒ²‘WΈ΄MQ–πv§ιH(ͺφ!ϊ„e{ν4 Ί]ΊŸ%HplO‡d{Λͺ‹”όΎθε^ G½e€:<ύoίόflTΡζLτΥωΞς΄S*ΤLT·δ(­ςίVŸW,ή?ΉpFAj Ρ9ΔM•Pυf‡’ +#:&œΰF`ϊύ°šiAΒ{―ΚyΐΎλήΔ ‚w¦Κ~!b"NsΧΠΩέ·d9J ΔX6<Όρ7ϊ-έ^l/MC&Ηδm›ΨΟ~ΟlαHWΘ™g"ΥWΤ΄›,£ΊχcγΒ{jωνπ₯λ}ΦrgMeeΚΝqΥZΎd:―n½ΊΔ›9ΑΠ#ŸαΖJξ +‰τ`žΥnθ¬i1ΚΒmΓDLbΑψΘψ 9φd ­—ͺAτ)EΡβΙd};ΓXKώG­μŒ#Ή’¦LσQΈΒβπͺ₯F$M³ :/6N§6S€P)ΐ)Δv+Ξ2 ‡α3~΅–ˆ +…{ν?ηΕ΅•²¨"NΧEη’ d¦+;Ž˜rHΏψ†πΚπ~·ν9~–ΨYŒ4YvμΆΌ=­l‡]"+Ϋ]g’’ώsh˜œσ• q;ϋ·ΫΪ‡Έ>O²+Zςξ=*™!i‘{ά> Q:"‚P…*/`*dκyLQsB¨{Z/νI!τβιή9q–yͺΒL-LΫ:ΚΖ ϊ\Z°[Μ%w–ΉμOΉΗOΖψ$β»ΐ΅λK(šϊPKYF €^σ&λ•ρwβΌΙΧCπσ`ž0Πξ~)rτY…£fQΑςV„Ξ§o»h0(jη΄dgΌŸmζξώψΗgΤΡΏ“5χh$MQIΪ•.YΉ;Δ(ξ· +}μνsž)wR^:4φυe¨θ·νΎ"vλΙ­Œb©°Ν.‡ϊ…kgp σ ŽVλgEšƒΤθuυΐ€,KSλOθάo$cAG&xe‚BX§σΒΑκ{OΗιζx±#θόΔΦnΰQνmpyΗ)ΜωE`iŽž9#Ϋ@;Έ,`α¦ej4AΝ¨΄rΤ8\^+%ω CNcμw‰ Ϊϋ·g„yΈŒόΓaX•GvλΑ¬ΎllQdsd"\pη γ μvΖ(Τ-œž4γ\ZΩΕ–«α/7―Ω–—\ +Ώ¦ΡֈlΌ‚Δ:ν„₯ܝτžΠχƒτ&Βώ’ΒUO΄'$ΩCpl΄ςξζ·»{_C%άξ$ίΗωθ›^δ…S„[uΪΣσŽιΘΎβw±οYžr8RoωRΊά‚₯V°EοhΣ?ο"ž¬f‘[‰vSδYvΠ%πάαδšΞLC―7¦Ά¨₯P6ΎΖΆ4E`γ7—_ήχ†Œ­χό3UΘƒΆΉ7VζΊ€9Λ„Ϋ'ΎIμD³υS=…rΰ‹T·’££ +κ‰I7&ƒͺ―˜ε2ΈZϋmLRΌςΟ*[Uί&¨?iOίμί΄‘A7Y2³ΊtuΑZi‰‚Y±”$ލη(χ–ϋGΝ΅έ)Ž?ށΤωΤΈπίS^‘ςΪΡf/tΊΑAn¦%O΄(ΤΛ I±_Λ-BΚΠ_9gαΚ§BΆ3C―yθ?`Μϊ›™ώagŒmδϊLRX~oψ1ξͺΩΆžͺ£έχEοΰυoλƒ:π¨ŽzKi)α:4ž–fgΔ £«ίCΎŠ?VΝ©s#ΎB7Ζ$D’eΨ<ΐ„`•Y(Ÿ—%6UΐavΫΡ!JwŸ΅!K›g5j “ ηUIώd£qVΏυZ?[Υ)qΖ°~Ξ€% $Ύz’'5“ΡGΦ >lN§Ωw]‰\Ξ₯N4σŠόιtA~Wo˜•Š@ŽθŒΫa:$jSΠψΙΉ –Μ?UŸ‹FZΔΐ澎­mΪσ°Aο‰ΰUΚ2*ΧωT)άΈ2ZΝιY;Φ©5s”žU~^§…πͺ;ͺΤώ¨-ΫΦΪ9”cφ·RsΓΙΉ³ΥŒ=υ5RτKeΟrΈœ­ ΘZΥύΌ₯ߚε~¬ιW‚ή~Ÿ)δ¦p'χ’ΙΨ<[ΡΆVζl·Y<c¦˜υqκγxzΩr™:_38;,P[Ÿ°ή4qkrΛ»Œ#SλρΈΕzΩͺK)† 1}#α[Š/Ξμ˜sR”<θT«—°…1s“+Ίa)a"<ξΊ·½θŠ|Ζ©x}­~ρλΐνΞ|Y~C3W3qηxEi5³wQΈ>v(όCλΗΛbkt`Ω%Ϋ#ΗΝΥφΩ·‹©˜Λκr[ΕΔξK‘NŽΊή=«ƒo›8tZ£©Α‚έ_‡:FΩ―άG +]‰λ„‚•IςΙ#ζvŒ9Ε­‰έ[.ψ/dXΒ +Ο’nC%–₯‹\„Μ\Α·¬-σ=ΟsŽځš°Vυ+š°ηήΒ,+‡YΈΚθ±qTψیίHε—–₯έ&Xτ~j'€ΨΊqΚζΡΦ†g¬Εό=πζ(4=Œ¨XΛZΛ‡3^ +B_xδ]šƒ=ΑʟξL{γ@s'KΙΕ]θAΡΆ6@ΒσΪΙxω· };.‚Ϋ5B3§α|¬z3/n)ψ‹’rѐl/2Υ3‘ίΦ'λ¬Τš°jRΧ—ρ|£qΜ8έλμͺzζ‘3κΐb8¨fΗ14\eω/bκ`Yƒ §ŒLρ¨Β(—-\€Šϊˆ΅dΩΕTΔu΅T΅LT‹˜ψeΎ%ιu‘·šΈ²YλmτMš1uΦάp|€Ίx½‘0*ΐ†©μU +ψn[ ΈλWͺ f6»ͺPρ«9ιΛδ€ŒΓPβ€ΖUOπVε \7#OdP-e՚έΦ’Τ¬‡#υPΜ>ώbOLpέEvΝ9ΐΩ'β…3»YjεΛχοΦp©ό$KζΦ•¨Eγ4ž`άgΝ9κώ Ό{.-.ΪΘŽδν Ππ$VmϊΠθž=ˆBώΚ +;RŽHΐ5g°εŽšb‹ΤͺϋfHq€ ›Δςμ°™ρc ˜C@Χ$S‘tΌWΓxοv"fΝpW†H­%SυΒRΕ!01ΣKΉOqΣ΅g©¦ΑxJΚώtΙ±R1!ŽΡ˜ _WΚ׍Ž ₯Λlπ‰g4νkΠ8?οΊ‘ΚδύϊK»lΝΝjώΞ‚‘Ύ„Ύ*& <†τ‹­PKα»Λwx=Ιܝ˜₯3ABr’θΥξeλ.c€ΓY°;Μ”›e*½χό‘œa,8<χTΑœO§F_ώNωN¨κjT4έΆΡΡ#μ>φΩνΐΪl­ZϋΈΙφ;\ΰˆQαxjΎΈ\£γρމ`1­ρΊ™ΊΪ15ςŽyWŽθρOFΖΊ?„ƒ9Χd?ΥωŒP»‘›;1VŸ>ξξ2 !ςδτDRI:K!QŒΚ‰x p0Η${³zμg—o @ ’ΰπ'mοι]tμυ~δ;Θ“ωŸ‘sw˜’σΪM…tδτΗ7’p£δς»F€Έ–Χl[]—ψ&ϊΚ|@θα₯]Ρ™Sϊ' IΔ±Y +»°b}†%-Τ½}μ72 έψ툌?υ%§mͺ&1Μ0 ½aώό‘·Γ•"Qυ—F«Nζ aΉΐ‘θη=fΓ“ ’;ΧΗi˜L4‚ΕLΖΘ|«"c/–Έƒ―:<£χ™+ΑYαμ‚#γ·²-Ck·Ž%&J<“–΄9q§ΪS‡#S‘UFm:λ1ώο|μ9ύ2ΘzG€Rr58ήFqy θœΑ)ΨΖ΅/‘6H=8Εδΰλ».ΰΙι‘ί1ŽΝτς6up³έ},[Ξƒ\“—Cw[ιCIx―RXO―%œv-κ$8₯7s ͺ/PJe©κu'CW¨=H"}lί;mς:Πa‹»<ΨυXx» +­-L63€³π "Bw—Λ|ͺx«ˆζψρΰT<κU›Ηΐ^Ψκ·ΠFΨ―Ιχš³θnyW†ϊ:ќψ! [2©fzσc|π &š Q­Έ¬νρpSΈΐžΎ+ΜΜYΕV½Θ潜WμξDoΑRZRNΧΞ»ϋ;S„F… ΰΚ¬bŒ7Έ,έwˆΛαvοΤ;%9ΧΦϋcΩ汄]q₯žΩΰε­FΞΕ +Ύ7”`-ZӐΒ,_κ>ΖKώ²]s-‹N Ω±9Ok A€Ε ΎΖ;“Wy•²K|¨Q<νCΰ΄ Γeθ€ vΗq_Π:ψr]Ί|Dͺφ±3C §Υ)¨ΐΨ}*°_εΔ'SmFVΨΧ| ‘fΝίύ˜ fj¬/5 {YωΙξ”E{…_T‘‰€±$\SbsMrΏΡGδfέ,κ\“φ‘ CΣ!“μGμχӝءQ?Z1kaxώcϋ―¬v±/ά²/δλ—λχ ?jeXώ΅ΛςZ”―a†ί Ι&ς£[ΕιοYΆΝ˜δ΄pόΈ„lŒΐ§ζMΖ";K87ΏΘυHDœvθ³$mdkst΄„Ζσnq.<н}žμ΄α>DαεΙά:QώβΆφΣΔβ―ψΩ`ΔΦx’Ϋέ9y!§Ί UΨΓ“Y£y€Y±ΣΏS;!υ#Kk»Φγp#F”ΪΆ± +Ηo­©ΓžπŒήy+“αA³ˆά0˜k M‘;ο2θϊΑΩN±ήΆΛ"idΐέπŒš"η]ΐŠg1ˆθDL‹ΖΚxB―ήyεκ)-*OΨΥ*rΰ―:ΈSm9‰δȈΰΪ(‚oτuͺKΨ–'Oz!C€έŸρu&}₯5‘qα±B·WNQΨp§‡μH(ÌL)5šλ•U†'b)£=YK΅Π'ͺ‡CΫ” hΆ{Ά–u2ωN8ƒΰEnσX₯eTέκΟα¦iυ4tς‘R’²*K‰ζμ»Ε¦Κ¦$MΛ_“VLκQ/Lώk^£ŒEŸ` Ϋω«…ύ˜>= ~}ΞΧBU܈Lιζκn;π$…?Σ•Ί ½•zž-1&<ΛY +6-LGχφz¦ΒζάΎΜΕ—JB ]ωŠΐˆ`tΕ +ή¦ζΝ"ύβξ —ν€ͺΘpΣo>½Υ8ΐΗ¦mʎ+c/wΣ‡΅οΛ$Βkτœ3Α#-ta*Ϋ7‡mLgΗaC{W}^κυl©ZMΜ  MΘ ΜλΙ¬‘λ ΄©Jœ4μ$“«α~ʚ…,ςSH‘γ‘Ξzβ“}ά(C*™ξνjά0ΙMΉU=\π\{Yxίg§΄¦ΛβϊSu@ νΟ¨Z•;@Σς\ϋ ₯]Χ}³Ž6ώŒŠΉΈ p<£1ςξWΗΟŸNθxυϋ‘μ·3°ϊΓΝόMϊ·& SΙυ%Lρfνρ'xοpž.Ÿ/θ”Fΐ¦g”»“ŽΕΎH‘£mOPΒΘό˜C…GŸά«αvr³›‹έvμ‹Υ +ζ’*ς_&α>\šσΈφEΐΣ‘!π΄ΰ2!«+Ά{Όc©ΞzχΎh/‹g0 θMπΖ„†πg)Ρ}Ρw^±² +ͺŽŒ™§’V8Q²; »ΐh˜+xA=¬nZ­¬Ή½ΞQqΌijnqέρΖӍόyθε~Εγœ™α%°Š >iΌΊ·|Γ½]mΨ!Α΄GšΦ{S|«²ΙrAtˆ’L‘hΞ±΄νUήπ(UX„eηΒπ%ηK˜4K—…1²žκA.¨^RΰΨR"β%‰m?…€Ψ_xς~ΝΑΎ_ρ‚Ϊρ³φ9Ϋw™4p=Χ°Οv€0ΐ°³VUΪΨή€`ΨkU%U4k΄―X#ž>Š.Βζυ*g+ϊΎzΦt8Δe»Υ[*“FγΑΫ¬6(m(Ή&¨M toθ’™ηɍ‰8ψΈ@٧ތννr.%“G΄eXPPHε iΙ“‰ gόœε£ό[ζ ŠκλE'ΜΠkFgJjR])ΉΈYVϊεZρό1Λ™»ΣΖbκ)š‡N4ͺHΒ(―Πΐy{&}x_JΎΗ,’|5₯ΞDϋ€Έ|ϋ•%ξ@ˆR#m«Ž ƒ OΪΝa2πΨ=οώqPύ9+‘€œΜxωԚ‘Ψ|%Ή’θoΦDŸŒΏ΄e†”Iv4κFjύ `Ej ζΗ Π‘Ίwμ6IE˜\qs†‚ ή «dК +•v +OmJyλe5₯$™”Γ4+ή™_ΰ# ζΩ‰‚1ΧH΅A’˜χk˜l,T‘Θc—8Vy… +νuψB–_ŽκΏiΗχ:―šEf"‡ΑΑΉ|M9Gο>3c―ρΈΝ–aΆc'λσ?φθ8ψΊ\Ϋ k93ώRδU΅_ŸΈo KhιdΦB[ΨΑTͺ”x·ΖO€/Βώ4K?Σι_"ςb¬υWtgΚΑ¦WΜ)ƒXR­fO•g£ڏ¦σtθΜ~8t˜8ίaŽ·Ρϋ_hϊo=bβ—_V©Ύˆt¨n/MN"4ΚςEžcP™Gb—α₯€e˜+Ξv_Ε°£Z!š~°{Z ›tA„{γ7)ƒάάΒڞ~ !³V-E<σˆžx1ήdŽ›κχŠHŽ|j3ωŸžQ³ί]h‘zΆδŠ]†•·^έϊJTέΑ΄œ•aΌ@Eτ+δK‹Ό„!(ΆΚ9—ΘΎΣϊ;ž(‚—ΆŠF]^T]j!Θ{Ι.E‘>U­&Ešrsό+Xν£~Λ{l2ͺ•Ρήξs’ƒQS^+Ίb‰‡Λdښtύz4vχωG‘ΊŽͺ“R β 2Ά 9`ΐ₯™|Φ*³ K1B漞A™?z΄¬)ά πΏΐ?ΑrδΌGG\‡ΘΌaΙ‰†€Λςrυ£W“FδŸ₯Y“€dL^c4βεŠ₯1ΐ§αE_ ―cΣ± GLΣz]Ο-ξ{XΟ† ϋ₯eMT +πθhΗ™«³%_–Ν‘<„t›cΌMδκα–my«Vό΅z=υWωοπg” ΅RΥ|œ qΗγi‡Θ"hF ΰ'Ft‹¬8{}ͺ¨Ώ6 Τρ0T˜αΊŸΆ§ž£ίΦΔA*ςKuδ|#7’|ΣK0 νg•¨b»₯aA‰š¨αΨ7΅/”όeΦΖ$πŠΠh­.‘[JΝ?»ΣI?γΪΞ1ΜΝμTuΣ•4Uφ—oFόoγKΠΛΉzϋψΨϋ…'Νm)5©M\{ΰŽ›q$.·J‚œ ;€85%}ƒN‘ΓΊλŠqΙΪ»ΓεΘ1χb-Pi‹«ΎLq3`―}=GωŠb­Μ£zΫn‘’=‡υ(V}ρX©yυ>^₯Γ5lό½§J M ι —€ρσ-ayG> 3Ήd…:§Φ•w«Q +s|uΡͺ2ΥΎϊ`v3σzιkΚή£›T9Όΐ7.dzBύH7•ΓO-%¨Ο½8₯Ωέξ‘ίΨ7C―f$œeUΓ”»iςΠΜQ₯ς›ΤF +ό£₯ψ%ρ΅xβ¬ΏρښϊV'L;ΠΊ»Α΅t¬*Τ;w ^€ίςvψΡβ,†I8ς“#4ύo€ΏSŒQ΅τςmfY +œθA“±ύœ’U+ώgnυ}‘Ϊ€ϋΏΛψίό1t+τz„žΜξΆΥ)7„lέ³€^’Ν‘Fm"¬9-ΒΥΚpΫ•4‘i*‘-cΥ„„΄έΏ1ί’Ι%6‘OQEεšΜςΉΟ Ή6±Έˆ¦έιrΥ£UΔσ5QΒo½Ϊ‰O!ΐJEξTK0ο!F]”πωΩ •ΒEξμ΄―ϊa KhJ-|bΣ’cβ6τϊΎiύA +M5&‹Κ +Ο +Ϋν/‰°'ABAέь=p² μ5σŸšιPiΧέΦ1 ζ0'‰x /©‰θwΈXq&/l*‹„L/Ύό,‰!Ο%vg/΅ω +Ο‘<ο¨=²νν$k ‰K‰χ/‘ΆQc©χDΏ ˆŠ\ωR•Μi§Ύΰ˜·ͺga3XΕΏG¦ΰDTuWc…GR -’On©cΉt~ίΒκνι»Ιš’/πψpγ‘“―€qΥ΄’ΆœΕωΝΊΨzAೊ,xόzHΗΗ&!,θΟ )Υ~x{i!-ѝ6£χ”±Nδ«iyZ‰0Cζ>sΊ3αkάgœYη­μVι©.qϋ?d6,Ν”³d + α"ψΊν,ΥЈȹτ©ΌΉ‘° +ΏoΕυIτ_Ϋiq…U|Λλ•»W#tρbpGς{žl`][οi½ϊ‘άΗδ2»ΔΓu„-―•gέQψ 0[“έιμ―πΤ[λnΎoHš^ͺ3˜=Π –n‡₯ΝX"$υ«κ–Εkž^ z‘%ΑΔ$pZ<c3Ÿό«εβ-'»zX£ s }•Ό5[ Œε=•©‘†ΰ>•0Δι+ΰ&“A+CwšŽ½RRΗΖ~ώ₯L(SMѐ,φβ± ՞­EGΠzŒ;p‹ήnn]xυΌηΈKΘpζο«ζO͚I)zιA¬§—ύΐ€)b"€lοΨe-p ‰3d§‘Φϋ]SΓδΠΪ/8@ΰgu)ϋΣ":O9}₯#ΔυaXοπ²χΠφH9½§Ε§γoJJP«πβ<Τ]£υD±».3 +7g=]BSύ•'Ϋ6S) 3ΔΘιTgr}n†Ι9σGϋ~Τeώt“ήϊSa/κξςΤΎ_F‚¦xƒΔ£wε&Π–UŽoς:•Wι―}6ώι/žώΧ―P +<‚Ότ> ’¬Ηoέ–H”OXΕ(+Jh2-ΧȚζ|,—Nr~©ϋβ˜YŒ,Š@Ο!| }’[KLι9:γΡ&/Ζ!ΗvI3/ΧAπ™mξΆΩiΜ”W(π0ωΥήΤξΙ­7bcΘ υ$M|Eαωχ$˜ΰ‹π ΕgUΉσx’ΟΨR’―5_ω*–TӏΈνq1`Ά[΄~lPΪ‘MxR\«ύ$ΪgŸρƒ+nή[Ά{š`Θο„ώ?ΡƒΕfvJލωάGKž/'~―‹nl@΅yiAοΧy|νJ- ›:ϊԜ±fέb„ϋ‚G–’Ι+„«ο£Ξp"v)£Ξ Šκp΅ΡkvΧa<–ŠιBΡ b*-ZΠXwήλ:Π2΅lΒCα"ΖP₯pΗΰ>=Εb>RD&š Uˆ³/ί‡KΓ1Ϊ $,­μ,ΌFN³ή€eϋsIKΩzθΩT ϋ,'4f,3?Κϊ[νλ³`ι―ξD5NQTΔΏ4ρIΉŠT‰?σŽ“"₯ϋ“xξ·9ή₯Ν›dH€Άσγ³+:κΎ,‚1~gmkšW WσΓ!­ΤF_U£Γ[σUτλΞZBIžΙM%πx›}Cι%؟`˜GFοt2Fnaa]‘’ΐUΤάΣ?Ώ°Fΰ‘ŽμΘV¨2^ΰε:|NBVƒ/Ψ·―‹NΎMΜPiΜΦdΉόΠ4JVύΰ$8 q‚ hΣVάΌ§• +”0EΥRΆΌ νaυ©‡q¬‹«­Όο6Vlύ₯»ΞFz^jŠCGH‰9θ0iΨ$kϊІΊξ₯ατ"‡τ‚ΪΔrΗ…ο|†Θυ5Y*PƒΚΔh½ψ©ΡŠE{%{Ί4}s¦p{&σσŠ—&`؍U ’₯φr·½Β' ϊ i\dςxŸΪσG§g—POԊόΕΚK-K1c‚Χ0Οd‘5l α£Cϋ6Υλݍ«T§s?n-”oΈ-„†aƒΑ ω%©}$?“Υ:ΜΟ—b^’qžΚ#‰cf~Χ> bVP]S­"ΦE†§θ€ŽxWRz›gα—`Η’ώ'œaΌS£οΐτ§θ•D,αGbB5Λύα'χΜ‡r1 n?)->$9xHθθήaGCέ€i¦Ωn­vΔVΧ=ϊJ†΅LP»B‚’Ζ€^‰˜cΙ;/άF皱š­Θ)t1Gάn‰ΌJƒ"Κ\:ν¬6ΛβrΟwkH‘k)*(qίά:e + •"λxnj( β¦δ›’΄›ΙϋΗ4£\ΜbύJΕς›uLξ ›©B}Τ…šΐ^=bq€"+€HH΄qδΦtξŸvn©0Β?=J₯η…¨)ΙNoA»Ϋβˆήcϊ―[,yaɐαŸΐ|'±΄;p Μ₯7=IτΓ')Ω`'?ƒ@ LB׊>­ay T͌3ιΣm‹Ze$υΞ– Ν¨Ml±π†αΩΠί3¬©¨’ΘΜ΅γ"Β„ώξ}ί»‚4Dn–’wŽ―}¦ωΛZω‚©:Ϋ1jΥΡν~Λϊ<…†ΠλΫδ{Υ͌Žcα' $Χ£Bϋ`*ΝRίΚΟZξaP Ί¬΄0Χ§Tε―[όΛυ[¬ψΦΫ,Y3ό·£6μΕ½Ι“xo—ƒu‘TΘX(q“ͺ. ρ’OΪ‘ψ0νΑΑUm#Κ· KΝQŒψWοWΖ΅΄Ζ­Χ›ηεrm»> '£Uρf¬Τ#T… Zζν₯–Ÿύ4v΄ύIΕŒkΩ-Ѝ”έ +^Gή'†§‘œίD‰°δ‘œψnͺ—šv•ω&OηG ξ˜ ‹fa A…‡‡‘Cu„h€νύ._Η—cmzΌΓΖ§ΑAδαΞχ8uςxˆ<ιΒΓMVΈ:JΣ9Ά@—¨zMΘL¬Ξ³±’Οό±i‚bηPW~;9w+d«²φίoύCεT:τΐur}B+“wΎm&XΊψ3ρxΏΨ•Η­xν^ρSξ™•ς!ι’œζi’Η}Ÿ:ΫJ]θVE Χΐώ>Μbvδ Ά %ύ(NFμ*P*Κ„ή?XEžL·FόΠ3τνΗnβaœ@dŒ 0 9q€χ‡ό +τS+„A/ΑΈΘα{]‘$φ‚tλr¦žΏNΎ‘ Θ£ήWc&δip“R.)ύ9Έ€'Rω7έKvbΧνF£ œ&™s Τ•ΡÁ‰46Fcυ±¬ΗΫMm€'β–Liπ€θwώŒ3 `t”_@ήoIsNqzO!|œ{v*Fͺ‡²˜¬Ώεζ{~ΪBτŠI=o¨`”gϊ¨ήx‡gUˆ™ε θ‚Ό¨S―ΫΟ‘Τ¦Α_Cλώpώ‡ΛoaΕϊδxτ[·‡(j–q²”ΠΔόωŸΩRώ„%˜θ¬Ρ5ήπeΚΦd½ζIJ¨ž_œ$ΨΫ Y’΄ΐ€FpO_ώXηΏUt/Υ?U6 η²3υ “u™kΣ§°ϋ§4N›Ώ±<#f&ρ_IXψκB²₯Tq:ˆ·Y΄ιγ4gŸLk”xΉXΊiӏ„•c§ΩB;AαœYͺΪ“^{<|{¦! λ ƒ” ΞίΩ–ΦM!fU½βoΫ/ΨxΚκ ^ϋ?†—xϊJΠ8;³L’›bΠ€(•]EXώΪΟf"‘Žl j> ύϋΫgμxEB“Υ―yΔt¬"j7^ζ©j~Ξ^Β‡Άk)ό: ڌγ?ΈnŸ™)°ψΛβ4Ά[…‹νK€|pεπQμD›νŒ²τΓ6ycŽ"]ε pΘZν TΩA.v‘ΆŒκžlG»{=‰νwΔt¬ΗέεU_ξ‹Ι9λ‰Ίι:XνΙÜώž΄W8ΛβΜ4=eκ‰nG΄kAΜ΅΅ζ+ϊ NkPπζξπΙΩΆ$3ΝMγ;€Cδp›ω CΑΰσΔf‹6kGeCœ~ΝΛW’©7l4=«³xμ°sΪx†~QΫ’#ήT·ŸL·O¦>OT/=ψ|)λb+KΜΟΫFΡL{~_«p¦―}θ)ΓρΟE*ξcμ&.Q<)α…τΘ£Ζ<ΘΩ{γ7t†ΌskA~BΑ]7Ξe&«U’WόΏψEΞεΝΑ„R“`³Ω|²>“;ΧΞ.pε–Ί)eϊ·„›_g“±Ώ^x©•\ͺ°ρb?šΞΣN*9«‡WXπΟ jX?Ών(¨RΖ3…Σ:ιΥΥΥΗΰ%e… ^*ήΰΚDΑLηZL­\’ΞλkΤV}ΣΆ‡ &S·κeޜΫu*3·5 €~HŠe£Ώςν4Αα?q δ3³Ά'!eNPT!Ξ1(βΊX΄¦ς/„’—γkc !Ϊ-Ϋ:2πΦwΑ7Ηψ₯ψ~WKΔͺΣ}"ωΩεΥB‚6ΉπΜ‰#Όυ]xΞ–μžΩMΒsχΪ΅DΦ)ΥcŸ§@ής{“ω³ό­Z‘o|4μ¨φ*rIρš’£N(TλΌ^ Ι\ϋ6@uΒς,ΏVΌx=₯}©Tμ­NkVυK So +/ζ·‰’=“{Λ`φ/B‘KqSGΔΧ£Ω$£h)’}€O˜oCA”ŠΈ2s‘Ήψtυ™6V‚‰Š――³έ€e•΅ϊΉ²xλγ1f!Qι•\7„aΏkρ#dσεVΞρ@±›S»σ|΅μFΈα‘ξPbΐΘσ¨« Ε†bί+g`ωύ©ΘF±ΧΎό½F©œ¬ζZ­»-¦d4‚ωΩPPϋŒή³f&A­ΕΉŒ¬ήωΫ¬—nKm™6p³—D…ρf5υ›`pΠ‰Λ­‚S3ό<)k†Vχ€$Ζ7ΫMδ/Bˆˆ”Α~\nX6Šδ…βα”΅uΑ!~φšyQΨJ?%Ϊ7 ΌAΪ:Šˆ½C“ψ‘€αΛ'ΙƒaŸD˜H’žΎxpΐLEWχKδω—NFŠ{»O€Ή|Rxnχ94xαε² Ÿ£°φ«wp₯Η&[$=Όάί¬σ|²Λλ`Ν΄ ΟsϋκlΆNΛί(–vG8k£ΝάήvxטϊΫΠ©ύT΄-8¨$k‚ψ΅‡‘<ί“r­BϊdΪΨw0VΑάμ«ΆF?ΓΖ„W2ΰκx™»‘ΡΚΫ;Hβ +ΰΐ±RZX½ #~uκ΅&lhZq•¨„πο={{ξΣο{ίF™‘ΚΎ<0΄Ζ~LΦΡ’φA@EŠώ…Œhμd4±Εpά™&ΕtΦ–RlΩ Γοω1S%υ@Λ§ρsΞξ—Œρdf4ψN%ty©`~‡‰¨υAέ©#F‚ΆfΗώ‹$έ}°av”Hv$χ΄ΫΎrWΠk6C)JY}%ŒΙO­ΎΙS«΄qΠ©†X#ρώšg_ΖPaƒή»T-°Žκ ΈΝ.tήŒΜκέ1μχŠoγI(ΚΗMΞΏΛΟ`3Ρb}F¨θ'ΨN;HŸg§%A΄§>2Dέ €iRuΏγ ŽE |Μ£OΛ+œ1‹m{βΞpQγα2Eφ²π7ŠγŽ<μ!ζΪ•Ζ²BκƎѴ¨Όyd„1$Xl&=Mΰ6:UZ|Ί‘hUͺγIV1w”FΪδΙ6MΊΟt»ΐQ]€&Ύ^_>³gΥ£ž{θ +’•žX\VΟ΄Ρ²ΚtbΛTίj +Γ:•Βl₯ yαόŒeIz›V†dIO +X“”cΕq{ ―§Υ‡ r§AΤQOΨ<ϋr»>Άh’%zΚ–β-NA˜h{C™=D9 "†dιέqJ“ψμJ„(S G|Μ?K&b₯σ™§κ?ίςΪ^G©˜Τ#Ά=―6Yœynl7†λ’w~NΪ±QjΆΧ­N‰δI‹e0W(Θ―ΌΧΤ;Yj»‚ι"έιϊΜfΣ‘R4 ΙrE"ηFoΞDP•΄έ#βψ+χΣυ(&ί¬Π\’΅_Z"cοRϊlχ7’DΑ)‰$ηρθ±kΥτ«ψ•>2Šθ"aθM2Dτžω„,7/id?ŒZŸA«hxΖ“Λ{\²‡vρΏρ]αUξ΅άΤΘBχ±tjϊIσU`%ͺ «ζuΚYW―}’ +z”XO^qQOW‡Œ 1aΞ%3φχ|{B!­Χ§πα‘v8γ$ΏσΕ 3‘©βLέρLΞΒI1ΗΞ#έGN\ΘΫΚ‘³)¨·I'Δ…ΊJάβ,XKmμ\πω!%ΟrA6H“Χω_ηΊ e©R4CTL+―π€±‹Uγ™n;FjfzΏδνqZΞgΛ"ξξΐ (]!5`“DlD(vΛιψ9:G½Ξ₯t<ΠόnTΑ™jσzͺqƒ½G/zQσΣGΞ¨mςˆ›:Τί^ι$;Gυ&Κό›ynE‚³@)•Όƒ™«r(F œYϊίΔ|N6”„₯…Α:ƒχld‚|“&:~¦NUh€%Χ—WνOΪ”x%mg΅ΖwΏ\Z±7δhύO`uέ "ΆΗ‘€]ωβηpΪ]B¬Eφ Ω +Kζπ1ΨΉV°q*}‘ψ₯=( O駏XehŠ‹ω&Œ¦<ΓoG9‘‘E֞FYŒωΎ‡,·5VFδFMB’λ¬$Τγ΅\νa$ΧφͺzF=Δ$YšŒΖnG8La%¦›ζ |Ιk.²© ώ8@W6FhŒ_„+ΣΚΆΡƒkΪaR˜ΦΨ#ΨTN0Ϋ ςιφtΗyΟΒ'Ή5+ΡΞρφΣτ0VčΏ hήRρT¬e—›?}kšŽΟ»©σ۝KŽ}λ/η‚όXαά)vΏ3P2CmWnͺ£ε΄Ι₯Έ̏1κοΙ—GΝΠ*ό»–Y\»$oτ.9‚Χ­aš1Žtx–δ.]ώψίυ%ΫT`–S-τ…©γω“†ŒS_ΠΎ²€Ž1Q#CΑόΉΨΜςϊ/a]₯—/K _Έg-xZˆIΗ%kΎuδ8/Ε νeΠΎ™ΐ‘πΎ+λΨΊρ>ݏ†CQ—73n{°αi]Ε½BžΧ‘¦ΨMPΙ%ν†h m4ΦΎ•δ€ΆχVβλ˜xBρi]fuΈ»Χ‰ ⸞βGΌ]Τ;!šfΐˆΠy ‚Ν2'ϋΰΦa™“’Ωχm59@Ρ ¦‡7j2›5χW"}–α5Mί΅€W ²GΉ0ξS4ςUΩ\šTΪΣQβn>6fΘ;ήn}NιO§ν$ՏfΦ¨νš•lvύ<ωyHv@>k-KΣΘτˆ[Η™2$\ΰ΅ΨΠπφΖ‘ F•΅ΦOφΞ쎡œh™ωύΘ†0@ΆXη εςΊoΟ“ϊυγύ6+'T°cώ΅W§5›δ19hβ0¬υυίωv„xΩ+Ρ^³;ε†ΡLJ5UXJέ=’YΙρχ ΊfεtYΐ)«1ν'#ςΆ΅%ŠžJ_jμp S:&˜γZŽΠΘΑ‚kf:{qβ–Α‚\ϊΰ‹Š8Ψe—όbF·c€IΌ,ƒAC#Eι&'a[Wv±hžμr!Φν-Χx}]ε·1M Έ‰σΦW9’Ιέ[ηΛ nΫGΪ4J5mϊ’m“ŽXύͺ;ψšκ©ΐ « |+”ξ3ύgΡͺη«½Κ4λ!!yU½±χLΫp‘Έ’¬aΝr hI%/Χϋ½ΟΌ†¦[…†)«ϋγʎΑ|!‘ΗMΩΫυΌ³§ηΉ~ ±πͺN–Β‘‹όAμϊNyΑ„Δ8sv›kqd +z@"Zτ؟A4Φ΅Σύo₯*uNP4q)χϊ“°•ηθο’5 xŠ>Ÿ½-=AΊ―›­ήΞn—σ½iέM±flN‘€'$·†χz©΄Η³w΅o{½§eRΏXV™έ©ύ˜ΞŒ„q‹JϊΕ½ύΓσ£? +;‹ύΣ ―dΩάΗdρ¦ Η:έ¬ˆρ^―Ε…~ΒΦΞ΅,aΑd=Ψ=5ϋ­)§ΖПaΪ— ‚IƒxcΘrPsz2ˆ=TΣnΊ―|­±«BΣοηυ„ΓΑ Α+Ν¨(ΏΫqοϋΐ3cJ²ϊ‘%Z{ΦjςpώβΨ’%η[πRšP$#ˆP“)’Ί  ΘΉnx +Ρ²–Bτ@G¨Lώ/~…ˆΨ*ή“A‚υ€£$Φγ³:a|e"¦lΝδ=κ›"ΏΖN „έP-œq“@»ΝAΰσ ”Eκf ?α@^A; •ΨΛ¦ Μk‚λπεƒ.«=Μ1Υε5χEWtΓͺύΡhuλ^77ZˎΕ1-Χύ“΅h²΅qΣt‚κ8f| Tή ,>‰+[6ΎͺΆτΙew\<φδΈ½`ΊΎ.|_»F―>† "6b˜ς%N’Ο•NŒ=ί`Ψ;£qτ‹K^†ms£œώΛ"{f£")Ό/@ζBfhρ΅ώ# k‘α`βPΨ΄Η`2Χ’ Τ +i3zμ-1GΆuC6Β*/υ~'Ž”Η hN»C+Β‘’™Mηk‚o1ded©MrG€€8~¦…K–ζ}0†ΰφmŠΜsΒlΏzΦr˜9ϋε$ †rΕΣZC»νTΜM©ΏΆΔvs˜yPΥ)-ι Z0]γ PώΜnh΅ή[rυͺ)$΄°xRˆvVzExχ…Ybjb n΄€ι9uΛϊ{sΛώΜ“Im™c½₯;½μtBwPŒ)ψ*―%ώΘLQΣ,€ε¦E_μ‘-ςΈΚΦΪ…P_"Ή,*©‘hx>ΌΎΌ¨ζ”?˜Ρδ"1ρOΒλδ«"ΡΛσ QT–Oήπ‰ςΘεnhΡξ6Ώ ώ5 rD†ι#ΖvŒ’658T¦ LcωŸG@φ•°Nχ~g¦Όa€6^'Ξ½o5˜²F©6±ΊΓζ3£ΥOΈΐdjΰ9 M@Š^n<ΟΕΤ‰σ­έ^΅α±ΆKΙαΤ/£TΰFS]§.€&59c³y  zW,+κΫ†-ΜKv Aα;tε©dΜ―y3D3½Ζžvζΐύ΄ΤΈIέͺΖέ`έΓΣ%n°Ε5…¬HJ&–|iςbE4cχ―{ζ ³‘ωcaΠ·NρGIGΗΊQ­η‘Cw5!Ogn ώΞόΙZ-κ”.qLIU*κ—XS½cφŽqΥ8¦U>‡-sεg…Œε§D_K)>εŸv“βλΘͺΓ:’cκž7Ά1Q" ‡ƒ‚ZέέΗ= xΨTχ(cΝ!{"aΠΰd8o‰x*2-|«ϋYη_ˆΘD±Κ8άΨͺy™€¨JKz41ΐ-œά0 ρΕdoΦt`i=ˆωάΏΜb«"mCώ‡ρν¦”Κb†4jκΞ΅βFΏzΧϊ1HFφU£«ψ`†.]ώ‡pϊμΑA¦<γ41‡Λφ’ελδ~Πͺ,΅ζwδRl”:°@Ύ xΙ;¦Yr/ 2Βͺ‡ΛŽ£h¦9 +S[ˆΏυΪ”ϋυ•qλ₯ΫΓJ¬=σL*ͺ #kΥ4™1ox{νg#rε£Σ5ΝsχΣ·‘ΦƒITlφ£­mΜϊύR[ΐ—>}€Σδ­wυ5^R}ُܦŸΨ@€ q΅Ν.˜½A”‡y>φΟ©:ίLTb€¬ΦψŸšΣ­Ύ˜'Ι3Ž•ωΦV ”ζ‰)žf£*Α¦=?όχ ξΧ πε0―(#πϊβˆΫ*‡_‘ςVΨԟn>ΪO‰»„ψ<9G7«V qθΡ[VΦ8ΗW0oDžςα*–bΪμΨώηk½„*IΠ’ύ“x>&ρ »ŽΧC O\Ί UχDνmΒ’»Δ%DŒs°ΥΈ‘dŠ€Q$ ›ζ"NlΠ[‡lλΩ6jz$œK‹Š‰_Ε’=$ΣeΏcΰΝBΝ'μ6fΣN5“U`2ϊ|βΦ€'Κ‚8Όͺ/j՝κzОϊ­ + UQΩ±vΉτtΛ.@„Δ57”λΨςσξβίΥD@`؏Αr@9FΠ{Ε!8Β p7σH'΅σG―§υIK +/υ39X΅’±ρκzΊ«Ϊ4§¬Θ³<βτEx>‘ιψΰε5¬ώWŽGaχY°‹εkή‘f‚z5Ξd]q*Sύ—Dl’΅,ΰΛ_¦)Ρ8ν‘#k‰;~„δp—J=κυξoIέZZK}jΌβ6‹βΤΛ6ωσ¨™3SΝ6Χ>θ±tƒe~\P‚’±u&ŽΌΚ“£t‹Ξ°¦`PGŠβηYΟ΄9v\ΈΟΣ*€›„Θ]fuΘ¬ŽEOΗx +.ΣSΆ)½€iΉ^©œη©»)ζϊ!zζ¬m7ΕlΨ(ΡΞͺ‘βM„*'θI\*Šš­ΔΑ[0ωα.œΉΩ±ΚϋλyڜΦΆœiξ³F'#Q¨cλJ$ΦfN›·G²χY5JΑ%}E’ρω;]Π·υ²Π…[ζš+|\‡€ŠΊhΞ€ήοIΚϋKM½ƒύς¬| ΔΪ|8°"WRβŒvkκΔύƒΎ?jΧs6#=ͺ›9•ŒP£ρ+x.“δSi™+j°¦_A-BsλγΚνΪU‚4Š«·|‚š!£c7G†‘JVίާ:sσΗ—N9“φ8Γΰφ~%αέTΞOΘ€?-|™(O[)TWi fŸ]+'°,e±ψHζλŸdί₯ΧbΆgφ*”­8Γy6ΐ[IL±\ίXΕ ͺ“~έΈ¬ΤΥΧ368ψ’΅›’σA8^δO2”Μ‹ώ3Ο2ΥI.ΩΎΆB뽴œΨ?Γ3žjix­¬Uγϊι zΝL‚λ8%Amet~Ÿ:K’Β«jΦ@·]_JSS\1&άΩ7ςΥR9¦9ύΌr™„H(΄–mŠ•_α°ίΪ;αžC]£)ΖοώοE^B{!%Ρ΍ΛώΔeπ³~œ»Υλ[=ΘZX.ŒvΔ™Δr?m' ‘Ύ`byX›2EΧ₯0β6Ψ Š +=’"/ΨukY +ΚΊρΊxSSY6"YvBσ\ν±FΊWF™:M©›žΐ‰œjšψή솂γCYt‘Βϊ[ψŸ Ύ« Eφ’Έ.?HμςΫ€χΣ)¨JFΖDχ UΫΥΈ’Λ›&:§s;Wχ₯Ε Œθš‹ςwό’Μ†DΖt[Ώ_SρNΰ4xY4Q9²+°ΦΨ5Δ@₯2sίΠλ†UˆΈUΖΈYU_Α­/*w9ƒό&ώξi3ξ‡όΥΑ;Nυ_{0h½U‘3νόϊ”ξ<=Ν-P™σ€Χš=ΗΌ]»ϊh%k0Γ§œ°₯rSx$εbc²φΌαI‹LG{ mC­4P .uW|€–ŠηΛΓf&O ‚Œκ!’h5»νqλ<Εb^χ²Ι‡}­Y|Ÿ†ͺ6ήk”ΈμW?›adDS΅Ž‹σAψ“ξρ‘Ώ[y §χλd—i”"μUωβЌe™9S«fH ΩρF@7Λφͺ=ΪγχͺΛδ=κ(ZόΪ +•X ˜z‰Ϋ§FLt©uΘ£«`˜NO–^ίΜΛAζΞυ [Š—°²Ί˜υ@1…woΟkzϋΐ )&/² +χρ$5»‘–c<ου²„!Τ­R^Yšv!·2c_d@ξ.ΎΪrκ4Qγώ/‘@VΉC+εΡ‡Ϊ!–γo‰δσuΕwf3XMWpνJf~Ώ©WœΆFΤϊ§ώxΉυ{Χh¦αW6€SΞήΆ β*ν$Vηnm@™Μ\Χυ='Ή‘4Cντ[ €³ρ7[Ψνό-η˜τ%Cžkηb³ίΡ³Ψb Ή·•W²GCΜ΅*†Šϊ•„\#t‘fθ6ξΌ‹__Ηtf1SaoΪpSΘN“Cw Ϊ «$δ|mΐkΊ]”-ΠΪͺ?ζ ωqt‚#{nXg³βύ‰ ΈιϋŒ«rjΞφ~\{4!οίϋ©§θw›|nηLρ?;ΥΚ###φEAOί›θωΝpπUηϋnƒjυΌΈH€Ώ*»ΊBHΫ€1I1G€T»Εθ2}"ΰhNiοά…;DyŸ«!oa»σ'T-τ«ο‚²Š¨Τ£;­š…θ(φβ'‚¨‡ά³›yΦ )τ™†$"——ЊΏΫg:*Ь ·ΏLΡ†’‡M•z яΧώμ²―ή5uώOψxœςΈάογ›aUWΏ«ΔˆWr0“dfύ[έ1L_| {‚(`E­ρyεΙετlΫΈ¨Τ +ΫvΆ΄‘‘dΛ‡m€ͺbͺ¦ƒϊ‹³¬|YꄦEά`}{UΐvmAαnΦα_OΥ>νΕψx_Κ~πΣΔ¬†‚δ–ϊ,؁π=b ψ »λΞ +)πΤpK'Ζ΄ύ€AΧ&·LEͺdAFd€&τ¦ε£<|δβFWROLT +MX#Fg£92€•Pl^ΘkΘxDΌˆέ+mpNι5Ό7η(3™8NZ6yϘ`!EfΪΨ-”¦D=‘-QdΓ­r!Tœyٝ»€({°†°9ό(fΠ«{Σ‡κΈ *Ba€ +ΔΔB4Œ7c}+α€ΒΠμmήρF€’γΫi¬:\ήέΕcΧωΗΟBά‘hs CιTˆhjͺ]ŒwβΑΣλ\γoΨ‘|Oθgκ? ^iΒCP™@˜#ځΑ±$φ2‘ΐW ˜eC…_ωv\~xϊΝ–wΝVόq€\!ω3Ώ²€\’ d³hKlβnj‰yžοH’ε².g!kV-Ά!7sqͺjζ} h8F^ΡΨw²λϊΜ1ΊψC‘ΈΊΰΞΝoύnέ0^5Δyjτσn£‰ΠΪ–ΜΚ“ƒ§G3z8D±‘PPϋιΠ{΄Ϊ7R™μ΄c6ikρe'veVšEd…ζ )-ˆο©s7Vg±φ% OΤR H.(ώV=ξσι~ϋxς41’dFϊN¨ί%θ=ΆH+ν\š+π*†~ε«αζΚqενάτΧ…²/'>7=1~6Λf³γ'λ²τΫΐQ5‹ιgœΰ ά?”oΠζΰ}™Εo—C‘Θ(Ό₯9eΐ§eξΈ€;ΨΧ‡eσc½Ί pHΞκFΑΧAAήό%@ »\q•<EΪτ†ZO;FK.Jψp h΅w@Hβε@§‡Σ/FΫ’μΫ!ΝΔ #β½άjά=&aά‘C«‚‡ϋdΠ ­xΟ7SςΖr™"£28ΊσΞ8Ώ{ΨοΛžΩ3μw=ͺϋInΘ20ΧP&ˆ ΐ¬γ¨‰YΡΘjšn:ΐŸdͺRSW8£ΪCψ·δ N…Ϋθθ±Y”0ΣβELf Pφa]h;iθΞ½…=kΥ "&όΰΡς£~γύΪPΕ}Φ†+˜ή ƒά€#U†G‘b4‰ΗD©ΟόΦ+ϋ}M $tK)­μΗΝ'ήηΟD0”²nSϊΕa΅θΏώt£ΘaβΚ5fαd€”8_₯\±W う§±Τ©cΗ‡ΦjŽ™N ή€΄DϊΕ&dΉ”A‰#0€»£ΐ€ΛΘ Z!SLΊ- Ƹ坢{‰Ϋ†Θj°•†3’΅°ζ]μ·(šΈ…R‘;ςY¦EuqυΪa0Τυ8;‡]άšγŒ„<δύ#UUMWLρ I€΄™&ŠqEτ +C9fŽ!² §d³φΚaθ"­8ΰl D:ΡΨΞ&ω[όiHIύ’f:υz3θψΌ·O$qDόίvδ;"ήΠΠΐΖ Κ ·„ν7ϊ–γ-v MσAkΔψ@Θζοσu60ΐ#Zσν=©hF |ƒ«ψε]{ι +ΆγΆ ‹…šΐx²|„ΚJ‡ƒ‘Uθ”Š:XιI’“σƒτ၇EΘ +³t"•}_cΐ}«œς»ζ©ΰΒς§U:6 „ωΚ¬›Ό8#TfΔϊK!JηhαΦj`ˆž3Δ΄­Utϊ>ΐ°¦έΐγ*j€΄iM5T¦–Џές€KΖΆ˜zZց7:eeUm¦ώ4Μ©x&/·*YθDH€Εsl»aŽΈ\lΰ΄ΞΡR­@zr.\9•=4€dΈΛ—UβVό έΝΝ¨G ½ΒΚέ[΄ g%¨lj}Š«θv›λN)Z¦«¬ΞΏπ&υIφ7‹yˆUŸO§έ cŠΎmΌc]‚uZχ2zΙ»δ6:rψ½Α!(1€ΎsfQΏ›pϘxθ2Ώι§₯ΧΚYoš8£Ψ³ ŽΘlR ‚2»²AΆ^τdιΟΗo™Ήejύ‘NŽΦΪꎢιͺϊλŸ}=(°ΚόeKΈ xcήΥίb‘»ŸΡ<'c;sƒO¨‹ΤΰΤ- +_ΗR .;fθP0PΠqκ`”LŒ•_Ψ±ΣΝrgGœ„9¨€JS6o§ZώšσΏ¨'eɎΨ"SŒΡib/Bϊ– όcxαmDFΙ£ξ—––ζ υ(7‘ϋορ1ˆ €Ÿ ρk|8Nύ—)ee¬#¨ΏρύhU) Moψ9m&ΏΕfɟe?Λ/##mαE0°;@<¦ +-P`Ε±k(ΥΖ*½˜rͺDωανZΠ”­v•²π‹x8_‘rόx€ΎΗWižyμsβω}ΎίŠ˜AŠš°ΏyZGο₯P>% κ“λR„j%wEΆrτ.4‘FΠKΙp¦ ŽλoΒu΄χn‹NYrkί3]ΞΖδ›)s­ήCŽ£N“Xέ-MSL|ξκ©ΪŽZQΕζΤΪνξη η~±YLHz4ΕΖ +ό"¦ w6Ή6Μ +y<s2!\l2Dfςυ†ΐ`oΉηGŒΘι>[―ςP’]™=΄φFpRε*"Τονχ9Ξώ ΑaZμ4ܐπι:h—΅Α6 6ΈΎ `@Φ-F² ³i€ξκ“5Π³ΐβ£₯g'n{γ)†ˆν}'σ1ς0dλΎΟ΅Ž»‹LΘΙ1χεΊMφ¬χψ;ΌΧ—sVϋMθA>Χτ/ή™ ΄UŸqM@AεΦiψ©™:GP}žΘώ[›ΰ.Š-vA‰©žΤ“Qηkγ:gN'sy²ξTξ@ vœ»ΠΣΆAΪ;bνnήe +UpJψΓu„…_ο|+η> Yτ@kέQ2½δZXξhˆ;ΉΔaœaΤ₯ +ψιΫ"oκο_Γ΄w6OΧ•,­₯JΗ)ƒιπΖ’pJN ψT~’zhΜd]ΒjUΎΏ]wv©Ό ΡBu£$K/Ε―,α&/ξ. ϊ…ϊͺ<‘ζ»±l΅ˆΊεYτ3 Ÿ©ΓZΓΐ¬4ύIR»¦€•θMπθ―_`@κ +αhhμE₯0ΡΪwΛπ“|zΨ7Έ„{Θ”\jߓ΄TΈ8(”ό3ͺΤ°Όάˆ,B²-‰+€ΌΕl'NhΪ>ͺ‘·Α†κΤ–±… ΏήΧ³β£±†f-έ+G^ΉDν(ξΒ ήΉ%ΣUΰ’Ζσ-ˆ\lNΚ›…½χΒeCόΠ„ +Œω8»sζ)Q…SKZΒH +―ν-‰9mŽΡΒwS―«ΰλƒG”XI/ΛΰΒx5]Ÿ‘ΣR‹ΈvsL™"τ‘=|•Υu–Οzgϋ±ΰvV―VΈςβsDΣiI-£υŸ§•―άά4І iVύ'ˆE"ΐ?ΗpX»›7.ϋ^iΫW9˜΅Φ–‡ΝP@ί}š*5ƒΏ½wτωΗρ–QU»¬QΩͺο}\„oσŸB + gš’QpY «m-]ύΦ7B³"5顁φExbxP:τLn€'―ΘƒγσX /ΥλΎΘ`r–6ω£Α_ήZά3γ,U°B½₯NU$ž 䧉aφ#³υڞ‹~mƒεϊ˜δήƒέ9^AΣt¨εΡ!¨G<(°­JΟα)ΡjΌŸOόtΰͺ ίΦ<ž©}ϊ»Oέ½;ΐp³sψν Χλ-?£w~·"H²§ΡvšϋογΠ|έ}eO'Δ2‡@ ξτ tp~cR8šyΦ9…‹°…΅oΣΐζ1ΊLϊ‘xrv6˜]]jz(Q,ηy;Τ^ +Š…Ζ„¨θjί‘}ό?ͺλωƒΚΊŽ—Υ„mΪ- DinώWΐ©ΧN…J§Ι·Mύ¬ θ&΅Ν‹κί€Ÿ-[΅&΅xμe©­Ψ†δό―ˆ™έ"ƒ‘›ΖtNΧΟ eϊ«ΆΰeΜ σ]NuέΛ¦,sέβ`αψ ™υΒ5―W.G¬Ξμ Y£ς»BΫD8φΠμΘ6Q€#Έώ»žeL+59K(Μπ-rΉHXIZ/nQθ?ΆTΥβ<―/!)ύoRd}ξΚήRFj Ο#, Ι’ΡpΟ L;Ρ‰_ΏΩΧΦN<3lΜajFˆšqΝβΙX2>ρ˜1!ΆmΣ½ϊ€5,}ί5;Ο]:¬„"Φ~aΞ’χΠTCΟΤδυΊΚ…jΦnωγΫΪ +£’lΆή³›Cω£lΚΝ=γ‘ΟWίV ’ΎΖΰ’|<ΪυΰŸ©£ζ«\ΰ΅[ίϋΧ‘-dΠP―ΡiΎρ˜ΜVδ°IύΝς`yΑ.;λ—ΨΠJ1Q!ΆΡlό-HΣ±-Bv@ͺ"ΑW];Aτ‡©€―6;\y_ZΠ£έ—˜lΰΣ-ŽΗ²ym³r`ωfuj—Š i~ύ½}]…ΘԘ2d€½Ετ,V;Ξ§?Υ)!Sp$±jΆ-©n V–:ΓbσΫ‚σWΏ§ϋͺΗγ1}ά©…Φt8 +Ζ +s2πώ}+ ?—K"ƒ³ϋ–½Π9 „ισΈαw’ˆ²€m ?Δδ­Ro34BVQ+ddΙ­}<vJFΙxξΈ7ΪοfΎΛ_Ϊ/Ύqz«ζ ¬%(‰ͺΨUiΪc«Pλjγ?ΜOΐζh_› n1€Ώ‘>Q»MλeƒύΏtΘ συgy`zLΏu’zšϋqϋVΧ5ΩχΑFΑ*΅„φτϊ©’oΨa,ΨτYz…ΫZ+ΚkΗͺΡFςlٞ`Ώ_ΏH³³H~ƒ#oqϋΓ"ΓJdi!ˆ/¦³ςΔr‹’φη}Α…ωϋβάή<έ,Ν©a A{Tϊ‰“R}‡²EC,~Œ‘†…ΣwHζG’wώ)›+bΠ’Ά#p~eJ¦΄¬Ό³ΗΓ‚T<`·~Ο‚ό₯˜Ξ‹€jB·ΦŠδσ»έœpΚ<+Π²†S;ίl™ H±†:DqΎΥPΦ ‘°έEKυ]χΥόβ‘"‘“w9’h«½}Δ‘ύ–1θ{F³€π#ΐίΥξΠb6‡Y(=ŒIαFn’&™¬Sωρ†·ίΠQ0K™'½θ|x§ƒ"άhoc–%™γ/Φ…³β‹©ΎiΣΖΟ(υΑσΆ”α4λ7~C_γTκ@5.ΏE“h㓁ς‘’†8¬‚H)βΗaΌ8ζBΏ5ϊD€οΖΔ‡Žξ5ΣΨήv~YΨ ΣΙγa76F 6‹ώδDŒΓΛG―΄ΙΩōKϊšoΈ ―ΟύVΠΚ΄α›κΟWo‚¦όΣtρ5~eέΉ?Ήφ’²τzΜ©#Τξ#/„Dη] ©^}Š<ς<Η―φψ,”$—„Θ[­Šω*kDiMΕ½8’ΫΒ…ν6ΐIZ™ς+{ƒ}9 o š)V`W€lMξ΅·ŠΦξJ™ΐΠΣ‰=*- ΕΫΥω€Θ+¬ϋ¨DΆή…‚ΚΎ₯‹‘γyNJϊ¬`Ε"N?sΌ%€΅LηLΣ½¨~Ar™ˆ•ΛJΏEΐο’`Q‡œgNJ(1Η―VD„3Ϋ>7ΆμXXKγšέ-Y‡aΊ9gΕώRΊAσ5>Ž˜ΛŒw0³ηی°a’6)GFr ΥΔ·JΘ„$ΕκZ~h§χ=8g)Γε\dPr—c2¦“š,ΜΔ<(μŸΣqΊ‰Ά²ΛΥk0ayΨέ‡<ͺςΒ0Φp&0†P1­€X—P%ς#‡[μ΅ήΫ_R2‰yςu>+eΤκτd`΄Œ` +"ˆ¦Ό‰·™~ξ“Τ'ΜjΆΣ{bόρΉ’n2ΓΛg2iΌΔ Pmi3mπΰGψПž—†Ή‡λ=ΒŠd.4>•">ωa[Υόύ{8Ž1–B"¨Xψ=ͺΙl4?Ί² +\4Έυϊ«/>ΓZ2œC[5Ύ:"¦Ž6Ψ‚P—Ή‘ŠΒz&WAΙπ­ŒsoHβcρΣγΊEU’ν 9]<ΆδƒwE ε֜?Ν— –™<₯΅ ¬›Ι(sρZΉb…΄ͺώδηyλž^tα’Ÿjλ¬σd/‹zb΄οΧπκ0E»€ψŸe«c·,8©°Lt³WGnΥPˆ΅Rgφtœΰ*ΣN@ ·§Z„ƒ―ΔΎtQK…“[6ƒωςJΔΐ€,Τc&sΐ~H?4ΔΓώyD/,κ‘ΕKθή +GυΰΛ'ηNB#ΐ%ηWJ/₯Ÿ] πŠ Γ’=K#HAK ―Yμ΅ Ρ;i—Ο<;†Mσœ…ΆD.,ΑtŽΗ₯t•Nl¨GR[rΒ΅˜p½EάTaP˜)σψD ι`ΓΚ±ΌŒš“π "±€‘ΘΕ֞ŽLΙΪπo_#!L"—•–€1κ&H۞₯Nντβ@#«σ>Οk<ƒ·φ αC$}’πDH> Ψ{ηυ|Δ_†‹†”,0ζΗ•§™ŸΤY¦2rh‘ sΚ(exΠj=܊£fͺŸ4δE’hVώ€‹Έ–{h+wP“t‘WBWe―ΩuST+*H3ΫƒSƒ³±GΣ e²œ!λΚΕχaGς„6%γΤή›i%Σ«NνΘρ ±}j|±.9¦j$ΠF7]hŒύΚΓπΛDT9ΏHb]Ο ŸΐfήO㎏—@Tόύζ)Dq1jϊΉπͺάΕφŸdJ ±šχŠ{φzΤδkKυz ξ₯G1\?g5ύ+]|Nθ4w°M£ΫŠ\₯N?FDΐzε +65*u$JrμFB‡rώ%ρδόR? [M7’Θ"όŸς6{jmΏXHO†\(†Υϊogζhμρ—ήξZBkΔΜΈΰ3aX‡¦Ύ©§žI_πβ¦”ξ•˜ΜƒYχG•ήΑ€vν¨/CΚJ­d< aϋΟ³fΠΘΫw ©Ηi ͺQσίОS―3.Ÿ―r―uλ^ωIΊKc˜Ξρ[<ϋPάω‚Ε3$ρΡ:RδδSž¨ύNNΌΟ|κIjplΑd¦]${ιΞΎπΡH>•s’,vaA +‘yΞτ#ίεΫbkΛ!'¨T‡G?9ΠliQ!I,φ‘1vΐHˆ’nSβc4Ηζyζbβνοp]{YΫ»ΐ‘€fψ yξΤT§κσΈζ‘βUΖ—ˆ˜q\h]½yxJψΏ¬ $ +[Ϋύ.žνζzYt―^³ .,5Ψ%2’Αj(ίO±‘ζ¬d;Κmnυ··ό=(y ++hΘ#&v₯rΙH©Α3ύϋωι7ήνdώoΗή»;G6κ~…Š<:ύ‚ΖμwgΙντΩΓ5k` ³6ˆ€iΩg§3Π8vcf O΅ΦŠ‡Δ Π›Sa’$lLψmν‘)%U9λL9y¬'κFΞ mwβ (RfΊ-ΨeζιΔΈΠdŒρZ·Œ'%·‹‘ΰƒΏχΜ£η{O›VKώœB]!δη'λ­φ£ό*μ=‡!ύό±%Ξ’¬*&hœ‰RTΔZ Μ¨ΕθϋBΞ—ΈS€ήŽ2Vg¬·²‹Ε μνΚΖ€b­$#ΏΪ(?ΤKfΆveL6#t2Η&σΙ­βΘ0x«ΓYΠt›ύ\ςLΦͺ4η±ΐ}ΑŸΑ¬’νnuH1”y|ΚjuΑdΖΛmlσ[™Ί%uoθΆH£›²!L΅ͺ§jΌxM&€Pv„δ,_iβν¦oG’ΧUd% dΝ΅ΎZ)e­η3°ΑΚUΜpZ$eό5ωΎšš}θ…½˜ΆΊΘ¦7=z¨CΩ„ Ž +JεL±K~¦<‘Ϊk™σΨ‡α(Rz`{HŠBό&‹κqœ ΑεΊB!υ*ί|’<ΚxΨ π,ώVŸLˆzψr¨Ν—3ΊωϐF Ψ +•ά ξ­™Ψ—Ζ™Ν§Δy™-¬F­ƒ?£ό—βmG1Λ/ƒ8W)}m’€ΩDEλŒτ~O’Q‘QΧM&^ΣΛx2ΒρΊ‹3)\G)z {Q™ωk‰έSξΣΆwb9U̎„£$•ΐ»4™ΨεkŒ»Δ› Ύ–K ++—~³(6MFΞVŸ»%?ͺ™Ή[ΒΚΌΟ-θθIy|Cl&Ό]έ#DX~tΩ~ΉlwΆί?*VtEΖ§"££υcιv° +ˆ:š5Aκp5(ŒσPi©U<ρ1Τυη%›š‰½αα fΓtŒ–΅x Λ\‘-΄`.ρ$σ‘b{<ΆΝΧ?Ι}PT*™± ˆλ|ŒEίž„r^ eζ―’εc¨ψš#ΝϊΛΥΠp±ώΡ"1ΡgU3M*«Q©΅Π› ΰΏ>Œn8·ψν€ΨΜ­nΟΤ:|šψ1u©WνΞHvΕκ˜ζMΥέm;ΖlœΜκΆ@ξ;ή§ͺΎαΪ χP΅>Ϋ…ηΎ-2 :±ŽΙqω"ΈB© +pېRΖVΘCγJSp<@MοA›oU0[mo²Ή[ΕvΫΚρCΥ ·₯~Ο†ΘάΖ©ν»Qn–ψ:(hgί-ψ†έ―ρmz‚ΠψΖGΕΎUι=Ι°’i9~Iv›εIμA@+°σˆ=7W{ Α«»;o(h7υ?4K"*Ž+‘‹ΥΦ)ΗLceΩ%ίK\T?/χG‹"‹ώΙ“U”m΄ΖΦrF-Dψ£6œŒ<'GΆL^β» +ŽΔNঋ£·Οό5TA5Ϋ0β\yΦΧN;P6[ŠΔC9#ΣρϋρI>Βv@…εζeΆύξy4 ς g)¨™Ψw6 ˜ž±·W{pSσ"ϊHύΩ 9£…χΛ/&³eE…/Vω ΡΘNλΊ‚_»\σlxƒ` y"‹δQ&ΐ£[ι °ό;/ 4;ύ€ ‘wŽγ MΌωW»¬ΓC‹Yγ½_‡ArEOΪΠΆnkC.σ·YΚ§E`h(@·Άυ3ΕΕIZΜJ\₯§ΘΣD’ +Αv~Œ ΅vdS¦in±γ‰{V‡ΠΈ΅Ϋυk†"gίK=6劣£Eκ#»Τϊ +cL–Y>ΫίUk@8κ£…!έΧ#}NN“!₯¦΅ρΈZ*nΥAς&$„”ά\’_Κ°ι;'ΥτT0ϋXμEYΪ’£KgΧΑξ2l1h?•Œx\fΪ•!-Z†Ώ―u^υΎRΎwbάΜΓ_’žDώΚ|ρ°;θJ©ΒjΰΠurφK6_§MΗΞ=ϊ;kζ¦βyΜ¦ϋ‰UΐPπ2i“_Y;‚g.:σ+v,ΣsΌ>nΤιΙ[K,οZΏΕ‚¨cΉγ₯†{?K xΚ;70Ώε}\‡=$že mš²VFž'δ#–7¬`œmK­ΝΉ3Ž΄Xƒr¦TR6kΰζΏlΟΕϋ^BυCλT.…K˜©’AΗώ2½ζ\μ—Αω‰>ΨΝυ:3SͺˆΌzo^1Ψτ _73WΠ~‘ΘύΕs΅=†πΔTLQώ˜l§ +6’Ο―£Œ‘ ;ώ/ƒΟΤ[!αBσ­φ$£b%Υ»Gρδ'QQ/يR³ΏZλ.ϋ)›HI…FΗ–$9&Ž_π°βέ ~·σu vδ#ζΘ₯Λ$τ k2±ΪŽ΅ιΛ3ϊ‘§ΦΆΝj |ΣΡ!cΘvs+ϋ;}Ό|bδ£²4 »&mΕvαVχ―Η§·%³‘ˆ^wG:€Dή*’δ­_SM(Nδ„ωF8ρΛ­·C¦‰„!p.ΊξηΊIΉFΦTž‰θ2‚ iSWΘK*ν菒UšVΗΨ.¬;λΝ’“ΘΔicνP δ+Χv|·έΎΌωۚ”ΘzŸπσoϋΰΥ’k&x†€”>·}Ϊ• 1Dπ‹g±לνœρb©ό'ŒP#;*WF6ΙtΞ/Κ[[ϋΥ%ΜΌ‡Ÿ,;΅λU― χΙκ(ΛήYιύ’(Σ픆†‹ς Β(εfΚ³F^ΩΫ?VΈ›’rW:“ΰƒ«:6ό„’’ξ‰πix+±-}A)αΡxš$"·uxΩz_S«ΪθΤH§5eΗ6ω‘Δ;ό17”xqzψF‚d+#ιϋ(jUεvsμRJΣPμu8@₯PHχ@κά«’A¨>δΐ#9ŒβGlߏΧΩ’‹€€μΚύΥ„«_XŠi΄¬°mέΡcƒ^™”ίλ–R‰βKF&V±ͺXύeί&Ίb‰ΔpœΤۘ€,₯3‚8ΘΗέ#!vοSA+Rη_θMΒεΝ²)Ž•υρ8'R1wˆAz›ŠNΥWζ;‰—£·`tΧΩΠΊ†}_§²XŒ\Q$oΎ'Ζ|„QΌ{ΑώŠ€ωaςύϋν`ς€Τ!ΚR`‡7‘"7δN~p&ΠExvΡ2±NήS‡d2ΰ}v>ŠΔόΣu½’ήΩϊœb;D± +0‡ΛΘεWJ³n)―ΟύUδOˆ5B>’[n…¦ ρuΤε“q­΄‰Ύχέ`qΌ# g +K–χσ ’ ”'Η |ώ§΄{D–lsvR+ΫJ:ΕmγβEΊφΒ:¨ΡΧ(eκϊΌ@ 7‰ησ’,,? Χ)`£F +ΠkfOζžΫ >Ί£8ηω]D©Όά€?@uPΟS21η―qaΥΦpΑ1όΗ©ΊΦ2Y™` ›Ηl3 +ΐ»TΫ –7ζ.¦2y&:<‰Ην”j']κΒδ Q₯ΩuVΏΫΊ ›wΎ2Ύ£†ίy%γ3ΕόG lΦΟ»ΞωύuΓεbΨ:&PΝ?αžΘω/΅± ϋ±‹ΡͺT«ϊh½~x[Lͺ,cΚKbȊŁκΧ ~/ άΓΡΊb@ω±MQmF9rŒ6¬φqζc°ζ}’—~Ί”ڍLιŸpγ £ΏυΠΤΎΖ .’α="ΛCHŸ―72:(€{%g:ϋ ή)<ν7Φ6ΝΈaΌ‘{\Ό–<'z§D…«#)•:Ϋν·ΧŽBIšŽΪή}kUπ$k‘F›PE‚Λ 7½ΐ/P▁³A_œΘJHt†±“5ΠφoάΪρη +8g>΄κΰ(ρƒ.Ηρˆΰΐ;—FΖ„ qέT›-œ@Eή q°^° jσ^π'K‚pJΞθΆe_―6$t‘Ξο― +G’ +ͺtϋs ²ΗTΚD8kήΊ›`Ȝ΅L _ΫPΧ ―ƒ˜”ΕC Άζΰ!˜Α·A‹πlλ0Α³η•Gυώk_F‡:!­Ύu¨PIΌSdiΏΪf52ΌΌ§·{γv²+ίηE™˜Ωzβh_‚Υ ·žo ²¬μχ)Ζ/–ϋ‡rqWT]πΰΜΎθ Δ *eΎˆΓb/«Ρύœfα"n“ξΪjΧ€‰ξύ¦„Ψ;”Q τ,Ι’]‘’ΈŸŽ°ΟΌ‡›S0Ά¨0„υ4_²΅Πƒνδτ˜Η%zΏ‚ΚC‡σ3¦LmcOšU–IGˆ΅―݈?‰uχšPμ|šάLgbu&ΪO9ΈQ €-E~15 Σ›˜ώ$b Jmΰ\ Ά±·AϋPi­τ>V:έ‡ύ”ez·s€.ŠΉ'yWsΞ2Œ[Θ:Θ%ΦBΛέ?X;>”5&ž΄ώ“TΓ’Dό†sΆ«ύ™t}ͺ‹ 5j†ˆZϋΡ Μ,W1F +ς+Ώ±βrκ°ίΪ/vX …Θhg βάjΥ|·Ι‡F!Ίq`8{^Ε`!š·}oσΞdΨ/a΅$s5ΏϊχετdK§DΉAδ`—u[{M…;)η2°|2»=μHϊ85ΐw5OIΑ2䏧S^•Αβ„C&£ΐ‘1–cIƒX‰πΰEσθHhŽ΅‰Pχνεrή‘l끠η.l@ΠjΆ‹R$k98qέ4Zdԟ3ς γƁzhŽ›Ty^·"MόΫ³τΊξaBDM«λξnšHγ¨ϋΔΚLaχίρœ¬Z_„X‘'RοDΏrHΈ#¦wKŒCοǐιΐά±q)œΘ„ Δ8ϊ~••‹'¦Uφˆk7Ήy|¨όͺΌ΅φεˆύΣΎ@\#Γ«έ`mωωzΰ3QφyŽg/²M‹ωξJϊS Ο’ 1ε&Še|ŽYΆ¬ž9%:d@ˆˆ_Γ7σι¬im?Z†BαΆ£`Ψ’jk»ƒΚ1d(MΨΡ»χv8,ΩpρCm…#©DΪΔ^]…#τ¨ŸΘ&f‹ Ddδpž~θ†ƒιŸ±Gι[{O£ί[/βλS;˜|%Ni +ΊNΜαυ|Še„ 3o@ΘLd D ­v­b°$)Pί™Kfsς Ι·Ϊ²’,4kή„Εkt­jšΥ {ή#ΩΠSΧQΡp$B•\Ξ7α\ώH–ŒyίΔG Χˆ!„dωVΫΑUγΟ.ΉFPR4%^1Q‹yΈΙοzeτςt(ΝA«_žΏ€ΓΦcE(nIύ¬υΖ?©tξέ΄χCvmΧ{λ"―a³α‰±ηGχ³ΏόJ…ΉΫŒGiΫσŸ―@ΝXZSpx~‚Ρδξ—Εsχl©Š"τ±ύ¨IηžaGy’φ"…΄lΩ‹š’7υPΪYtŸ^…:§<˜δ6₯ νˆηIW>κ XL%&x]… ε“‘$†ω€("Ά ~t9±A”μεοσ Α‹B €u kΤDςΐ»_ŸΒkΡ;π`%+ωΏD 'Ί Ιιx`Vž³Ν`ΡŊh£‘άΞΎΦί–lŒτ‚=Χ «> +kVμͺ7¬ O„ΖΤG“ ω€•‡Τ5»ϊvιˆΜ[dοkΣ9Ž€mρ ΒΔŽΫ_‰@” lΕωφ>ΰuχ§…CΓγ›i_G3Xˆΐδk°(θrΰ2Σ%)3*Ηψ_έγώy©ΰkΦοOζ&φ HΨχΖΥg",,‚Δνσ!yͺ‚½ j@6s$og%W9'ŸIb9—Zχ—4ΒηΛ]Jδ4Ϊ&fσ{Γτ+CΞΩ"ΜzAοd‘ώφUΈ—@™°«! lΙΔx₯ΎΈΆˆš==YδΙΧBjˆ%}ι6{Qžaζ3“κ‘£x+W­½}–·Ίο‘[ jΨu«+§p‚сB{ε(6ί‘λΙŽšBκrZ±ε‘fŽΚ%Θ  Κ©νά1Z°« φ°”ύŠ1όJš?ΕW§‘Qψ_Ό²υWμYΥA$ιΤtΈZΧ„FŠG–bl}~.›˜I΅―iφMeκjs€£«ΓR΄υ\ΓJa4QgλĝG 1£Ή[œksΣ„φjΌ‚ρM‰yŽ+f4CτHd)Ύ­ΝHd&,ΰ.›ΔΤjmμ—αΨ(5–yΦ₯W«Θϋ!Ρz³νέ/ςt₯ίKpΫzB~OΈ3Α9Tj˜άΖ½8ηXWJςΌ―ςΩΛNμ°Ν_'¦€6„A΄ I5ΒdrV +”œ9ΠTx~‡ikϋSέvo»4c8§?K;ΆAΒa_@DΎ>ώD–εΜΊηxϊΑ1|Žg"gτ©&X$tρn-ί·τ y²²W@FdσΡA-ι fψΥ@gœq$ 'Zξψη’βμυlŠ„Γώ;”<œ”½—ωψ7z’ΐtβE/#Nηm£ΌΥ’B*uc +v…mξXV°Ψ&;4ˆ‰ος³ƒό›Τ¦²£πδ#,%ύ…/­uym7ΌE d-XmUƒ­.ψS«dΉέ vζω ?―έΏ$Ί±‚ŒFt@Fυ%gLε:©p_σb@0έΗY€mΒ‰hτΰ•…0Ίyi,šG a’‰ύΥڍ`-λz"2= <#ΒlXAŠ8Τ€Σ]e9χ) Ξφ³#ύ(S:β‘;e >ή€ωΨ °0€ΣσZΡ²'Χ1{ίং4& yuΐΪθXGΘs Ά‘`ε±6Φφ\ ƒ+p«cγD tΞ<Ήl•ρΖ”žN½ΒωQΝ £Cχ*Ομ§ΑS@ΰ‘π>α?²zOA{Τηi.ώ2–Tω€pvͺϊ#9˜1Ξ£UK‘Ϊ{XΛ<©Ϊ5<²m3θΈΧoΫ?>z­ωͺ©Ηy8n_Γo΅KbAακέ©}•ΒŸ5"σ”ΑsTM_"zžPLMϊ—§nT=ξ\ŸS‘o“ Γπ{"ŸΚΞ_Άϊ™Γφ'gΆ.£wdΖD*ΰ1PΞ£―Ό²ΎωiδNy†…8@XτZφE§ΈX›Δ ±¦MŠΚ1ƒΝ%[mόd+Gί4LΣ£scϊ;°Ί*Gƒ¨εόμzητ¦1ήύμΛ’ήΧώ`Μβ’{€ΉmPρ­Zϋ›™Ρ·\ΔυަAΜΏFWm―yκΪbΛa•ΈfpS’Λ½°=Χ΄μ„Ϋb~fœθ‘–WBΐΣ 1ʁʆtδ½ΰGρν†ΨEΑ₯άLLν²{§΄»9oΛ ρ+fLϋP—š*a—ϋHπά|.!„­r+L:0•]f™‘<«gY)Ω³(8|2ξσgž½BUsplI7δϊw5ΔD°Ο“§Τ‘Χ‘Ρ*#V“3a³œ% ΥΟ–ΤNEυ*Σθ;μΫΠΦ²ώ””α|:šaΩσ„υ³ + ’s‘§vδWΣ*ΐ;&[ζGŠ–φ‡ξ­d·χχsηώΉd„_š%+ͺ •~τoθ %ζq ­΄ 6 U2':[‡K}e‘‚DA’χR'vfΗ­uJφޝGŽ$¬_„Γζ<ͺ‹ΜδŠϊΘώvΧ6ώ#ˈΑΑ~oπX.c†Φ΅,Ρ…ΘΡ@‚Κΰψ’ͺΜ)β ³΅yψ>άF‹ +ΐFzΤ{ΏŠ=;Ά+/œαϊΕ8±e@υβύaϊ`<δ \5Œu+Ί3tΒ ήUYs_€\Σ‹‘q{ς*έίΑΓΣ–ΐ[ω«ΥMOh%λHφ½„Ρέe({Dͺτ,T9.χBV]έά&}°nέd¦Χ‹I¦ΏΤιΉ^ŸΓrί8P|<†τR,ΫιίΩ>τq‹HΡ„’τΈ ήΟ”G₯ζMγΛ|Έ~8dR~ζ0kg9$Ήx‡¨ψCΒπ₯P΅ςLΕ1‰“ϋl…ˆ”(ο~ηΓYU_‘s‘ώH:+„τΈ6υ[ιŒ\€eΤ·’Ιψzˆu©Inιχ mSќœ™·Σ7UV’Φ&Β€πC7vAνυ‘ο5Θ0Ώ;Τψ„ξ…Z€χQψ‡«j ₯IεΚδ;Χκiΐ₯Ο959PCΰ~޽I‚iΫ_΅ΠTœ&x\ΟΎ²›ϋ₯ήFΫ:£YmΘP%βN1–ZˆNχφ%€UŠA˜n:ΆΫ_aΰ*jqi’Hώ<εKΣεw`t» 5ƒΠasΓθ3zrHrδμHΈX/‚7²zΡ+64Ί“ΧTέ‘\Jma8Οtα9oQ h Βυ«°ςΉM,y 'ιί{Ο“jΘ]Ύ[}`Ɲwˆ +ŽIιΤ«­o€\3Yηaχ†fΘdΓ]@uc»nΞ<Ώf:±σ ™Y«ŒM Vo―z[Εΐ‘qό'D₯v²κΘϊ΅ +u& +7$_ΥΟ"©o€εΖΰfΘ&{ίxD»Ή©’oQ•Ϋ€J7hŽma!,πŠ‘4q• MξςΑQδJ0X[aκNPΜ+_`{©κ^h8³  4ΦΛ’@Ζ΄0q„!d7QrÈ`ds,Y,K₯^';XPςnhΥz5EΊ5φ*R¨M#£–"πβ=©~Q% ty―ύ ‰] \._σ9ͺβvιt*°³πΒ`pt0ΎJαώ?ϋδ=&cb…1ϋžΫ•ΩΗeά²ˆΔΠ¦γ£Ζ.:­VžLΟθΧδ€ŠϋHv’ΝG ˆoΦ;iρtJGΧ‚[6Z:š’ΟPΨβEΗΖ ζβι&ΫmσΌM=3³Υω¨œDηf[|'χ_φ·˜+ψβύ`±ΣͺCIΙα(χήο;'kjΠύ=· xmΕ~ π#«x'€Τ–T[ + …@ρλόl&γΚ5n<‘/2ΏOK.q‚H z4ΤξηΤ£Γ-$N~ »ώAΈΜ“Κ'; ’^™Ksoδβ֟ ψθzΦαqφ΅η*€ψΧ[7ρΔ¦Μ3hž‡DΨ έγˆ†)D”:OWf¦Ht»’ϋξΣf±oeu†ΤΦΗΌ£šGί~ƒΝ&±h ΝP‡μŒ‹½s5t8aω―,Ÿˆ‹τΘ!―‹±BΒΖTτ½ƒ°Ξ[ί%IͺάW§ζΐ ΅6ωΝ‡¬“ˆΰ&»|7θZ‘|W$`² (Πκ;pˆ‘2,ρθ +Υt€3WΎn›L^αλ«-F/Π>φη›•qΤ0υ£±TΓy{υͺˆz ½όnΰ€brΌg":?œtS°J‡»Z±~>―ση}B!<(–Ul0ΪϋΖΚ€Σ=ͺ4.T-u—l΅κ75BΧ+Ϊ:C[G\Œ”L'P¬K Φyy΄άΞR-<ΰΝ«(CtŸK1PϋίΝΣψƒΐ½Š:·S΄ τΉx΄„ΡΎXo‚†9„ϊb^>% |—¨ΕΚ9σ2SΥ‰΅tΛ2Rcας…ΫψT2¨λ·K€ξΉ9Θ*Œ“™ŠyΌ βΡΪ<ζΖ"ΌL8€ž+±€Ž9›‡ΚΚB©Τ%Ιb,ΔξξLQtIϋ_©M"—g!?ήΠ΄χCid€;‘„Ÿ?_Τˆ¬ζs<Θ±‹—±κy’k9σ ‡Ÿ€νλ WI0Ζ=nRΎξΫYή1Ձ“Q+κWHόŠ@ά$ή0-0&¨†Gލj‚ΗΣΛ(½p‘šA§Υ3‚jΑ€Fwš"υ§u‘™΄y‡†Θ4ΏΑω^ΏŸ:Τ:”Sω4›™•ψ 0 Ν`άw₯6₯βsŽƒ„ͺ•δ%i†Πε,Ψnc%ΰ[ +ωΣ―VXΙgJ τΖ»i Ή¦ϊΗeκΟΤ,#α&²Ϋ’rp―Z‡Ÿ’δ‹·²j>-Φ8š‹ύͺ,4’ψΕ‘Ϋ„NͺΖ΄Αo(½pΐ{l`ͺΑφ H:Ÿϋ¨Ε›€NZFRύΎΨ^Ε¨ O=[°$β5η)Ί) Ή€Oώb9Š{37ή9wς©ΣHV«΅H&φ9Ϋ7]λ”r=%-η…‡>6+Žά=₯cO+;\φgφnΥΎ%§ΚκΡ²θ k]|ΰτί5έ:ι>I)tα~:κςt븝zd5˜:! ’ΉwΠ)σJO±μ†UQ(@jσ~™SNυόΉh5€" †Pς1δͺ„ΨoΤ,δΖΘΘΑΝ>™š’έυ·/[}dθWθ~–βΝL±₯Ττœͺόΐ(^:7b)*Χ%•QqΧ[L?FάU€ςΛτAK ’[\°ΣΔ`hβϋ%ͺœΧύQ Ω ΪΪ%ι'‰>^½&MGΊ?€δ¬e‹«Z"πά§Ž‡»Ξ40²[R΅G=I”Ϊ€όΑΔI +iφtλ²³Ί*έ+,ηη™f‘sθ„ρfΞ|~₯Bμkjω―…ΎT‚JŒ”G³Cπθυ€ͺP‹uύΎO‘°ͺŠΒχ …ΘfΡ±ϊρΡURΘ[>bόAˆπhœΆ˜Τp9Ο(go«]™δz›Dΐς/©²7‹xi̝ή^€z +d„Ττ@d’ΗΦ`ώލ #7CSbΠΝ`Zν|EJ™qRdλ^neΎΏζΈ,E N@GƒV0Χ"νΘβ˜Γρ’`&ήhςγ!+Έ!}¦Ms‘­ž™½Vαν:γΡsς—³ŒwΥ  Θ8nΠυβΉ―³8mλςAeimGϊΑοΕ’“FK2Ω ΐΟ­–ž/oΦ\ΚΧLΖ戯ΞeΖ+Λ₯@P§Ν"nnŠW\Δo#ΩB< y–'V"TΩηΣ£–7žεHscsͺ^$ΗέΉΖ;CκT_²– +ηU­2MΝ*œΥKϊŠ܊ «ζˆΘγ]ςou+ςΎ©!œbY$Τux_~Ρ»Di~vrν΅ˆ0iPΑΖ-κ•’W>έΆJfM{r+oΥΎΈ+zβšλέc- ‹4bΠ©ΧΔέ2ώ₯X‰p‘"J]‚έΣ §MϋΊΌ.1#eD{Eφ§K:ΨHΓYzήqžEˆ&ύ―ά¨δoφwωμβχΣ‚υ ŠΚ”ΞvΞΪXωqž}k0S0dA-ρw Ğϊ&2ό»Αυ[Sσ .jΖp=&ΐVμΪb›|ΰ-<ΗT^Β³ΰXΜσ²Yun\s'!%θ8?x’ζ"Η•Τkςοϋ肎› b WCΩ‘§dƒtxX©ΗΕ{ΩΩσίζtΊΪήJ–ͺζ‘J’₯ -9Υ;[€Y~CΕP$lΔΦ#uσΣL^(ά廐(:βϋρ\>Wό δ½“0. 1-ΈRκ”5aZ%xƒsqττ7 ϋNΣ+Ά{Ώ,Ϊ•M΄%l½ͺTsGe%6σLΈ–…~­uώΒ”.Ρ –\ΧΒDΊ" *X±δ€Noιγ’Ϋ’ψσ~cO¨ήάušYd;Lν‚6sΉ‘ζκίC:f +4φoqPϊ’ςΓΤσρσά²¦&O~'‘Ιγ!€d©n―ΚΔτ{Ρ‰ψφ΄萅 €αOzX2΅Ώ‹cFΤηΆ^€eΪ›\ΜV$‘|ŒNδIX^ςΛ‡–G+BŒSX3–€Φ.l7Šx‘«]‰­° +μnVC-χΎx²Τρ^}Ÿ-pΧΖ‡&oΣL H†Y‘aœZ’ΓlΰωH:€„p1†Π©Ό Zwδ«Ω+Ύό₯2/yˆ§CŒΫ8*2qe5»\TnQ7ΦΉ§=€Ή΄Ψύ‘/3.»αόή\ ˆWϋ§wΠ ΄±‡—Ακ~ –|z4+[6;΄.2Iόδ NφhR%Υ‚_r˜π›J‘>J±λ₯{ιhΙ6Νπ•ΚΦNξ)A3h—rnΚΏ}Ν»ί§4wΜͺν4άj!ވy +RίΐΎΪ+ˆ•ΉH]ΊP?…vT4†G¦«Ψžkά•ŠRR,ΆΪ:δŠΞΑΙ”K/νbuλŠ7λCc˜ +'ž=Οωρ$€Ξ5„ΫWΏxqtŽ₯Άρ)sŸ('e5ρ»Ahn„‰­£9;}s)%«e| ΏOO‘ξΥjΆ²ζΘ₯zψθ©u_CΥΎϊϊ«qFεvπŒ*DΚi`d|β-sΑhC˜±―`f™χ<Š&wœ'p†Υ²†*-²‹w4G§τžϊ+¦)Κβ»Μ‘±δWΏ{#³]gΞ#«B([eΤΎiθξΊ›g†w£Φ&/d†Š7¨ϊcΩ!`ƒUΪ|,G!ό7>)Βs )RΌ­Κ.¦i&¬ασOT[JαFρtφ žησ¨ZŽΎδΓ”ρͺΞς•Α(°;TβζYl}릘‡/φ€e~ύ’|s¨z+2ΤΧσΐ”–p$ο$8υ B}κIΚjΟΞ­’Μ„šϋ“₯+Ϋ·΅κσκB́‹Žl€ύρ~Άήψ…ξeˆ"ΝDΕx¬$zœ 4lΎkΥ`Κe3ϋBΆ^σσ»'˜“ΎBΖH +Ξ“ RεG― \}Š–p¬;N˜2α; 5ÐψΠΌζί&~‡e₯Ο4"έαη ”‹τ—°}¦~]Ω’ Α„ώJPh˜άpV‹m Υ―‰ΨIrυ"ξO?²©θm ad ‚ΰrΓβ}k«£Οϊ“Αp +endstream +endobj +602 0 obj +<< /D (subsection.2.1) /S /GoTo >> +endobj +603 0 obj +<< /A 721 0 R /Next 567 0 R /Parent 99 0 R /Prev 566 0 R /Title 722 0 R >> +endobj +604 0 obj + +endobj +605 0 obj +<< /D (subsection.2.3) /S /GoTo >> +endobj +606 0 obj + +endobj +607 0 obj +<< /D (section.3) /S /GoTo >> +endobj +608 0 obj +<< /A 723 0 R /Count -2 /First 724 0 R /Last 725 0 R /Next 726 0 R /Parent 6 0 R /Prev 568 0 R /Title 727 0 R >> +endobj +609 0 obj + +endobj +610 0 obj +<< /D (subsection.8.1) /S /GoTo >> +endobj +611 0 obj +<< /A 728 0 R /Next 572 0 R /Parent 102 0 R /Prev 571 0 R /Title 729 0 R >> +endobj +612 0 obj + +endobj +613 0 obj +<< /D (subsection.8.3) /S /GoTo >> +endobj +614 0 obj + +endobj +615 0 obj +<< /D (section.7) /S /GoTo >> +endobj +616 0 obj +<< /A 730 0 R /Count -4 /First 731 0 R /Last 732 0 R /Next 573 0 R /Parent 6 0 R /Prev 726 0 R /Title 733 0 R >> +endobj +617 0 obj + +endobj +618 0 obj +<< /Ascent 686 /CapHeight 704 /CharSet (/equal/uni2209/uni2260) /Descent -177 /Flags 4 /FontBBox [ -400 -243 1032 871 ] /FontFile 734 0 R /FontName /UZJQXT+txmiaX /ItalicAngle 0 /StemV 65 /Type /FontDescriptor /XHeight 450 >> +endobj +619 0 obj +<< /Filter /FlateDecode /Length 844 >> +stream +xΪmUΛnΫ:άλ+ΨE€tαš=‹ΐ€%Š@mŠ&ΈθΦ‘˜\±dΘ2pσχ—sŽ,΅E6ކη1ލ›O?7ϋvxφσEŠŸώ<\ΖΖoͺo‡Stsc‡ζrτύτέϋΦ·ΧΥσWρcšG?‰Ϋκήήχέτ9$ίχΝΫ₯υΧ¬“JΪυk +ζˆΫ'k3ύwμΏ6Ο—ξmκϊDςS7½…€ΧEş  ’όxξ†ώ«P_€”¨ϋΆŽΨΖ9ΪΞTΔφJξ₯λΫqζ#žΑ.RZ΄]3ΝOτίƒ(~|?OώxίΏ ΡݝΨώ ‹ηi|'ŽŸ£νΓΨϊ±λ_ΕνŸΤΒγεtzσ !d΄Ϋ‰ΦΏ„ŽaίG/ΆξqΙyz?y‘ιY1―fhύωthόxθ_}t'εNά9·‹|ίώ΅¦3.y~ω=W*ό™Βμ!Ξ Ψ' +Δ{r%βš€R@‘‡β’2€Ε%•€eΐεB¬yl•@W(#Œ €ICl@Fλ<€~¦$ £<ΦαΟζ&±VΜVΝc”&jm°X@ζ)’²ύ +ΦΜ04«P#3GfΎN7&Ητ|θ3r»€1) Y‘H… W@§XιΔ‘ΐδ2^ΐD°L¦+ˆΜηQΊ+Xο!F!uE*KLuθηHκ¬NΙ W'€κκŒζίΓNY³'θΔ Η؍Œ9†02aW ΅L9&9Šμ£Ψ(±ζž%ΕάΣΒ@š{ZθͺΉ§#œ{VΨ΅Ξ8¦œΩk8F½HζΈjΆ'Νn!hβ!΅CμΨgˆΌΦW)Ωl±IE.3z]ΗlcΦgμΥwEژœηα> +endobj +622 0 obj +<< /Filter /FlateDecode /Length 590 >> +stream +xΪ…TΛjγ@Όλ+fη xFoc°^`Ψ<ˆΝ²W[;[zόχΫ5²lΎ“ΝίεAΆ}YΙη}ρΌρΝΓPž)79Κve&ϊLFΗμ»c¦.ϊ#Ϋ¬«'&9η€UΧ Ω‹›PΆ˜€ΛͺhojΩΪ a±’Μϋ[¦žω…άBρφΪυς²©Ž΅±\²Ε;v}{UΊŒΕk[ΘΆ¬NlώH"m‡¦9KbάX­X!t7ωτ²ΏHΆψΑ;{wm$³T.F­y]ΘΩη²έW'i,9_±e–­ Y_Ξ„3–ŽΧ&.χρ°©Ž€ρz<)βŒO"J„@X`b'€GΨnہ.DΈbΣ LΧθj0ˆrΧΧ`ΧyΦfJ›‡ΞΎ­AΘρΑτΧχς5Ίϋΐψ.‰* āšf# @Uhίςk2Ζς&£ς}{σ”σHY„©Έѝ[*Ž\ΔΡθΪq<Ζ1βDΕΚΑ•oδ8ΕbŒSΔ–Št +wτχ;z°=œHη0Ο‰uŽ^N’σ yͺ}Θ3«Χθ|ίΡ³ω£pοS|β$ϊmΘSν9ζ B­ύBu?·0sθŽq|s^9o[w_Œ|h[Ϊ΅šjπιӊܷ·©T©ŸZϋι_Ωkfό5γ_φ +endstream +endobj +623 0 obj +[ 720 566 492 715 719 304 270 678 553 848 714 732 540 732 601 506 578 684 637 923 681 594 618 535 524 431 538 443 336 501 563 307 288 519 292 863 574 494 527 543 388 400 351 570 438 741 515 ] +endobj +624 0 obj +<< /Ascent 441 /CapHeight 660 /CharSet (/R) /Descent 0 /Flags 4 /FontBBox [ -45 -187 2238 794 ] /FontFile 736 0 R /FontName /JESRHI+txsyb /ItalicAngle 0 /StemV 74 /Type /FontDescriptor /XHeight 441 >> +endobj +625 0 obj +<< /Filter /FlateDecode /Length 713 >> +stream +xΪmTMoΫ0 ½ϋWh‡ν!$;–Sό δ°΅hŠaΧΔV:‰Ψ°όϋι‘v—=$ Ÿ©GŠζέ·—ν,Ϊ½ωRΌΪΎ½t₯₯ίwgοξ.kΛΛΙ6Γk+[M§ύ“xιΪrkqŸn²MSŽΌiΚγ₯²λkRbίλζχˆϋ7ϋk6όι―ϋΩώR‡Ί™Ipίκαθ8_ ‡‰O˜ ŸΆλλΆyκQJι€Ό©φ„zo>κσIΩ‘nͺn#φζ)-ͺΊFώΛ“k‚·Χ~°§Msh½ΥJΜ_έa?tWRψΰ͟»Κvuσ.ξ?)s'ΫΛω|΄P!€·^‹Κ\BWϋέɊωW~Pήg+4ωŠU•meϋσ΄έy·ήJΚ΅XΕΪ³Mυί™Cφ‡[TξOλ0Z;@π 0@`ΑŒ@ΐ#°ΌΙ‘ΘΔ‘q€Β-ŠoΙΑPΘ‘(Gœ, ‡Z2°H89€ΐ²@€š“F1(Υ€4N±oHͺΓ‡ŸkΙΑπβSˆZpΑqR:Ϋ[…(.€βt€°„ιJ rΞ‘„τ SΏƒ`κω{ΧΉN‘ Ψdχ©s\°Ι'?'?.&ΏΰΙΡWw=œόρΜδ§δ“p­Ή3‹ρLϋμO±šΉ5iΟuΤ΅„ϊ숰Ήέ1ζΐΧlg°y`β6kΚ g=9ΕR­*%> +endobj +628 0 obj +<< /Filter /FlateDecode /Length 558 >> +stream +xΪm”Koβ0…χωž]P'!‘BHyJ,¦­ +Ν’  œ(©όϋρ±h+Xί=Ύ7>Χ&Ώή·³°¨4sž9ϋ Άκ›œfρο}mM&I•χ’έ+QAΕ8ΫΎ°χ¦Κ·Τ±iΌI6²μžTςFζηΎ 1λqRD§RήS°›ξθο¬ϋ€ΟvvθΛsWΚGξμΞ*ηΡ4Sϋ¦1]ς‡šΆ¬δ ³Ÿ9ηJHeWτΠZσΑ›ΞŽ₯,šΑ ;ΐše V”y7DzΜ/j3PΌ½Ά]6ςXY«›¨ΙΆkΪα“5k +jJybΣoΞΤΜΆ―λ3ΑγΦzΝ +:ͺͺή_χbσG ήRvך˜Π±m\εUAm½Ο©ΩΛY+ΞΧl•ek‹dρc.0‡γ 5°ΥΐΉΦJp!xj!, ψFΠ!„‚ŸB3Ε‘AˆH Ύ_„%„䋐*ŽΕMPFGKŽ;ZΜνkεθf‚eŽ­9[€…ΡS°cτ%Ψ5ΜΑža]»0ωΨ7:ϊsΓ6xi8‡†pdX―ΰΔpN »ΰΜ¬Ÿξΰ9ξΰ»ζώ±–;ψχ{fίl;K9<ωάΔαΫχή|qαΙwξ1ΌB‡=Γπš}­/ k=6 _Αpdύ {©9Ӝ€ο›_}’ϊδpωπPnΧ:ο›FέxύšτMΖ.%έ\]Υ¨?ύRΗDo™υ[ΧB. +endstream +endobj +629 0 obj +[ 808 945 853 853 853 853 853 853 1323 ] +endobj +630 0 obj +<< /Ascent 438 /CapHeight 438 /CharSet (/greater/less/uni22B2) /Descent -9 /Flags 4 /FontBBox [ -342 -214 906 786 ] /FontFile 738 0 R /FontName /IIOJER+NewTXMI /ItalicAngle -15 /StemV 82 /Type /FontDescriptor /XHeight 400 >> +endobj +631 0 obj +<< /Filter /FlateDecode /Length 673 >> +stream +xΪmTMkγ0½ϋWh…φF’ΏKΘv 9l[š°τšΨJאΨΖqXϊοWoG₯τ3στFzσ&έ―ΧΝLΥέ^ΟόGΞήτΉ» •žεΏw½wwWtΥε€ΫρYλZΧΣκω‰½]΅Ρ#»ΟΧΕΊmΖC^·ΥρRλ‰υ3)ΣMλ(8‡έoυϋμYΫΎ^Οφ—ζ86νŒƒ½mΖ£aύL`eίPFeτpnΊφ‰‰GΞΉVmw'trφζW5l>ι;4m=\%±=zB²Ί©ΖkFίκd,Aρζσ<κΣΊ=tήbΑζofρ<Ÿ€ςΑ›Ώ ΅šφƒέΣfΦ6—Ύ?jθ`ά[.Y­fKγΑσξ€Ωόη6o€νg―™€\XeUWλsΏ«τ°k?΄·ΰ|ΙeΉτt[[©-Ω&oΈ<ΖΗ7uH+ DVˆKσ‘ΌΘ P"B‚!"©ω‰Ή0„ˆPLΰͺ3”C¦QΖ,b»HN`IΪ"œϋ„œΜXέΚN3€ωM’©4 šH¨?Σ›T₯ΎR¨Hj-Λ`σT§9 +κΣ<’2“PΨO‘GE arۏ&χ«Ώ»α:(Ξ3ςVq‘’%.)ΞBΔ™…BœΫΚxA1Ω*8ι1c4±°ρ +±€X yځaΐΉ΅ΒAζrτδ.ΗYAαςωΚ K /]N³O\~Έήb«ƒ}‰“/œΒ8AΎrƒDΏIκτΰΌ4Έ­‡”‡δ«„idcšfiGXYο%ΞVΒβΰ+ς,βπE‘>_Β?ۘFl!ΪTjoΌWτGαΏN¦ŒKˆGγvΑ«Λ0˜»O/ έhάε¦Υ·Η§οzTя^­ι₯DφRzxβ +endstream +endobj +632 0 obj +[ 418 418 636 441 636 ] +endobj +633 0 obj +<< /CreationDate (D:20250605072812Z00'00') /ModDate (D:20250605072812Z00'00') /Producer (macOS Version 15.4 \(Build 24E248\) Quartz PDFContext) >> +endobj +634 0 obj +[ /ICCBased 739 0 R ] +endobj +635 0 obj +<< /BaseFont /AAAAAC+Calibri-Light /FirstChar 33 /FontDescriptor 740 0 R /LastChar 42 /Subtype /TrueType /ToUnicode 741 0 R /Type /Font /Widths [ 507 507 507 507 507 507 507 507 507 507 ] >> +endobj +636 0 obj +<< /BaseFont /AAAAAE+Calibri-Bold /FirstChar 33 /FontDescriptor 742 0 R /LastChar 44 /Subtype /TrueType /ToUnicode 743 0 R /Type /Font /Widths [ 686 537 503 355 474 488 537 347 226 532 538 246 ] >> +endobj +637 0 obj +<< /BitsPerComponent 8 /ColorSpace 634 0 R /Filter /FlateDecode /Height 214 /Interpolate true /SMask 744 0 R /Subtype /Image /Type /XObject /Width 214 /Length 2977 >> +stream +xνΫqδ6E7€ B)8Ι[ώπ—Ώœ„«œΚ*€ΝΘΖ (l’>ЍΧq©TCrΫ‡}ζhύν‘ +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + @u +Ό}|V7&4Œoο?ίή!p˜xΧ7Q—oΏ¬ohŒhψΉŒxˆhW7IoΑΒκΗ€P`²`Ÿ1β"^Ϋ' ƈk Μγ όλ―ί0β1b^Χ,½ϋη@vΔuE¨χΡψΌηόοοο?ώy²#ξ=θυΜ/X°ΓΟύ„4XΟIί + bΔ}Η½žΩΕμ!Ĉλ‰Nχ#,`χΣg‚Ε,Ĉ‹¨ϋ¬-#ξ>θυLpΣ‚E¬g΄Œ€?6-Xˆχχzf΄gΑq=1κx$ i°c˜ZA,Ĉ †©γΣŒwϊ¦φ‚E¬ǎ‘'^Z° #ξ)ϊ5ΜεˆcΔ5DͺΛ1΄`‘»”‚IQΰ  1β"Ακ²ΣγŒw @ΩI²`‘ˎœήϋPΰ” 1β>(;‹³Œ—Wg½_°`‘;„ι+pΑ‚±qΘ:λξšcĝaPj:—-X€ΑRγ§ίΦΈlΑ‚@ŒΈuJŽcΔ₯’ΦMΏ7-X€Αnda"f +ά΄`A FlΈn:ΊoΑq70ΨO$‹‹4h? zlW,,ĈΫεΑ~δΉ,#Ά]=f΄`‘;‡)(Ρ‚±Aψ:θ"―cΔ a9…μ, ε\θ«E²[° #n‘ +Λ1kX0FlΑ¦ϋR²`‘›–ˆΑ«* dΑ‚@ŒX5ˆM7gΑqΣ`Ψ ^Υ‚E΄™½΄₯€ͺ 1βΆΨ°­ΆcΔ6ql΄ i°Q‘Ά’,Ĉ•BΩh³6Œ7Їφ°Ν,X€AνyΡ~+ +˜Y° #n…νqZZ0F¬ΝζΪ7Ά`‘›“‹gWΐΨ‚qφ€6Χ ½cΔΝA’7ΰ", ήμhΉ~ŠX° #Ÿ½–²`ŒX/¦ ΅\Π‚ElH΄βC}Fνg/Ώ?]όχΟ?<φΏύυ›OΒ½θωγχŸͺ”†Όρ%έ#ˆM$Π1ργŸ¦Υ“ƒψTΕΟ7ξVΞqΏNΓ¦μS_ά£KƒM«·Όƒt³Ÿ`Ϋο"ŠesHM^Ϋ+hŠί” ?&BϋΠο1ZΔ: +ΰ7Aψ>92GΒr ό”·Β|Χ‡aoβ²₯τUJ—m¦€3Αψ‰Lθ twG)eθΧ@Ώu:*x&dBww‘ EΊ˜ρ+·πK@WiX!D΅Σ²ΫήxβUU Š4ξL­ό<Α‘Ι„E€ΙΫiUΫ^‘ξ‡@˜ƒR­5ŠŸΘ„TiJρs³ίyίQMΥ%‘τ6/…LΘω& φρΫ n+'Ω ΫΓsΏΗΏ*«.gα@ΘδΏΏίΓ¦…ΆΆ½ΗiB~ξτ\Υε8~ώaYH•ζ'JŸmzΫ{Ε!d%5-όjω²Αq¨ΞΎ―A’χ©?“ο؟FγύBͺ4z\ly@όΞψ σ'ζ¬νv ΆJ~»pX]ΉJâ׊²Χύ ΈAΏΧXΨΎcΫ^jΞΆ˜₯{ ΛΒΎ«4nΡ;ύπϋ4%ϊL8ό‰G Β^υ铃σ©PͺνοΕ΄€φς +ϋλΑ…ϋ£.žΡ΄„@{Β^υθ μΫ‚ŠσN„uΰ+$Œ―{ ξž@αW)†₯ 1b©ξ±`oΗ,S(Ί6ˆ{1βB”₯ΊΗ‚=„”S4˜_ O‡}tFψ›S–κΠ[° Κμω9#NιΒ5+Fx²ΎΉ0b+Ύ^χγcαΒ:LŸΑˆ_“aςŽ‘κ0ρ 51GL0Kt2Tfƒ@χUŽ$ψΠΏ4Z&†p2bΎ%¨Ω^ΓZ°η₯ΰf燡`O`¨Ι`ΔfΘ‰ŽFΆ`!5„εဏBβE Fl Ϋf_ή‚‡z²&0ρ¦DœTU`ΜG!k1bU̍{εG{²&q½KƒΧaby8’‡Y’εΑλ0ςp$‹Β%κ01„<Q@,Υd=μ°†Ώb)˜ΒEαZ%μΨ›φ‘οŸe9 5Ž(ΰΆΡdq v2Ύ―Ε₯4„Σ½ΐχd6xΙ|ͺψ£8υΉlμ¦VKgΌB3{gζlΏ9n'ΈYpCG‹Τ·ό].*βΘΑˆχ•γJJ= +Y§>1ŸGr.ϊ<ΑˆED”½ΞΖf7―ϊ–©o=Η‚Ι#^‡#ϋ™ίΰŒΪ/‚»=Ι?τO΅„A;ς4Tώr$;vQƒ>Έf ­9υβ`Οα|³°#Ž˜Ιϋ[°.š3~W³Š=„_c>”«σ†f„ΦB@΅w±ιΈOΒ° L™₯ κΰC©šEκΛυˆΑ BŒX™ΐΗ=©OαO ‡.S9T”2Ή_¨δΊwTΪVγͺB”RίZα‘ή­„―eΟrΖΗΞΙ›=uh§>1}νΒu0bΡ/‡7Pz2e WlyUjΎ9~ρqΥdˆ ΅³zU3.ŸBxΆ\ |‘!FœΉΈ‘¬\<§Ύσ₯ζx`χ_‡©e\N7ΧΥ2ζύIυΧ‚S–]°‹ΞŒ_1Καœήo3ϋcιڌΌί'p±ι¨,:y9όΊΛ +¬.…ΈζO…Πά±`‘ϊκ,—…™ήwd–‚‘φqΉ“©O‘Ԝq²©ΐ‘£Θέ8Χξ;Œ8cPξXp©o­U€πN2τΊΥ™νΧSφ̝G!m₯>‚ϋ…kŒXHzνΠg'ζY3šτ7/5_›ζή§ξ$Γ`Δ{sώˆ…εŸ6Τόfπ„ρΖορ:¨ζΐ9υ•.5§ηuφκ51β³:‹χُΰ'6’©ƒξŽ1wξ©ΔRπ²μ^α½8`κ[+ L¬ 1β΅nΟ$,xδΤ'Τ;RΈφJް>βά9|λΚ)οŸksωZΨX«ωΞt΄?›N†ρύ½€NΊ˜ΐΩPp²΄^θš€0θΆψI֏BζΤΧW©9)Γι‹{bΔg₯τŠΉ›Χε@χ{Əβκ+)7!Ĉ_ΙΆΈ4tψ-6”U:₯‚†ξ^vNFΜύ›lΎζΥs·mœϊΨΚΝ{C8+Ι]|@½iΡςά ?_³ι8 ΪΞ[bsG€Ει…bΆEΎGGBŽf¦2•H.R_6…γΒ5λ™΄¬§ υ₯5ΊzuΊ»Y +ξ ψ|BκΫθφ•„μˆoΛH(€(€(€(€(€(€(€(€(€(€(€(€(€(€(€(€(€(€(€iώOu4Ω +endstream +endobj +638 0 obj +<< /Author (Yongji Wu\nYichuan Wang) /CreationDate (D:20250609221353Z00'00') /Creator (OmniGraffle 7.24.5) /ModDate (D:20250609221353Z00'00') /Producer (macOS Version 15.0.1 \(Build 24A348\) Quartz PDFContext) /Title (workflow.graffle) >> +endobj +639 0 obj +[ /ICCBased 745 0 R ] +endobj +640 0 obj +[ /ICCBased 746 0 R ] +endobj +641 0 obj +[ /ICCBased 747 0 R ] +endobj +642 0 obj +<< /BaseFont /AAAAAB+HelveticaNeue-Medium /Encoding /MacRomanEncoding /FirstChar 32 /FontDescriptor 748 0 R /LastChar 223 /Subtype /TrueType /Type /Font /Widths [ 278 0 0 0 0 0 0 0 278 278 0 0 0 0 278 0 0 556 556 0 556 556 0 0 0 0 0 0 0 0 0 0 0 667 704 0 722 0 0 759 722 278 0 0 574 0 722 760 667 760 0 648 593 722 0 0 0 0 0 0 0 0 0 0 0 556 0 556 611 556 315 593 574 241 0 0 241 0 574 593 611 0 352 519 333 574 519 778 537 519 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 556 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 556 ] >> +endobj +643 0 obj +<< /BaseFont /AAAAAC+HelveticaNeue /Encoding /MacRomanEncoding /FirstChar 32 /FontDescriptor 749 0 R /LastChar 121 /Subtype /TrueType /Type /Font /Widths [ 278 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 556 556 0 0 0 0 0 0 0 0 0 0 0 0 0 0 648 685 722 704 611 0 759 0 0 0 0 0 0 0 0 648 760 685 648 0 0 0 0 0 0 0 0 0 0 0 0 0 537 593 537 593 537 0 574 556 222 0 0 0 853 556 574 593 0 333 500 315 556 500 0 518 500 ] >> +endobj +644 0 obj +<< /BaseFont /AAAAAD+HelveticaNeue-Bold /Encoding /MacRomanEncoding /FirstChar 32 /FontDescriptor 750 0 R /LastChar 116 /Subtype /TrueType /Type /Font /Widths [ 278 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 556 556 556 556 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 649 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 574 0 0 0 0 0 0 0 0 0 0 611 0 0 0 352 ] >> +endobj +645 0 obj +<< /BitsPerComponent 8 /ColorSpace 640 0 R /Filter /FlateDecode /Height 587 /Intent /Perceptual /Interpolate true /SMask 751 0 R /Subtype /Image /Type /XObject /Width 512 /Length 3955 >> +stream +xνΠ Β χOm7ˆ@aΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ώΒΓ +endstream +endobj +646 0 obj +<< /BitsPerComponent 8 /ColorSpace 640 0 R /Filter /FlateDecode /Height 512 /Intent /Perceptual /Interpolate true /SMask 752 0 R /Subtype /Image /Type /XObject /Width 512 /Length 13152 >> +stream +xνϋ³eeyη{Ÿ>§o§otΣ‡¦‘Ύ€\š¦»iΎ}ε"!€ΑΨΠt Γ€TpœT&±‚b3ΰLŒΖ &fLi*¦jf*sI+Q'3ΈΑF±PΛLώ“y+gfמ΅ΧΪηϋ^ΧZΟωPό°Φ»ŸχyŸχ³žοw^{Ÿ}–,α?@€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€<Μ,[Ήeϋώ½‡ΞΎν·ŽίϋΙSΌΘ€€NΞNΤ{έ²ύF'sS t˜Z:³γκ“Ηήυ ­Ξ  ޽λΉνW˜Z:½Œ-.L`ωŠ΅ϋO|hBΓπ `Œΐ“^±jύΒξ@„iΛW»ωΏλmΆ,Hΰ¦;Ÿv?ϋ™Ά767‰€{μ³ΔΏX°O€LpπηAΠ$‹4ύš{ζo²«Ω Ψ~ΥqΣ&Ηζκ Μ,[ΕΎ’Fƒ€Uξνΰι>To’†GέG=­Ά4ϋ‚to»Α°Ρ±΅Zξsώz‡ X%°η࣡Α a‡om«ύΜΎ ΐ‘Ϋώ•a£ck΅ŽίϋΌή!DBV 8+¨΅ °ΪΜμ π%`ΨθΨZ-ί!°J Φ"4Lΐj'³/@ΐ—€a£ck΅|;„x@ΐ*Z‹`Π0«ΜΎ _†Ž­Υπνβ!«j-‚AΓ¬v2ϋ‚| 6:ΆVKΐ·Cˆ‡¬¨΅ °ΪΙμ π%`ΨθΨZ-ί!°J Φ"4Lΐj'³/@ΐ—€a£ck΅|;„x@ΐ*Z‹`Π0«ΜΎ _†Ž­Υπνβ!«j-‚AΓ¬v2ϋ‚| 6:ΆVKΐ·Cˆ‡¬¨΅ °ΪΙμ π%`ΨθΨZ-ί!°J Φ"4Lΐj'³/@ΐ—€a£ck΅|;„x@ΐ*Z‹`Π0«ΜΎ _†Ž­Υπνβ!«j-‚AΓ¬v2ϋ‚| 6:ΆVKΐ·CϊžCg7mΉvي5ƒΑ –ƒ˜@ΐ΅kΧB{=υΐ }λ½ +žΐ—Lπj~½ηΩ ›ήnςͺ±©Vl˜»Β5UΏTΰUm+TY΄E^νΡ£ΰ›οϊΨ욹Α²΄I©\kυH^₯šΌdlj―φθKπ-w?3»φ’ »ζ%X΅z“Υ[@0&φ”@_,]―σοi+φ¨l«·€]JMB@χΥ^DbώIΊ‚$ 0y XpΧ#Π W‹Δό5gΗ·cοΠqΰ”—œ€h­έΓό“χ $`μ°ΰ~ 0F ϋΖTˆωkΛmΗ- GΨ)5 Ε];ƒω'ι’0s &ΐĞθΈ·/XζίΣΖ3VΆ[€±‹Βv$° Αv9σ_πϊPŒ€[@1V,Τ]ΆχΙ΅aώi!Κθϋ-`Έ ΙΫΩW1EŸ½Ϋf―o½£MΑ‘:λπ +‹1»yδ΅WΗ_π?&8σΖ«›Ϋ&ώήΒφΛ¬ώ˜`³έ| σŸΰZΌ”ΐ"ΌτΧΗ¨<Œ@7MΎ©*Μ?‘Ώ‘jA‹νζ!Μκ/&§νΰ8ζΏ _œΐ’ΊτΧΗ¨<Œ@}ΎΆ$Μ?Ή³‘P$°xnaΒ¬ώ¨5Ϋ bώ’S–‰€»Μν³vp}ŒΚΓtΝκΗλΑό3yi½,†[@˜‡0«ΏΖύΆS#dώ›ΓπςQO/#xAζoaBcV tΚν+Ε`ώ :… ΨΎτΧΗ¨<Œ@Εr»sŠωv6– Ύ„y³ϊK ;†?Z ζ/za­°z 诏QyQΧνΘ1ζߊ§±¨“·€0aV tΔσ‡e`ώ^.Dp‹μέϊλcTF`hΌ]8ΐό[t3– `μζ!Μκ/.Ψώ| ˜€0₯u–nύυ1*#ΠΗό[χ1 +&`ζζ!Μκ/.ψ?ζμΑOx©w‚ί Ψ²νΖ2: )SzM L_-[±Ζ—?ωχΞί(xAa +X½vsϊŠ”ψΎ(ΣWƒΑ”(ΜA'! §n+V/£S/‘l€@ϋ +σο©³QΆHΐχ°vΓΦκΤ€ϋ±…υΟόE!¬ΧΌή Ψvεριη΄A k}ε~4κ΅)"w P~/ΐ=;=|ϋowM§6܏]t­―Dν”[ΐŽ«O•©[?\lΚ΄–NΥ€¨Ωtξ°εΘα&\²γΐΙϋ_(#RόΏι*/ΣZ:@]8DBΐΗ~ωΦΑgž^uΡχν(³k/Ϊ}ΰ‘2ς’λ”H†—>λΞΚ†’Ω| <φ?ϋ•ΏϊO·Ύτ…½‡ίWμEυΊN‰΄A ™NuVΎͺ!Ζd ’VΧ)‘6(]£³2¦eΆ_ρr Ξ λ”H‚[Εk’ΞΚW,ΔCΐ/e₯ ΦuJ€ iϋ§)›Ξʘ–Ω| 4‰¨ΐΈS"m(ΠTn •―Xˆ‡€1e$Y»ŠS"m¨mƒδƒ:+cZf;π%\}zB]§DΪ  χFL€ΞΚW,ΔCΐ‘EΞΥuJ€ ‘ #NΧYΣ2ہ€/QS9Βtiƒ@Ž.Ο©³ς ρ0F`\>ΕFtiƒ@™ΦYΣ2ہ€/2’¬]EΧ)‘6ΤΆAςA•―Xˆ‡€1ΙΥ§'ΤuJ€ zoΔDꬌi™ν@ΐ—@ŒΠ"ηκ:%Θ†§λ¬|ΕB<Œ5•#LΧ)‘6δθ’ρœ:+cZf;π%0.Ÿb#ΊN‰΄A Lkι¬|ΕB<Œ(#ΙΪUtiƒ@m$ΤYΣ2ہ€/δκΣκ:%½7b"uVΎb!ΖΔ-rS"mˆlqΊΞʘ–Ω| ˆšΚ¦λ”HrtΡxN•―Xˆ‡€1γς)6’λ”HΚ΄–Ξʘ–Ω| ”‘dν*ΊN‰΄A Ά ’κ¬|ΕB<ŒH>=‘S"mΠ{#&RgeLΛl^Ξύμ''ξϋTŒΦbζκ:%˜nΡηκ¬ΌΔB0lψΥΏϋφΞ‡ί;{ρζy₯,_Ήξm—:rΗGt‰%‰ΤuJ€ IΪfΑ$:+rfΠ yώΩ©ιιqLM-έΉοΕ•0`ΌFlHΨ<Rι uΥ nyαS“Υ±σNPVΪ—&WΒ«φ€νŸ¦l:7Šf  όΪΛί›^±b²:¦–NίtηΣMΚJ;>Ή^΅G m4eΣΉ‰Β! μ~βqEΫ<ή€¬΄γJ1ΔX"Άš²ιΔ ˆš-@@$pΑ•W(X½ξβ&e₯WŠ!Ɓ΄ύΣ”M'& +‡0 0½j₯"₯Σ˚”•v\)†KφOS6˜Q³ˆt]4)+νΈ^‘6€νŸ¦l:+Q8„Aΐ]MΚJ;ΧC€ iϋ§)›ΞΚ€¨ΩDΊ.š”•v\―‡HφOS6•(Β `€€‹&e₯Χλ!΄ύΣ”Mge@Τl"]MΚJ;ΧC€ iϋ§)›ΞJa0@@ΧE“²Žλυiƒ@ΪώiΚ¦³2 jΆ‘€‹&e₯Χλ!΄ύΣ”Mg% +‡0  λ’IYiΗυzˆ΄A m4eΣY5[€€H@ΧE“²Žλυiƒ@ΪώiΚ¦³…C ΠuΡ€¬΄γz=DΪ Άš²ι¬ ˆš-@@$ λ’IYiΗυzˆ΄A m4eΣY‰Β! θΊhRVΪq½"mHΫ?MΩtVDΝ ΠuΡ€¬΄γz=DΪ Άš²ι¬Dαt]4)+νΈ^‘6€νŸ¦l:+’f  θΊhRVΪq½"mHΫ?MΩtV’pƒ€Ί.š”•v\―‡HφOS6•Q³ˆt]4)+νΈ^‘6€νŸ¦l:+Q8„Aΐ]MΚJ;ΧC€ iϋ§)›ΞΚ€¨ΩDΊ.š”•v\―‡HφOS6•(œΘ°3―ŸΏυΛ_άύΔγ—?Άξ²Λ–―[753£ιώΨλΖ]Χμ{ς§ΟΏYpΊKεnΌf§ψ—d•Κ +dύε—;8Ρ­/}α‘pΈb™·―λž|όΎrpσέE¦z”Λ1Σ€¬΄γz=DΪ Άš²ι¬|…ιξηoƒσΆ£7–.ΥKJ93;{ς ŸσͺΌ6Ψ%q©’—7žΠαΊδ–›Žύώ§Ούμ'΅•€<ωωΟΦξ+7ίjσΥ³|ύϊqΪγ#3ΛV5)+νψψŒΨ&Άš²ι }΅)Ζ;γ:ψΡί™έ|‘^IήΘΑ ςΰ¦/ ς9–}Υά܁§?rφ­7DμaΞl'ν+š›oIYλΉθ†λΗΧ ¬ΏpG“²ŽΧ¬Νiiϋ§)›ŽΠWžJόν_ύγ΅ΫΆκ5”‰œY½ϊασ?Pκq#jB.Sωš­—ήώ'_―*~DΩW 7ί +sΧsπ™§•KvΥήϋš”•v\)†KφOS6˜―B'Η»ϋw>rZ_½pδΎ§>8Ήώ¦Wέ3Β₯Ž/wυι_Kώ8Θ=σ_h|$˜[Ο¦ράυΈI­ή²e|ƒ£#+Wm8qίο6)+νψθΊ/iϋ§)›N²I‰γ§ψύ w_«/]>½°/7eΓΝε«_Ρα=ύΚ +ΫBν,q_Αάj0X žϋώσœ™]5Ξv~dιτς'?ά$«δγMe0n•@ςͺM¨Σ› F―—ϊήwΧnί¦―ΫJ€{†γ΅©ap‹* άƒ΅‡ώϋw†…ELpΒΡuƒΉω–W¦žϋΏω_.ΈκΚΡ Ξ―^wρΑSΏQ+¨Lƒγ50b›@¦Fͺ€Υϊ*΄6ή=WwŸκΤm+=Κ­ΑΑΆ +]Χ½κ_΅ωkD”$ vιΪΑΘεϋε[§^ϊόU½{σώέ»½[Άοί{θμΙϋ_¨θ(χiνΦ4L wGΝηΧFκΘMw₯;ώΨgH#ψ9Ζ0CG.ΌvW’χτνΔχ‰’‘•zΚH²v}ΏDΪ PΫΙuVŠ*'Ηtω ί +‡}zrς^š^­διΒ©ΓήT­>oDΟΩJ=ΙΥ§'ΤχK€ zoΔDκ¬bΤκ溏zκk΅Ήl͚‡_ύϋ°ύΆ[yΣκ~Ψv†³š2§d=_·i$a1B‹œΫ΄;Ζ­ˆlqΊN/FGξD?η_ΏχΑΰΤ—ώ0x³υ9Ϋuπ#Ÿι;Fη5±•zDMεΣχK€ 9Ίh<§ΞΚKž•`χΎϊB-FΊŸόcΜίνΊΕβ'/ν.Aε’xNN>ϊͺWΪΰΰΡ'/1>q\>ΕF&ο‘Wν(ΣZ:·q9ˆ#ξ»}:τυuv&tο“ΊgώΑ}†(κwblε¦M1ί‘οaˆ"λA+υ”‘dν*ϊ~‰΄A Ά ’ꬂεμΎΨM_eyΙMGnϊΤσοώΞ·Ξώτυΰ₯ΛOΦΏΐΑ`P›Cρΰ·ΪaqpΘ_χς±Ο|:`Ρω)uωκΗ‚—πšXΏvέ¨WΪΙΑΙΥ§'¬Ϋc– 轩œ, ―ΊoυΤWq‘λvlΏλ6!a—_Rwδ£wˆ(uΉŠsb4ƒΧ±ΎWΪΰΰVκ‰Zδ\}ΏDΪ Ω0βtU˜Tέχω{}₯σζ7–Nϋ°­ΥΞRyFϋΏ[݁rΏ”€Έd‰»ξrΤ–½ΰ ΎΚ‚©’΄R¨©aϊ~‰΄A GηΤY…ΙΦύ1} χm―Νί!R7›ΒέrΌφŠΧ—iάϊG_ +»ŽκΎ–, Λο;«•zΖεSlDί/‘6”i-•―Bηγݟ©—θοcŸ!u³‰όί­{Χ7Ύ.ΊdɞώΟ†₯zθKx₯ n₯ž2’¬]Eί/‘6ΤΆAςAU˜T/=q\\Β½§ΆD§f‰›uH%aΩ[ŽΧέzςDΨΊb~–ίwV+υ$WŸžPί/‘6轩³ςUθ|ΌώεΝΏχΙ°%:5Kε™ΤoϊέOˆλΊ/ί Γ%ζwaaω}g΅ROŒΠ"ηκϋ%Θ†§λ¬|:Ώόιο¨Ί2άG=Γ–θΤ,•gRw +Χ]±α‚0\b~–ίwV+υˆšΚ¦ο—HrtΡxN•―Bηγ§ffΔ%}σΗaKtj–ΈΩ΄Ο:qέ₯Λ–…ασ»°°όΎ³Z©g\>ΕFτύiƒ@™ΦYω*t>>wώ°ͺςΝRχ›τηGς#q]ό?ζ—‘dν*βυ%Μ Ϊ6H>¨γ +NξόaUε›ΥΚ~ό[ž4‚Ox­“«OOΨΈ=^0J@H^˜Žrη«*ί¬VφΛϋΏ°'ΌΦ1B‹œ;aƒΌd’@dÈΣuta:ʝ?¬ͺ|³ZΩο–Γ‡ΔuωόgΜ₯5•#LΌΎ„™!£‹ΖsκΈΒ„“;XUωf•ίο]ώ5}Q~+ζΛ§Ψˆ~‰‰΄A Lkι¬Β„“;XUωf©ϋMτώ―οχ?άφΗ/…ν]έŸyΰΕΚΥωiƒ@Ž.Ο©³²αa»Πgεζ9Z‰3―ο›š^zζWG3θΗ%χ₯TΥJ=γς)6’ο—HΚ΄–ΞJQεxLξόγ+Ά;RlΏξ±Χ7ΏΉΒήvμ–`8Εφ%VX²χ΅TξΉΩΆΫoέ΄εΪmW»ώθϋΛhst}ΏDΪ 0zυσλ¬DaVΒrη―,ΧϊiΦύΊίσrυtŸφΡίπ­Ηύ!ž`>£y&/α5qr £―z₯­κί}ϋ’Ν6ΌώΒGξψH>UŽg―ΫΖ{ Ljΰ" ρ4wώa§ΟΏ|έ“οίxΝΞιU+υEη#έ”»Ωχδ\’aΒ°ί₯‹Ε―š›‹ωπzaά|g¨ηΑΏωζ„o/™Y>{θΆίΜ!Ιڜϊ~‰΄A Ά ’κ¬|:Ÿ;ό*'?Ω™ΩY}­¦H—δδ>ΆSίύ6ՐiόΰΗψϋοΏΠ―μϋ~ρΣu—ν˜|-fΧ̝ψ•ίK.ΙΪ„“+αU{jΫ ω ΞMΧΞhdξόn-gώξλtτ…ˆ bn $oιeχNAΜ²^ψθΥΟwœ»ž#Ο?«,±σ&—dmB₯b,¨mƒδƒ:±0-ηΞοžΨ$ωΙ΄Ξ™Υ«>ƒάϋ]1χρ_ύJΨv†³τ +‡S²δη⃔%6lz{rIΦ&TŠ!ƁΪ6H>¨ “sξόξ™ΏΎ„Ήο©ζή―^Ldδ5gΫΛθ,½†ΡYωŽsΧ³|½τ½εξ]€δ’¬M¨ο—HjΫ ω Ξ*LΛΉσoΨΉS_Btoηή―^LL䦽{ΞύόΝ°½ŒΞk•ο8w=ςΕArIΦ&ΤχK€ ΅m|Pg¦εάωgfWιKθ‘ξ™RξύκΕGέΆυτΏΆ‘Κ,½†ΚΔL§ΉλΡσ'—dmB½"m¨mƒδƒ:«0!ηΟόα°ΰάϋ.”ιΐ™Cγ»a»Ÿ₯9>7ΗHξzτόΙ%Y›P―‡HjΫ ω Ξ*LΕΉσ»όλKxEζή―W1ΎΑξ±OͺŸόη9θ„q󝕻=rIΦ&Τλ!Ϊ6H>¨³ςUhίpΏ΄₯oΑ+2χ~½Šρ +~ϋύχ}데ϊ›fι4eH;ž»=rIΦ&Τλ!Ϊ6H>¨³ +Σoξό9>9_sξύκdτHχ9ψzΦn\―‘vzςΑάυθω“K²6‘^‘6ΤΆAςAU˜„sηwUΉ_Χ’?­‘—ψwΜ=HΊκ’9χΎ‘Ώδ5αλΕNH’π₯άυθω“K²6‘^‘6ΤΆAςAU˜xs矯Κέά/mιk)‘Ήχ«Τ°`Μ`ιRχ­žΗ>σι$ςœ°ε+LH’π₯αr „-Ί`Ϊa@rIΦ&.ΗΑ"!PΫΙu˜Ήu–8ΛύΖϋ₯­ ―έ•κ‘ΓΜ^:Ο€Θ©™χdλ/Ώ|λ©ξ‰έs ώ>―MΉ`½ZίΜaρΉλΡσ'—dmB½"m¨mƒδƒ:«nκΤ·ͺνχά·ψγssσέZξzτόΙ%Y›P―‡HjΫ ω ΞΚW‘σρΉσϋV•»όψ?z|!gεB{pZEϟ\’΅ υzˆ΄A Ά ’κ¬*OsηΛ†εΗ#?ώδΜσ—ƒsΠ*zώδ’¬M¨ΧC€ ΅m|Pg5΄P―ƒάω½ŠqΑΉλρȏ9σόχΰ΄Šž?Ή$kκυiƒ@m$ΤYω:mϊVΥ‘ύβAΞ\¦―τ>I.ΙΪ„z=DΪ PΫΙuVΎN[F§ΎUuhΏψ?ώΐ‹’’υΎ%±1"ΓtVΎN‹Ofϋ8ώγ“E²ˆ_4vqΊWxπΔρό_Μ"‹ <2L‡ͺΈίxLξόγ+NΙ]G~όΗuΑ,²ΘHc§λP'ϋjΣ«ξWVΕ%}σΗMIR?ϊ“‰Ε,]Ά,lQ1Ώ λΧσcΣ•…Φ΄:γV D6Œ8]§ζ‡ξϋ +Δ%ήύo…-‘ΟzπoZ,fΕΖ zΪΡH1Ώ λ—s›¬(΄Θ°¦Υ·J ²aΔι:½Q—ӏΧ]ΆC\β¦O=―§ ‹Όι“Ο‰ΕΈοΨ [BΜοΒϊε\Η¦++ +-2¬iuΖ­ˆlqΊN/Μ/=q\\bΛ‘ΓaKθ³.>tP,fΫ­'υ΄£‘b~Φ/η:6]YQh‘aM«3n•@dÈΣuz£.§ο~βq}‰»Ύρu=³oδφ§z%{~ύ ίόσρϊύςcΣ•…Φ΄:γV D6Œ8]§ζ‡·~ω‹ϊk.}ΫΓ―ώ}ΨB“g=ςΪ+ξocι•άώ'_žœ°ιU}‰~ω?Χ±ιʊB‹ kZq«"FœΣkrΌΙγg^??5½T_eσώWOΞιϋκ#?zeσ7θ5LMO?ϊζkΎ«ΜΗλ«τΛΉŽMWVZdXΣκŒ[%Ω0βt^˜ΊYξTι«ΈHχƒϊ]ώ΅ΰε*έcŸ΅ΫΆzΰžuW’θ§ϊBύςG€λX{qE‘E†Υ.Ν a‘ #NΧκX‰<ώV_eΉεπ!χqζ›Ώΰ~zwέtύ ίαΊξΰΔη>SΩ‚~:šgςqοόŸλX{AE‘E†Υ.Ν a‘ #NΧκX‰tšvvσEϊBνFΞnΉψ}Ώψie ϊ©^|οόŸλX{qE‘E†Υ.Ν a‘ #NΧκ8yπcΏ£/ΤnδαgŸ―_Ρ‹ο;\Ηρλ+ +-2l|]FlˆlqΊΞPχΐρHχ£γΊΛ.ΣΧj+rݎνΤρϊυ½ς>ϊ?ΧqόϊŠB‹ _—Ϋ"Fœ3Τ=°6ςŽ―~E_«ΘΑΰ_jmρϊ ^yύίqΰ:V.±(΄Θ°Κ’œš'Ω0βt£ξM‘ΧœyX_|䡏kͺ\ΧΛξ©;\ΗΡ«, +-2ltEŽΘ†§λ$ulŠtO6νέ£―X2rnίu1oϋ·¬Χά_η:Ž^eQh‘a£+rΌD6Œ8]'9΄Έ˜χλ½|#ΐύ‚ΐι~?f_ΓΉ<{υύΓ Ξp‡ZZdΨp9 Θ†§λ0+|ϊΠχΎλήfΥΧΝιΎΩς=σΏo§2qfv•R° «Lμέ©νλ¨\ΔωQh‘az=DΪ Ω0βtUBƒ:}ώεMΧνΥ—Ξιϋ<|ώ ·ΆρšJ΅.,α’m₯2|•‹8# +-2L―‡H"Fœ>55­ΰ +ώ{XMΦδž!ο:χθ’Α@Y=SΜΥο}O’gώ£{άχΤ•j]Ψθ¬ώ[½ŽΚEœ…¦ΧC€ ‘ #N_9»QΑεΎ“'‡G½σkNΓ"JbŒ{"ώ£ž΅@άΏ&fV―ž\† HϋŽΪJJΪ»Ž“―ΰθ«’Π"ΓFWδx1ˆlqϊ%;€?‰²σ‘Σ™όΔύyθ㝽xs™kκΎήΑύ†oςϋGαœϊNϊwΝ`ΰFγm»Žz7ŠB‹ Σλ!Θ†§<υƒ…Β –.}ΰ[•Υ¦œ!μο»/ήτϊ²hύB»―tvΙέ»euώ!"ηπ΅ +pƒ'ΏψΉa˜½3ΧQo-Qh‘az=DΪ Ω0ϊτνW-π'χ~ΰΧ‹9•ϋκΞΫΎς’ϋσ[ξ/0ϋε+6\ΰήzπ½ nŠ›θ¦»$.•Kό}ώΑwŸtω/ΌvΧτͺ•ξw°οCOfϊλ6ΑEζ›Ψχλ¨·œ.΄˜H½"mˆι―Ή'οαˏ4AΫuφΜcπ³|FAft@“ΖΗ½΄<Ύ.#Ά ·JΨΔ}G_·qϋπYΠ`jκ’w_φAmRrΠ½%LnΎ³τzˆ΄Aΐ·C’Δ½ηΩύǟΊχ/½ϋ›‰Ή%F~t–€ξ!It·`½"mX°%ςtV•2tΙ'ΓΡΜz=DΪ 0zυ —‘«@ ³t)£M½"m(ΣW΅«tV•2t©UPςA½"mHήBzΒ2ct–€ξ!Ί¬b"υzˆ΄A ¦["ηvV•2t‰Τš8]―‡HΔΖΘVFb¬ΞΠ=$‡Ηsκυiƒΐxι¬*) eθRF•z=DΪ P¦―jW)#1V@g θR« δƒz=DΪ Ό…τ„U%…A  έCtYΕDκυiƒ@L·DΞ-#1V@g θ©5qΊ^‘6ˆ‘#¬³ͺ€0”! {HŽηΤλ!ρ(6RFb¬ΞΠ=€Œ*υzˆ΄A L_ΥYURΚΠ=€VAΙυzˆ΄A y ι ΛHŒU ΠYΊ‡θ²Š‰Τλ!˜n‰œΫYURΚΠ=$Rkβt½"m#GX‰± +:K@χΟ©ΧC€ γ=Pl€³ͺ€0”! {HUκυiƒ@™Ύͺ]₯ŒΔX% {H­‚’κυiƒ@ςvV•2tΡe©ΧC€ 1έ9·ŒΔX% {H€ΦΔιz=DΪ 6FްΞͺ’Β P†€ξ!98žS―‡HΖ{ ΐΘ-w?sγ±ήσώβτω—ˍU ΠAΊ‡P₯[B―‡HΚτΥp•½‡Ξέ°uΙ’Α₯7lΊnο­τ₯j“’ ›€ξ!Ce=Πλ!¬ν4šόδύϊ’š ]ύήχ<φΛ·rˍόθ&9ŒJ)ίρψΊŒΨ&―—*™·^qt2ΙέO<ή)mR r˜¬ˆΡW+jΚt:Ί"Η‹@¦Fͺ€=pκ_ο™OΦΑΤΤ}υ/s+Žόθ)Τ W•ι΄fa†LΘΤH•΄[ΆοW(Ί§@έΡ&•@ 7Eσ1Ae:Υλ!LTI»rv£‚kνΆ­ΉG~t‡€"Šω˜Š 2κυiƒ@¦Fͺ€ššVp-]Ά¬;Ϊ€δ& ˆb>¦"¨L§z=DΪ ©‘*iuVΉG~t‡€‹Š 2κυiƒ@¦Fͺ€ΥYuG›Tάt]T•ιT―‡H25R%­Ξ*·βȁξΠuQT¦S½"mΘΤH•΄:«ξh“J ›€‹Š 2κυiƒ@¦Fͺ€ΥYεVω!ΠΊ.*‚ΚtͺΧC€ ™©’VgΥmR rΠuQT¦S½"mΘΤH•΄:«άŠ#?ΊC@ΧEEP™Nυzˆ΄A S#Uκ¬Ί£M*@nΊ.*‚ΚtͺΧC€ ™©’Vg•[qδ‡@wθΊ¨*Σ©^‘6dj€JZUw΄I%ΘM@ΧEEP™Nυzˆ΄A S#Uκ¬r+Žόθ]Ae:Υλ!LTI«³κŽ6©Ή θΊ¨*Σ©^‘6dj€JZUnΕ‘έ! λ’"¨L§z=DΪ ©‘*iuVέΡ&•@ 7]Ae:Υλ!LTI«³Κ­8ςC ;t]T•ιT―‡H25R%­Ξͺ;Ϊ€δ& λ’"¨L§z=DΪ ©‘*iuVΉG~t‡€‹Š 2κυiƒ@¦Fͺ€ΥYuG›Tάt]T•ιT―‡H25R%­Ξ*·βȁξΠuQT¦S½"mΘΤH•΄:«ξh“J ›€‹Š 2κυiƒ@¦Fͺ€ΥYyύ|nΡ‘] ΰZ]ΧEEP™Nυzˆ΄A S#Uκ¬6ξΊζ‘Χ^ι‚<©ωœyγΥMΧνΥuQT¦S½"mΘΤH•΄^¬Έδ³2w€―ω;ωT•ιΤK§ ©‘*iƒ)/VξΩΝƒ .85$'ΰΫ΅·—œ|*‚ΚtκUΑdj€JΪe+Φψ²βάyHΨ:σwΒYΎbmEP™N}EJ|ί dj€JΪM[ Εƒ ΦύŠxμ3―šΉKvW•ι4@€Lι5LTI»χΠΉ0Jό+ ‘ͺEa?ωΟ«fοαsAe: )³ϊK S#₯}aΓάa”Έ΄θZ,„@ŒωoΌθΚ15½˜i$L‘Μκ/L4žφθ=ΟΞ™ Ε- ‰ ‘€1ζο$γ„3¦L#aςdV dj€Ϊ΄·άύρΩ΅›ΓXρ^@+ήΕ’‘‚Ÿω;™¬Z³ιζ»>V+₯LƒaΪdV dj€¦΄ά"ύ„ι="Π/σwšν―Qy&£Ξ7Ξ- GF©Αzgώψ˜…φzV>ŸŸ™[@°«0±ϊhώψ―<¬ψ .υ%n½π1Š ΠSσΗΓ,΄Χ³²šόδδάΌ…)'Π_σΗ{νδaΕOΆθά―r θΈ›Qž^›?ώf‘½ž•ΫαΜΟ-ΐΛaξ,Ύ›?ώίk'+~A.ΐ- ³žFa"揇Yh―g°we n’ΟΦA6Μο΅“‡―˜s™nt6JZ€σΗΓ,΄Χ³Κx»Έ +·€έ†€N°dώψ―<¬xΡ™‹…q θ”ΏQΜΖΜ³Π^Ο*fμϊBά&x/u„€=σΗ{νδaΕλΆ\2’[@G\Ž2j ˜4ό?ΜB{=«€«{­Ε- Φyl€UσΗ{νδaΕ{yrα`n­{T6ό?ΜB{=«°₯ϋ.Η- β?œΆHΐΆωγ½vς°β} Ή|<·€₯‡’ΜuιΏδ¦Σ0aV „υIαY·άύ 8rhD”'°Μί‰ΊΏ>Fεa +;yπrάΚ›+ΞX$揇Yh―grω‰ά0δςωγ½vς°βΛΫx̊άΚΰb^qQ™?ώf‘½žγΖ­Με°˜ Ήδή›ωγ½vς°β[ρπΘEΉ”΄ΑΕΉΦ"4ό?ΜBϋ;k0˜Š΄βΆ¦Ηάϊ{½¨ΌϋVυ䣞΅Κν>^*LH`ωŠ΅΅mΠ‹An ;TIτΪόκ“@ I_Μ]²§VίT$·€ΎtΪb¨³οζ/†.έγήΓ皬΅/γάF/(Ηm0`ώψ[ΝΣΚΊζμ‹ΙO“[@+ύΓ’C6Μ^PσkΦo9zΟ³“}΅G―r 0ί±έ σΗ;Ϋc ›ššήzΕ-Ηούdμ])•[@Β&!•Hΐ’ωγβEοiΨε»ξΨ}ΰ‘£χ<§ΨicΈτ΄3{ZΆ1σΗ{Ϊ‡bΩ}΄tߚΉˆΝ@X${ζGΆDΗ§ϋziOγΉtΌ ”gόρ9a =υσ€²ΉLh^Š$`ΥόρΘΖθψτ#νο”›οϊΨ욹Ž_ΚλΧT΅ϊ«‹Ι•χξrP°N`ς₯·χͺϋ€λ†Ή+t>DB`2χ+3–>5=.ωΙΫηΥ^Ώά‹`δ…½‡ΞΝmΉΦ}ΣΡ`0θυε£ψVΈΆqΝ3wΙnΧHζυ +a-Cΐ|χ²A@ †@#b•VΔ4s!σZρ%-Cΐ|χ²A@ †@#b•VΔ4s!σZρ%-Cΐ|χ²A@ †@#b•VΔ4s!σZρ%-Cΐ|χ²A@ †@#b•VΔ4s!σZρ%-Cΐ|χ²A@ †@#b•VΔ4s!σZρ%-Cΐ|χ²A@ †@#b•VΔ4s!σZρ%-Cΐ|χ²A@ †@#b•VΔ4s!σZρ%-Cΐ|χ²A@ †@#b•VΔ4s!σZρ%-C`ιτ²5λ/Ωqυ©[ξώΈωNfƒ€€/2FΔ*νX:½|ΟΑ3Ύ½A< `›@»ΎΔκ ΈΨΦ2»ƒ€/‚ώΓR-˜žYΑƒ _ΓZΆ$–/K`ΗΞ[ 73[ƒΌ”΅Vk™€{;Ψ«=† hُXΎ,χF°αffk€€²φΓjν8~οσ^B0 `’ΐ±w}’}?’‚²ίώ[&›™MA^έφ›e½‡ΥΪ'°χΠY―&!0Iΐ} Ό}?’‚²ΆlΏΡd3³)@ΐ‹ΐΕ[―/λ=¬Φ>™e+½λ9―>!0Fΰθ=ΟMΟ¬lߏ¨ 8νW0ΦΜlπ"°νΚcō‡;A`jιτώςκ‚!3n<ώδΤΤt'̈"Ϊ °|ΕΪ›ξ|ΪL?³@@$pΣ;ŸvςoΓuX³C\μ?ώ”Ψ3„A8ωα+ΧwΘ†(₯=ξAΠφ«Žσv°]³L&ΰήπuΟόyμӞέvteχ1€‹·έ°ηΠYχϋ όvπdρ*zDΐΙΩ‰zΟΑGΐω΄OGύ—² @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @ ΓώR-” +endstream +endobj +647 0 obj +<< /BitsPerComponent 8 /ColorSpace 640 0 R /Filter /FlateDecode /Height 512 /Intent /Perceptual /Interpolate true /SMask 753 0 R /Subtype /Image /Type /XObject /Width 512 /Length 3453 >> +stream +xνΠ1Β υOmˆ@aΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ ψ΄ +endstream +endobj +648 0 obj +<< /BitsPerComponent 8 /ColorSpace 640 0 R /Filter /FlateDecode /Height 512 /Intent /Perceptual /Interpolate true /SMask 754 0 R /Subtype /Image /Type /XObject /Width 512 /Length 3453 >> +stream +xνΠ1Β υOmˆ@aΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ 0`ΐ€ ψ΄ +endstream +endobj +649 0 obj +<< /BitsPerComponent 8 /ColorSpace 640 0 R /Filter /FlateDecode /Height 512 /Intent /Perceptual /Interpolate true /SMask 755 0 R /Subtype /Image /Type /XObject /Width 512 /Length 39513 >> +stream +xμ½xΗώ>Ύ'ηΔ=!NHB $D „ BξwwwŠ;΄…R¨ uχR€F½΄Π(rkΧoϋ½·π{ώχpJΙ‘ωΜξ}Ξ›ΩΩΩΩwfή™ύΜG$I€@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €c" q“Ό‚€ 8)*SŠο$΅θ)₯υ–HmGIΉ₯‚™R§ωϊ_η…RιΚΫ~EσυW;LηΜ$΅κ#΅¬:KQYRp‚δ,i΄Ž‰ˆ¨΅@@ p.4Ι§‰ήJJ,ΦΣ5¨»λj©r§T{ŸΤχˆ*ΏΪΓRε.©λ©p¦”1XJ,‘"Zιλ€šˆC ΤCΐ+PŠΜR«€Ό)RΩ:©ζ *$Ο1w &¨OήT}έ"Σ%Ου0% —@ΐΓWŠΞΦΛa +gI;μ…νY&ˆŠνRΑt½ά 2(w—h,ρ’€@@&žώzy{›ώzΙ<δ-,dkηy κΆ^Ώ§Π4Gς𓠏Έ] 8n:)’”9TΟ“}‚σMMI˜Ρ°IέŠπ4±›μT}XΌŒ@@ @B5Ν»HωΣ₯ήχ8Γ:ίη›J―ή―Χ5j–'D€^#2 Œ€_Έ”ΪK*]εδK}S΄ί8½φT΄@Jξ¦W[‡@@ p> ΨΗjΏΛAϋ&?v >@ΙέΫωΪ_Ό‘@@ ΰrθΌτ&TXί:ΗfnγΥ»)5τ’!샻 ‹3—1β…Ξ€lf‘τϋn“Λ]5˜ΣΙΚ¬Ϊ-΅ξ+ω„:Cο 8=Zw½’#,8Ϋπuπι<ρ9 Cc§?βމ€oΈ”5L,ψUœψΊo”RΊ }!Η’Φ'E’Θ«ΥsΌcΓ΅·>"5Έ³ƒ7 q"Π$EοxΝIι«Τη^)gœδiΓƏ\ΣN*[+˜ίΖΐv³@@ŒKvBρ€Υ€N8η±Ϋ6ΖpΏVΌLο-{Έή [σb)6WΏ+ /πΨ’¨χηο)aΓΒπσ‹Π§ΰš¬Ο§s†ΐ³΄­/jωεΫμWΐ… bΘί`X'€@@  φ lŠύ0?τδaGœ;AjY)Ε΄•cΥυ½ m|Μˆ8P$΅ΥS.an'€τ9€WΈϋ*u~Q¬@ΐeŽ· •Ξ›₯œρϊ°\ΡYz*Ά‡ΐ+p^‡Xcρ₯ŒR§yΌϊΨv:ΐξpZ΅€σtΩ*^\ P D3ΑͺVΦ»uΞ“γ +τ‘ΆβΐΔ„Ϊ4Ί3E4H½ +S€@€ˆ;RzΨ`A‹Ή¦x©^nI άG8τ§Νςυί,°δ΅ώw|McGC€@€„ΆAυΛWuι-Β|¨’Βϋ™³ΊΑ„–$W°δ… ή(ͺ$Φ+‰MRη™.‹θ7šυ + c”ήΧ™ΞU G0ˆ0ΩYmο»I]…8ΘU:˜xO½―Nλμ`β)Ω#₯Π$žj:Ν=X–ƒ–‘\jνθΔD; xβEΕ€φ8Ό4«"‚¨'DΒ~nΗΉRl nβΔQ‡<|bΏ&jγγˆΤ*z²xq"pu E ©»ΪŽšα» ηλ :<6ιhl»δMQ}ƒ†Ϋ0v‡@@ ΰβ@©R“«ή]ασΓRαL),ΥΕa¦½>Ά`":œ +·E½VΖ4ŒζμΑ€‚Θ-(„@ΣφR―}j‘ŒΑA™pMΓέVΨΗ6qΩ:΅“ v|ΓΈ+(nθΥ·£±`o7sˆΥwq(‚ΐŸNV«^Xΐ3’8A²ίU!μ`&•JZͺ―\κ8[•VΓ‡άά‰V³jsЇ lΥ°?κ΅Woά$΄zΤnRhΜͺp‘λ ~PΕ!8% g,σίRμ}>FΉ»ύ`ζιγ™Τ2ΉCΗφΥJΖLλ³dΓ¨χέπΤ_˜ώΘΛ ž?Ήπ₯χWΎϋ՚“g ΏU'ΎF +~sŸ96ν‘GοΉΐšν•³–v9 %€δ…ΖΖ»ιμΙ< ϋιjμΪWί%aKH€“! σ’ +“?Ύ#`ΓeSw Zwχ¨”΄μŠ>3—ŒΩχΰ’Χ?ήρν?χ_½ψoο]ϋή·³}ψΦέ&Νm]ά=8ͺ©ϋβτΨ€p›φ=¬ί»λ'q΁΄Κ«φ(LySm|λπψ¬φ₯c§c=Ώό­Οφ\ό?Ε©ž½ΐ-_ύŒ‘ͺ­9½†6³AgŽP‹r λve?λ:Ν΅«:+)pZτT8p;αӊ‡—@«ΞeUs–Ο|μ5•–χμœo&η†Σρ R|Η”ΨVkͺΦÏ${Κ:‘θΆA³b(άΏ#’²+C”†XŠV9bR[χ˜2ή³Ηχ^ϊŸΦ΅ΟK?Ή IQ»ͺΎ>Φƒ…Z>+ΨάΨΣO³JS‹‡Š"€Pj(ŠδNT΄– ΣΉ{€v,°zۚSημ“Ψ©΅Βδ5χιwΊŽŸΣ¬αΫ*ώ7δφ°κ­9¨Ψ,€o=„B‡@@ ΰ@@  ψn―aa¨Κ¦§ΖΝ-­¨λ»lζoT‚u”όϋ\›χμ»%£§EΖ¨Ϋ•ΪΎσ"Ε¦΄{Z΅Ί₯ J!Ož*™w‘X₯ˆζ)ΥσW­θ‚£ΠΈόzξ»ό62°eμξ©^t3₯}ϊe Ξ‚”ξϋ’<€„$JΫ*(_βΒ(t`K·pΠŒΘ§SΗ-κCύVn‰n‘šŒ+xψ©ί‚rΞ;Lr‘= +υsQŒ@ΐzΐQŒβͺ€υ‰eΠ“μ»|Σφ³w\ήVΌζ˜aΒΰ¦UAλ^£•ZυQL¬Σ<‡Θ,»‹φˆ@XKΥ}ψΓζTΖ—ž=zο0€Rœ?£ΐΥ'ΎΕ1Œ—e`lβV, znUζC x©δξmβ1"Y °‘m$ΔO―ΏVW㏀Zρ€&|›žx;K«ύ +υš·/Dι@9pΠ‘TXgΘ”Δ@"»@@-’2”Tω3?qΐ½'εσ/}ύ΅9ΣωΚ‡| φn>A°-ζΥθ}τ!§ω&fΉZΊRB{qΆE ¦­*ώ<‘€ίˆGίA`DΛ'dηΐ7‚σ1³5ί³@Ν’΅ήώ,€³ζœ°r—SŒ=TU±Ύ†Θ'py`ž© ±1Ξ―#ŠΤE―nψϊί`Ώωϝ0oΣ +‡l>aMžtξgmώβΗΞ#'Ίi•s@ +g€Xΐ›mn¦«%ΛΕv°ΛsΐF@ΥSν ν7(Β}ΰƒ5G>ƒωRΝΒy2Ό"7~mψIΆεnGtΧPχjv{²μO +KcΞ™gΰΉ˜Hήό4Q4_(…r6ΈM ΐβκͺ§η_oΘGMyfρΏeΕ/œ*Ÿ±ςΫVΦΒΝώΔCO +ζ7 +”‚‰ο9—ΘέknΏQ#΅U` +(˜!AΛT€uΐχ;"-Φci•Ξ;m;ΉλΗσ½¦ ίΪaQKΎϋ½έœ΅Z²"–ρΎίQύ£œρΒ:Ψ8Ό"U  ,πνΣc³J„_W¬nΰƒCŸψΖΩOT ,»x­χσŸ„g*δˆ5Ό•T½Ώι9O2*ΫΝEi@C€›n*―όƒΗ<±ΰδΟ‚iνL#Ώω_ξβ-ξ>JθaΒ­_εNΉύ +‘₯Ε!¨…€F‚•Ιz>›ΟύfΟΤ'κf@`ω…k£Ξ]λϋΖΩπ¬ +τ8/—g#\{H‚)Š85@˜u•ΙΏpΛ‰½—nιω¦΅sπ€)`δΧŎ€›Ξ]n§ΓΎRχ²ϊ‚lSξ›‹ϋφ@\¬iiβΠτ;Ryΰc;§;Q½Ζ,ΌpS~•OœP@5»Kekeυ΄Š’·΅BžΩχ΅(ƒόw!“%·ή{Ί­—Ίi »½£Ÿϋ1·ˆ‡@`φy=γ7τγΏ&τ¨•ΫεΰΨ‘t•…ΎdΎ+ΒAΣ‹C €O“ΖŒMžπΫ Αlύ!+e4Μ)ώ£›wβ'‡ :QI£μ½z}ΪwN£ΞώΡ~ήΉΖ˜d~΄%Ώγ‹ŽH[ŽΒOνa)₯‡I Cύξ_φΡ_²ŠHt v_Ή>ρ»?AψθqδU―Πp“νΞr‚ ™{ Yž#ς&ΘJ[κίφa~XJ0β₯‘ώ³Βš§l=󋝨ͺ)v\Ύ6ξΫ›_ηυ?v>,½}ύΆ&ŸλΝ ehυ9$An)€@€¦92ȈήΩ/ΓQ0p”)JιŽ…ΐ–ΛΧΗΤ›†ωŸζύΊ€ι,ˆ ‰ύάΫGΒJ]Έ‰6Έ"0‰€_€¬`Ž™ƒM–|ϋ„\χΑχŽEt’Ά¦X{ι–‚ ؈΅ΎcΖν Nό ž¦δXL—$ ρ‘"»@ΐ΅€{F9:pν¨! Ί«·™β‘ξp,8Ϋ€Y oωNœΰΒp3.GύLΨ»6™‰·'#εξξNsoSυaxv§aγŽεD…M!°οΚυ©uκ@7τB1t½λ)­—ŒΈ½pΧχ0gŸΔά!ŒΒ†‘Θ"Π#™Ξ?ΦΊmι•xtθ;Τ™(’Ž˜ο«ŽŸ™ϋΜ±ιΏ4vΓΚώΖx|ϊ#/Γυ²7?[sκΆ³χ]ώC‘j;n!;―\―Ώ ώΗ―Ηύ―ΙrΤͺ†“±’龁#r4±‹μΗGμΝνΫΰΕ…~t›8GqΫ}ρχIχ=ΥeΤδ˜ΤΦ:wz₯ψοΠyxF·l•Υ³w·IsΛ0)ώ‚v^ΰ†Ϋ7 S@Εcοzrηj$ψyζώ,ΝΞί’βN€‹ Πaηƒχ­ˆV| M>όŒ‚lΆζδYΠΎoP_eΤΈΛΣΧ―Uη²κ«ρ ²ηβ)ψ²φ\Τ’‹ 70 T?σ‘WH'Θ°Fm/ηpXŠlΓω\q›@ΐˆiΗ;ΈŽHΙe|!h£R”xηΗ—ςϊ sΣΩ΅νΏ—* I”+D+›ρύ-‹Γ'ώ­yρ3~λ0ο©j7g/­ΨΞ!œδλΥβ.€ƒ!‹Λͺ=œ#«έά/;ξξGYΔZ·Λ; »ΦΏ1("Ίt܌ů~€ΘλΫg!Ψ{s ΈŽ _ό‚ °T ›|_2:ͺυ{ˆx’@ΐz@SšoLΑ₯τEΉŽ²‰³εΧφoώ–]^Γυ|»Έ©ea1vεγ`Ÿ%¬ϊΑΘ'¦€ΚǏ»ϋϊs6œŠπυU( )'θβ6ηE€[ςλώΡ|Έ t»|U™Ÿ^‰mΙW»Ί+ΆUΖΘνχ:₯Ph¦1)¦€ς‡ίΦyϋrΆχF|Jθ +^ΜYuq›@ΐž@TǞΌŽVšεσ½I«.έδ‹ύ!πOβ«€}ήm₯YOΌaŸ+yξZA +4ƘS@ΩέΟp: ΥyI=6q~ ’‘8ι8Η―—έψΜv;Ξύƒ›O 7nϋϊ―Mӝ― 5MnΝ Lm2ρ±«ΫW›a +(\³Ÿ³ƒ$8yγΑ" n%Δ!` π "xhΗ‡ύˆhž²ω‹e²ΣΎ+Χ2»χ’?άaξ€šPίε›œΖp`ίΥ릀@˜ڌ™ΝΩ0©½xψSF—ΕΒ/'ζβ6ηA@#u^Δ3‚ €Υύ­ΑNV&ωγvΈ ’?άρξHlŸΏώΓσςᲇ ]Ο;(hΦομ‰Uƒxšn¦:/διΐ˜β +yž(ξ8 z·*‡Ίu™Σzs`WŸSx^>­θ,ͺ8*ΰˆ·@©uά]ΘΝJXz#^ό-Ϊ―7 βίαYxριψ„Β³΄ΨζA\άγ`­r'Γ5¨›–‚ΎΛ6*ΒBν{Ισ*ΟQu›ή‚±ΣαΡBτlX"ENhδn:pό’wX$XΙΧ­LH'π)$€k"ΐηP {gM9Λο?\ζYρΞ77Ž +8ϊ-ΙΉ…N+mέFœBΤMε½ι¦γ²%ΧYσ2אeLWCΐ3@‚κ>ǐiYΙuv"ό_8ˆί֘£ζvu άΚm8}QmXΘ4ζ†Y wρΜ}›H½οαιΟνΗςϋG`B +΅‘υ!ηψΌƒvœC~–ΈA ΰΈΜΰY)q})w2FAR*0’„Ί—·χξϋQ–φ—φύoΧ=ύB»ΌRέδd†νΌgίUX+΅ Α¦tAo(U=q’g# 0–kasD +M–Σβ^€Γ ½}Ιb»x’/Q)i»Ύ·‚ά$9;ΞXωοyπ±Ζt­R +f­‡6a―‘œœŽ>,Ί`A +ΤvΦj|²†ρtο’ω<ΟχΎΕσbκ‹Β?’"*HώpJg―FοACT’z3ŞΎΦ*+.Ω&Ήz⁀ @ lŽ8Ϊ]Wλ…«Δ£ητE +’?ŠZόΚ‡€*Όφρ—fˆZ½KgώϊŸ’ž€ͺrgvθ)`‘₯O€~oηαOιTΖΓzβ85ωΣx†FDk*(PψTά^ ΆΓμΥhžB=†·Xς7½  ω‹‰ύνκηtά)@ο’ž pύ-ΰΊσόU{κΏ,ΣΉΖM‚s*žO€–Lε‹LGD»c‹XΦ­»»a­Ζξ˜½"Ε=Κ-²΄ͺ>ύΛ_›%v+Ψ_­qNǝXϊuφΘφΏ²…”ˆ6<ό_Θλ†ΞBmΔe€ ΐ2Ϊяξ“η)+ω1”6rΗ!φŠΨDψί`ByꝓZ-£ φΧ¬Λι SΛ'@ŸWΎzzΥ½)λ Eπa k$qœΨΉsDMΝOE"8:vΗ·T…·ίΛ^™šΑC°±Mώ:v{eζtΠ)ΐΌ-€AΤvΖJ28z=·Γδ―z‡'WLά °>A^ΰκΞ‰Η„ƒO¨Aώ(αΩλb'όώω+°A`―Ά―Ÿ_@ }Ησζq +Ψ~εΪ–vFœω=89νζ[2_8“Μˆ…!<1,2:πsΫky,΄₯[ +DuT‰ό”ρΡ1肇NχΪ±MΒΒΈϋ•#N3Μz2|τ8ό +>m· HδΚ‰ΦB ©”LώωXΘέΣkΝΙ³‚šŽΎuœΤΜΗ>8ύαg’’£IwΥΟμpS€y§ uΊ@ΝJθΎσ¦’{>’ ˆΈυϋ“8wh Ίί}#yΠ½=”Mœ­ω;ξϊvΑα‘Qμ=hο{uνϋψΜΩθώ΅ž>Ύ³Ύ©js(Yψ•λγΝΊƒ0L΅―žqs'†…―rθ0PuA:³·—Θ)°k"3ΘύΏφ>ɟ@Yx}Ÿΐΰ-_ύ¬$'\½ή 4G”Ύ*ϋΦ̚·όίι―Ύq―€%Aoμ΄=“<Φ8 ήKιΫΝδj‰VA€γš’Δ£vι θZρ?—g/'Έ²=n‚]κ+`Η•λwρ0δτ―ž!΄ΎΩ€yύƒοπΦ΄Š‰ά΅€Σ6¨ρPΏ‰:‘±ρ»/ό¦8α7(Πqωηα‡ΨΫΉvΐΐ:ώw©―€™ »ΐ˜ΪΝ^ΓζŸ9K–“‡@»Ρ€§΄Κμ°jΧΛ·?2oέό&-Ψ‚ί¨i+‡O^:pΜά>Γ¦VžΨ­χ°UCς:—·ΞΏKL ‹τππ$=EdΠHιAξωtG(`ζ\­ΖŸŽΛπΝήje=zΦηΧω +XΙ‚GPΓ.ΐΠOξΎ§>gΣς(@41Θf>|ύŸxχκ3οJύ=ϊφŸύtϋo­ήσ$&Ž σ7 °¨jΰψόβΚ”Φmƒ›D|2ΧWdt ±‹ΊψΗ`‘aρ‰{ψ―„ί LΗε‡_~ƒΡΒNE ψίEΎφ]½>Ža³@Ξό;ΩρΤη„G ›Ι!žζwbΙ–©δΟ’θρ«w=ωΡΪ}ΟΜXΎgΘ„…έk†·+(mΦΌ₯—/ ‘ΫΥM"χωŠΤΓ6ίΥ€¨UϊΣ•ω_‘―€YOΌ‘RΣ(Uμ|Ά]ΰαŸΛ'œ¨›Lw +Z΄€DΕεXψ\Α<ψpXΉσρ1³Φφ¨Ρ¦mA`°ˆeOj1gϜ=’Μ©½H „Δ4SάΟ§)2qqώw…)ΰN6>rm&uTΙέGͺ9@P₯ΨC„΅Ί‚τΞQΤ―ž]Χs“n­41»Cq`0Ω~Ÿ†ͺΘm·h΄RΥZ‡ΗN±Νωΐΐ5;LΡ΅β邝 +ΈΒ*BŒ`Ο βrΎ}¨²ΠΤ*ψ^Ίυ!Vυ–{žώxήΊƒ5C§΄i[θγKαGz}‘ΩŽΰpΫa2©ώa»ΞGqž7U ΰπΏΣO,ξΰ Αιγζ’Ί«Τ$…ΜΨ>£₯•ƒT%s™…?}κηݏž˜Ύ|wyΏΡΨ\vχ ΣQ ymŒΨ¨«XŠQŽŠ™KLq΅ι‚ όοάSΐfΠΐ“—Ι~‘9Τ! Α‘αOΏχ‹L–ΆΪνOžόqΛαΧ‘‘š‘S€£VSˆBδ΅6n:rΤΚ]DFΜ‚Όl8}Q ž7U¦ΰ:ώwβ)`οΥλc-Ή5¬ρorŸαΜφFFuθ΄j#ΆάχšΥ\Α=ςΦ―°kΠ<₯΅Ζ͍τΚ"³έ!Eχω>τνͺϊ™"j•Χη'ž枿VΗπζOͺŸ=M괇9dWšΉΩ ±σ€e›u+ί`ˊ¦°> Α+rΫ Βbδ£ΉΟS‰ηM+ψΏ;λ°ŽY„Ω!<3—6ζδ‘*υ‹dδκ6!m5Šύ‚;Όˆ½γθX+E2eΗYδ4€F‚0‡ΤΙ‰―b[e˜biυ7ζ§œv_½>ŠΝ ό_ΈξnΣΑΨ•ΈBΪΠΐ8jQn¬ γižyω5ΨΨΆeξzψέΑγ$ΆL7ώΪ"Υ~ΠGΎ;BϋQz8^΄ͺ­κρΌ©’ε§œ¦²ωCΤέ/€0ψά½₯šƒ΄ΡΡy‘|IšΉb―mΉZΥ§C‘tΜΜ5I-iΊ"$EfY€υ¦uoLώ„/\μόnϊό/¦XZ½tΑ¦ψίω¦€%™|vZC/Σi€Ο!½ωσaηZ JΝ;|~Š„Ρ1sΏ°VFͺ·Γ²u€ševο₯Ι›)YπΏώw²)`#e  κθ)R–šεΡψ $ŠS¬ˆθ8₯8ΦώΛyκΤOp|šW\₯Υκh­ r«4¨ZՐ*’^„w3δK‚ΝσΏ3MπΗΐ°ώΗΏ Ι„> ǞT_νiŸ’Ψ?u+[Γυ|R6taHT<‘!DVΕh–O^ΫPΌύϋ…4ΩsρΜ΅JW[δgš¦~Ηͺ +ώϜDΡK‰Z@•;αE”}°N_ΆKYv΅Žžόeυ“—Vώ—?4Mm³υλ_λΒj'k)V`˜Šw?Jό₯«hό€ͺΜ cνŸΥ¨αΖg―εό‰Λ;ίcΤr/‚a53φ.™‘νΒaG¦|Ζb« |£r\ώμ΅·Ω{dN‡f=}|w_ψMΑ‘ΝQ”γς;_}k’͌$Cuπ«ο.Καy3χΚ™ Ή±ΰ…S 'η–‰ίΡ>2'/6‚©©$ο`ڐΑψBaζcΨΔΕvΎVW£z{_₯ρΏaR˜²ύΘx‚8šΉ\&#uσ7ž°RjΥΉLΞ(Vδ^ΗεsϊŸΞ —XΈl…—yIΞΠ$ωΆ―ͺHk2BUκvπyΪ€/ίJ›π•Ν|tμΪ[ ‚΅σ2χΥ³6Ίΰ7•ΈδαsYōζvpŒZwςζ/Şέʑލ’ƒγςωί_b‹–μ½Π? @½OLr¦€œήΆŽJ‰ ˆ*@ΨΠh a¬%Xu©φΥ—˜jη\­FυvΏL7ž ͺΖ―Χ’6qΨΗ•ηDτFR7ή/iώ¬lθφ‘ŽXšk‡"•·IΟΈπγ―2—ϊfn—3LδεΊFQϋdωE‚8ƒ"hH*%4IjmΰtYΒΞ"ξp¬ΗΪs™;(ϋΏω)#V<μεK‰ιΓή$Κ“jωΫ… &…Dzσ?ͺ=-–οΠόΏεžϋ¨]/‘yβKoΎc†Γe^ΒE­ςG&΅ά}ρw‹ν₯Hͺ +(¦€΄‘ /•Eγ,œ(Ž@ο:ϊ‘=s΅uΫψœFyήLβδm―„‚RZά)³Rc~΅%h2‡Η')2–ebeώ?ϋ;yξβkωΕO€Gζοτ埱δθzE]Š7mίωτK―Ύσή‡2'NϊΕΉσ?όό·Ί‰γΤ'Ÿ‡„pΤͺfΡZ™­Ιxϋf’ ψΏhΣ!Βω4‘ρ?Ύ²q σ±tλCjp¬ύ–ωή/«žόΣ¨zgΉΥΠΐ&ƒkζ6qƌΉiέώo™+‹|M1ƒΥψκš΅ύ|όό ασ'£]Ξ†½χ|ϋο?δΜέͺͺ™!W=cxDD—’ΙΣgŽOY-ί¬—§―ίϊ.˜j)Σw\&Λz?χρΝj²ό―‘°€'ΙN#Ϋ°”kΘ3jΪJϋεκχUΌn™…ΫσΜΨ{< ”η•½œ$'”ωI}˜bωk΅Εžyή°Ϋυι‹–ΉΉwZž›χήχ—Ή§€ϋŸΥI:ۍװN$˜€ϊμŽ8σ»›;εS "}Ψ‘¨υ¬₯8ΗΪsχΌώ##·3f›Όύu±`‰74δxvπΝ|LΊο)σΜl«VΰIs˜G₯yJ‹OςWξ) m‡|σε;ΠU­»ϋΊΎ·Bӏω–ό ’°D—ƒΔ™CΨ›©]A©=Σ΅βuΫτ“σFς7dΎμA7\ΨLŸΣ»ύ?BψUν1SXγKkN³Β0·ψ΅ωΡWί‚¨§ρλ7Hι?b7ΓΛ#<Ρn,;έb«ΙΟ0ρ[ς'@bΥ h)έ c­p&{α.₯ϊψ‰_V*$όo0A” œΓŽΉΛε N uΰbB€ oŸ}—?Šε— 6ta +π€ΥjωŽ{ +¨<Τiϊ'¬Β­ F +l΅›³–2B ‘–O±Η|xϋψ)ΎΖΆΫχqYώ6 z£"¬pστBfΤ],cL[Zξ0™ ΈŒΆς©[‘Tεθη€ΨaYrηnώ‡ψ(6>ρAφŸΝ +†Σ)ΰ ό_Ίο(Ί 8Ϊπ©9HR}ΰΥ³vΛΨ +V žŸΧ>₯€ζ§QώG"φ‚EI㽚Μ:}€ρrŒ₯Ϊ‰ςfUωρ7Ž{{γiύGάΑΝΈριc§ΌΌ½νh©Q{/ύO‘ωέT!³Ώ'Λ«Ÿ=Mέ‡ΖψXπτg/Ηƒo+H³v[ΤέJοό6žŠΞf‡έ…r‚ΟI°I]ΩΑι1eΎ©iεtUωžΗŸfΗ€¬²—ώΗ½xœV§c’=ηTΫxξy2ύδo4Δzν£  fμε―άωΈέ’ΆR;zςΧΥj.ώ sΑβΏρ ΰ1Wao,‡ΜIυaΕώšƒΧοΆ2Ο›zœͺόχcO±c΅’J&γφmχήOr +Η^=+ηΜλ7ΜT“)’Nudy…p ͺOG\LΜ\±W)š΅ΫrvΏ"ΛηOγ₯Ύ©ρ `€W- ^βŒb"Ι<hΔΙψSΐ}ΟΌθ@PΔ5ΡD6NφςσίυύΏ‘z£…,$Ί€3π“6m ΈδO§ „Ξμ…;½ ΨΗ”΄ω2Εό†τ9>rscέ§co#ΗΞΩu ­χR€—ΛήψΤ訴~’σρ?¦€·Ύ8Ϋ&‹ΒTvΩSΗμ{P½ώ°ˆ‹zΤ jwm₯LΉk†NΆΫu»όŠ=uκ—υΟ¨ΈνΫx:HhGhYWΘZΎ{KΒΊύ›Ώ©7΄I%;%c +@t€₯·ϊϊΆν­SηφLjJRfjxΓϊ?mAΙMjӏ2‚ŽH™ƒΩ› ΄r|š΅ΫvΌd%ΙOέDP:x.;ψ.‘³ζ‘χb«‹ωΠyx’†ͺͺ™•1ΰΟ£§Ξπρυen;ΚΥT½¦_z‘l…) {ΑΘEjΡ“0‚ k‘3ŽύΌ.vΛή2+vΧk<‘Ύκ˜œοζΐμΰ;ND~!)τΨ̎IPD΄zγšZ²sσΏaψδκ―kvξΝ)μδpΪA«N|MmPΖόKΈδ?yΛw²χs)Ύ#mQL€3rŠd¬}ή~ψ­Ÿψ\ζ]3φž ΄¬Σg₯Ζ0-YΑItΛVŒƒΤ +Ω\ ³ώ…ΣιGŸ»rmίa#:–v…γ lΘό₯₯g6Kh’ iθ¦ύ*υ>ω—m°χs q€ETρRφΒ“Σ²μ“ΐεΤ +{Ύ«Υqυ`qvXxWμΰ;NhZΧνHπ€‘ά‘£Jƒš£X—βΊ‰@““ίώ°οα'†Œ$L©’[3ˆ£MYnαΣι~ίΛ„W M’ ’nλΩ i–(‡iνπή‡lGώ˜`ΐΎση„) +iιcζ#£[Λ΅NΑŠΟηώωίχ=Ψ’Ε[¦‰Ξ£^ ω\ςŸκg>4QScΙ1΄A„¨ρΜGp“;δpξ*ιΙ_}S/3_³ο~Ÿ{Θ’HλΊ”­«}‡Z‡ΫYž"ψ_qώ7ψέ­ή±' 0HΞhqΣιT + +9ο<Οώoν«g―γAD•»Ψ χζ&[{»ρπΫ?ΩJμS7#ŒZυ;ψΟ3¬%­λΆŎIΗΑ£Y˜Ω:y«Δ†bίωκΫτΆνΩϋFγœ+ήωBž0‡ξϊ?ύήώq M¦ψ„Qυ]&‹jtΑΣΛΫήhœ―>ί΄Ν†oσNzŒ’hv5jgK@4:’ό'k8;GNTcDσ•)ψ_UώGα_ύϊοβξ=Ω»Gƒœ=ΙΧ²ζοšEχώxςrƒκ™ϋ“A£Ο½ζJ»ύμUωψΦ~ξzϊύ_Υσνά€ή-ώ™Άδv€]ϋ―θlgPLW¬ΰΓόπ―»*ψ_mώGωΨθTZΖ7’j—ήYΧX +žL£ϋώπgΒ[xψV\γAB>τΙ“?Ϊ™SkrτΤ/›Ÿ·Ά‘—©Y`Ρύgt€ΰžFΫΓ™›Ά§uέ6ύΩίΎΫ€Ή +d™E ώ·γP=E˜KφNR—³hψx™MlτφIίρΘ‡}φΟΊŠY>ΡzΠψŸβ…ζρc—©¬k'ω|ηηuO[Υ½ƒ)ζ7€—YmΉ5]*5ψKk‚_”žΣ’6IόoώΗS^xο4‡oφ½ϊ«Ρ1ΖΡγbύ?ςλh‹y’•Κο^±>'Tγ½_ξzνG•β9š'ySW—?~!4Κyb'ϊ§™¬TΣ•Φ}ΝΦΰR© ΤΡ|e +ώ·γAγfμD έ&½k9_˚ΉkίΥλ£ΈψΔWΏ5θΜζώtΣ‘ωŸβDλθρ«β}W›g~ψψΟŸU>’»)bgL―O0»0ΧάΞt-*ƒΦuΣ²Ώ}Ω„YfΖ¦•/ ώ·&ώγίƒ‚Ω» +r¦δ)ή%φ€Ο‘γΏΰ–‘ŸώPymιε'ŠOžψ‹Ν)±OΏχλeΏΙ| ³Μ~}›ΪΤE²’υρΗ»Œš¬ψˆζ.PπΏ5ωϚ8g>i Ε₯gs7©·_α!ύώο?*οα§.;ΘώοοόdegΌ+dΛμB\šήΡ³F΄’uέΆ#ΩίΈΣΠ±¦¦υΣ[™ίϊόFΓή["[(ή+ξΌΔΉώpό"{Ν%― Ϊ ͺ=L +τ{Ώ0.Ώm•ν±ΏμxΡ^”|O +ζ .νξYΓRi]·ύφ—Ξο?BρΝ] ΰ+σ?—Ϊ†θ046ž»qMέΈκΞυΏ·Ξ±χsΙ· mυΎ‡½pw[±:Λs‘ήΉχΥΏ¬zξ>u³ΐ”νoxϊψ±ξZ9ƒh]7w;>9½š˜ΦOόo}ώ5y{oQƒω‚Ώ@ώΣη•/Ωk.ωGΡQΥφΒύόƒXxΨϊyž:υλ]―ΕΆΞ|κHήΤΙόCŸ†DΕ³£νr9ύ#i]7Ÿ0’Ϋ”φ΄>Ο›z’ΰλσΖύΩ”όΟηόό_ωΕK|`,mQόΏ…„EZŸΫΝ?›ΌίψqνS—L±€ΓΪ+&1½ΊbNͺι:‚Ε3 Ω9¦ΨΨϊι‚ΩωΛ_ώuκ»Kˆ,Ι~‹Ρœ½φ6sg‘Τΰ©\Ξΐew?Γ^s‰*DνΎ½πΘ¦ρζΩؚWŸΊΑόveejYόΰΧρ­:°γμ’9už΄₯KΩZv ΤsκΛ1}ώ7JΡυDςŽ)Σ£c›šΨΝΝ­]~αϊ=wsO/τ{oQƒωŒΏΐξ$|ΉHMshƒ¨d9;,qIiΦdxSΟ:zςψπ±siOέ\°ΰπη±)Ωμ »pNT{‘χVμ`ΗΚ' ˆƒ¨UΊEπ}ͺo|ΎσπC¦Β·ΞΜ>ώΝωΖ·XLSPφή’8οΎΒ©όώΟY°‘½ζRσbΒ‚ς?%ˆR«Μ¦8Ω:ι0ζΪύςU›ϋmγv‹'³ξ:ތΗ ‘ŝ)+BΊ³[―S\BύoΟΕS‰Ο©Ε +ώ7CΧΫ=`^W\Έj¦£—Ž!xQVœ7ρ*‚3&PŒR{FΖE‰"·¨‡uxΎΑS δΏο­Ÿ6=gwfΌζωΜϊ§ύ‚„e~κ~'­χΒΪ‘ωΨψι*Q«”_πΏQŠF≳ΌΌ½-6iqrS%˜J·-―ΈΘ©ό ώo9€ η,e ’ ,‚eiε Μ¬φŸžψ’ž΅6ežδM]­·Vηξi±'‹ ·!Πy­χBΫ™ωXψϋ*ρ9΅XΑ¦X‘|ΫσΙ·O˜*ΔhΊmω>Wδ/?~M;χ`ΔDŸ-g4ωΌργFG[πζθyΆlί•U‘σιΛ.AN„Œd>Ζέυ•¨UΚ/ψί(E#zchX8c{Ž™>Λh!¦mΛάΚ?ΰ €TFLτΩ:Ξ₯ ΔRφΒ‡LX¨*Crž­/\]uΤήυ9M-ϋ‡-}ΐ?˜΅³#ο*93‡Πzo,A«ͺΟ’ *ρ9΅XΑFYzžμύΌciW£…˜J΄-ζςό©_ŸύCηνΓ‹D• "θσ1eρ65ψšœGήωy6vν^ίν#}ι#ίεWŽ1ΏuŌ΄«flYIγ–μHΩOHΑFYϊ₯>aoΝVYF 1•hCώίv™_ωgΰ œ¨χδSs6‚‚γΩ1_Άνaω‰“ΏάϋζΫ^Ό²κIG]νΧMΓ—?›ΜŽ€ΘiψŽ΄ή›Mpg?&ΐ‚²4b΅οΖRαΟΗh!¦mΘ«dlώV<φ±·7‘ζL> Βθ’ωΨv[ςωΡγ?ίσϊ›ŸΏbWYꘜz2}χ±ν24f°]2#Υz‘’½•œJΤ¨”_πΏQ–vVώŸ{ž_ω§h όs2‘Ι4ώ‡5Ε-κ‘—ΏζγGrπ͟vΌtΥ5yLΝK:Ϋ₯ ‘δΓά;2z‡Π:0ΕzέM§Ϋuώ?*Q:©XΑ.ΕΈΒώ”ΗΟc67³4Λ§ Ÿž[nήiω­ΞύιS?³ση;„‹S$o4}ωcη{OήΨ$Ϊ2j" ¬FHLd†Μ“ωXόΚ‡$’V)³ΰΧα]2,14+&μpITγ―N„Ι%2&Ξ"ωC]ή7τλ|ηγ|ΓD°μ±σΥ“6Gόι–„™xDFfΊ­§­a| ΪV#ΆT‰IΕ +ώwώ_χζ/ψί/šB50ζ%©Ogg–RFNQCώοW,ς½υΣΎW~άςό•5vμxίθ2ž”ζο5ρΞΰˆXvΔ,ζΤyX6u΄Xˆ³e(˜IλΓ1mΩθ:~&‰¨UΚ,ψίuψώ~ώrϊW’|^*[G;)έΩΗN·ήΓΐPځ&βκn{αŠ3 σΝ̐φ \{PXSv¬Xrϊ5iV2ϋI–œ•'s0­S S;•ͺDι€b»Λώ—?τ&aμ»ι$μη’ΦQYμ嗏XθΠϊωfήΜ₯qw>Φ4‰%Ɯ-;VyΏχfJdΖ’=[σ.΄>œ7…ύύCΓφ]ΉFβj52; ŸΎόσζ»MžΏhΔΔ)σV­ƒO†ο»n”ΫYOgΫe~Ν:,έΞή·%jδΜˆΈΔ|τŸ½Χ O:ε₯Q«χπ’ί±€©Ρ΄( ζ―Ωϊ΅ΰ#€…&ΡψΏ;Ε;$­:~F J'•ιόεΟDDEΟ†ZΆNπΕΧYΨΎqηγε24Α εύŒ SIq΄Ss@Έ™*¬qϊ€­―8%Ι›z)Έρρ +kŒƒœ—_ξπm`~ΓOπΏ0α³οaBO=,!p σ1|λW«‘ΩΡωξ—Ν„SΧj΅+·νjLοSœ§σΖόωγηEΩmL@5Xό—¬`4’›V¦¨)Σ;ΦLbΗ‡%gxJ~χ₯oΦ‘ΏX›­ηfZOΖ'σQ8θ5(T¦Cσ?"p΅νoo„λ:pτY‹„ί ƒ“ρž+ΧGί q™Sνχ6!`Ύ9:=Ώ΅mΎλ_m“θ”$oκ₯V<ρC@A8V«ΖηΠσΙ¬]Z³εL}ςόί¨?Sς§Σψ?…ΰ Χ¬€šΧμάk²ακ]hΝίoΐπζt2ώ_+#ζ &‹’ΝχΥƒΣ)$9Υϋi£&ΉΜR‘·§ζv7E•N™>cο‰[//ο¬Iσvέ½€ω  +ωqh[υ‘υδόiΖΛ1– +}›Ώψ‘΄\W<³CσJZkcΈIΫϋΠγζ ΏΑU'γY2ά>€[gSSIAq΄!ωOXKS…5N/ͺβ” IόΜ~+ΰ‚£Ht­xfΗ姏2Χp·_λ\Φέ<α7ΈκLόΏVžΩoί7Οݎ₯₯Ώr'†LΩZK%ήv}ΖήγNΙσ¦^ +ΦΎή~Ο¨υΑ +KΚ-™σ”yζό_1#ηΤ-`8Žf>#’lkΰΈόπΛo0Γ,εv,jΐπζt&ώŸ)Oσ'oωNvœυ9Λ·ΡψΏν(φςΑ„Ψ5E•N™ΎώιΛmK²CdΘιΫaδNζόo[ͺ'JΖ£—ΌvZρU={‚NNΓ{^ΏC†ζ„?4·o>‘4ςΗχrB‘…XοrστB;$yΈe€IBυč½ΖeχΝ―Σo֞Eœ‘SUx΄Xσδ₯‰χ~~ξΆψU­»W=̝ϊGgΦ.«ήψ;ω#§Ψ5‰iRWZ—ξΉΥdQΖ.Τ,ZΛN׊ηόοάόΏςYfΏ#ΎϊΝέΗΟX·5‘2§ΚKbL”e$Ή°zΌRUκήO\„XΎοŒ]³—Ϊή¨e·_PχK©_+«ŸΌ΄ώ™Λ³ο?_»ν›ϊήnΠz‹ώ—°ζΟξΏͺzΣηυod<όo€·’‚θ]šΰŽ;9·PqVg/PπΏsσ”οdρ·C/šF/ΐ +‰«v“Ό¦˜{—RN*gαύ_έ±ζhΕΨ5νΚΗ¦d{xω}ϋΖ‰iy=ρu`ρY }ψ©^ψπΕ;Ϛbμœa›užΖŸλΧvΠzpΈ©{-¦ ώoάv¦h΄Rυ]΄^Mqf¨qsΫpϊ";c+›SπΏσfΡ~!ωΑ/₯A8/ιG +QσΏΓd“γΞΨ…ΉO[δR™°bŸΉδ°₯τ½2§ϋ0HœB£ŒΥ…5 β SU‚?¬φϋξΈm΅oŠ+ΦΌΧͺ|†_XΌαΩ0ζŠLλŒyAσž%ψί\sΜ ρ?%˜žΫΥVeY½4ΑNΜσΟσ;|ω8σ»gP¨ΉqΡΰZ“΄a‚/…Δβe˜ω3$2Ξ‘ς₯/8όΕΔ-/œw75Ή=G€dΓΑ¦βQέά΄“·Ώ~«†π<}ζ§έχ}νMχ;¦ίTz―ŸVϋΠΤUŽtΑf:žϋDW-œίRTšΫη³3Ά²9;+CνΜ·²„?eχ―HQd.š_Ώη>o]1K)’V©Α–{`GbT X hέ-{3‡§―ίφoώ¦ΰڞ₯(ΑΞΗ³εy{Ζβ?sβΒ›½’νΤ^dώ§ΨΘ°UΒNsΑχrεΪχUβm₯ŠόoΉχ$–;9άGSŽ!χ±Ά‚y;o“mσ5ςλ’=’‡—­# _‚›\Κ²³Ό Ι›R|N-GπΏε~γLΦp€9<εˆm•‘ ·³%ψίΙψ–c/ΌMV[£…˜JήεηT•Ÿ9ώώΝΧ²ό°qM¦Ρτ·>Ζr‘7sTd΄S‰§/ύtσVΛwοiͺ£ικρΏLW Κ'ˆΡf΅R―}΄A€_ψ”vΊΓΓ78’eΗ–eσFο­\cοs„ΰ¦ώˆ~[{˜ΦΫ{l&y8D5¬@UώGΤu_&`%iλΓFΩTβwΉΙXψŠ-;L•c*=>)™±πYKWš*ΔhΊJό/_νόWVΝψΦf‹+€ 'όᇧδ%w™3lK·Ε―5`TΗϊSπ?k·/Z@ξπa©¬…ίΘ—ί„ό…=c ͺς?Ψ―vθp–wχότ/5Κ–fGNšΚRΈ—·χ{ί_6SŽΡKΣ-e)\«ΣαKΔh ¦Uβi²%}ί8«ΡjYήϊVž.‹ΙÁπεΦƒl}a~@dRLfχΤn“±ΒGLΗbxσ΅όΟΪΏ8"\ "*εΠyxθ#ΛΜ¦6ƒλ|ό,YΈφNSli&ύƒ Wƒ‚C,B;yήB3…˜Ίτω΄μŒ*ΆΒγΤΰ™A~±ςΗ―ε€1ΑΌ-ƒ4Y#f_0₯±ϋΓέ'F[ yύ ΊŸ;|$ω`Hσjύ«2My τΎΞΣι΅α9όΟΪ%Ρ{kΦ<θσ^W*’ΤiΨ8™ΔΞx»ΪόΊΫΘQ,’ΝΐΫ³¦φϋί›βaσιχ?ͺ‡§§™Β;•–ϋΧΜbκκΡ·ŽϋψΈaxb»Ό‚―φ›©ΫM₯+Ξ{―^Ÿ Ϋΰ6_nξf4r)k(m @ψ“?ΝH9ΆKςςo—Ρ4³GJρ™΅KσΗμΥΛδR9<ΜxoρΜ'ΰω_γvλcMλαΥ¬]U·E―0–Π ›ΰB€9B·oέ—PΎ»1χ5'Ο2rΈœlVΰΠΰ/ΌΣ΄1:wχρ³ζ~ϋο?LQ%K:XΊYBσΖ…k΅Ϊaγ'ύΗ±b*Οσ§>JlΡ²qαpΩΪw؈3ύ©Ν€+ΞK/ΘςσlXό'χa’Τέ‚ΒΓOκ}7™‰1·ΗuΗk>!1Α±­Α–ρΉ}°9›Q³€Γ¨ES‚Δ^τΠ Cό™^=Ώ>σΧarGμΰx Αυa΄pΩ†άν‘#A‰€ + )‡Ψ A•kvξ-κΪ \ ΉJ›μv`ώ7>=c†'Ω/δ7μ½§ΈGy\b6…[gf™6σε?e/ΑLN|>`oΊ¬²WBrJhXxZzζπ “Ÿ=ρ™[Μ_R–wΚφσώ―}νkΘ·-tϋ—9l~Λ·I‚[ °Φηk‹dοA±­πkΌ]xJ~d«ΞX΄C>“T4’eΧρ θ9b ζέ±"‘σŸ/_yΌzΣLhη·΄ΈΆA;4ψ Œ½‹ϊ‚ΐhξOFκΉ™<$•™+³Ρ57­vε±/iœ;›Υψί<%ΊΤUeωŽloΰΔͺA: ΩψΆ­άEi4ε’ĎC¨<ζάω{,r³ £ΏθNa!ΏΕbE†[΄¬$w~L”ΕžΥΎz7±3ή(ψίϊS‚όΏAΆ«7οη?!«ύ$t&χΨBΥώKf?ιά|N};|μά’ ³gY}—“ +ο±μM³ε‰‹·#«^μκB°Oϊ5ΝΉ½ AΘ<οΩw™œ/›ΰΗεDxœ /Θ‹Aς_b‘#6ΈŒοίnh=Γ„θ %(&Δ`Ω―I³MaκψΤN$@ Adͺ(‘n“ΙC l­„±C9š΅ΙΪwω>ngΉKπΏγςΒ ²Β»ΘΏdΟγ”ώx#ol.Ήηƒ‰V05‹I ζτ™!’Χ0 0SΙθ½ˆά \ότgβߐ™ψ ŒGl;ΘΒδ|y;(oΉ€€«Ÿ_ύŸDΗ  ŽΕ?n‘+7GΕκS$sϊΜ½ξ$ˆθ½#H€ΐΦ Dn PΊŠ<t[Oύ‹P/.€ΰGδ(όΛ οkXό·›cA™ΔΘ(+ χy¬|’Ί)Κtt{Hτε"™=|XIΒ¨“ͺυ§YΆ•M7—«^i–Η3šεSρκ6qίςήβ]‚‘œW@ς3ΰέ ξ~΄¨ΡJ=6’ϋ|Υ nβ(GΑΈ»ΩιΛur²―[–M`‡Nͺ)#ςήDίΒŠ AG”ΓM§[ψϋɜ#ƒΰ‡γ;•ό`ύΧ΅₯ήΘΛαό‹Τ*ƒ »ξ½ε+vϊ²ηœΥ›>οΊΰ˜λv_ϊfΝ–32«Z8α^$α}šΰžhΛ™€(VW‡,Ow­<πdΞ± @w…ΰ½?ό—ƒαΝί2jη}μνΥ«@λ³₯σ=ρ­/Ξ²cή$yύάuεϊΈo°φ-ήω0{ώΜ ž[Ι½½z?ΥαOzο…2ynνΗfWθ1&z™ιd›,Ϊτ3]’‘+ ΜΚu°Σ—}ζ„ϊ½){jgu™ρw΅«6|ά$±½ΰn&ΕΆ­dzBiπqσVρ?­kΙόO¨™»—χͺ_Χ±"'Σy™ύƒC›ΘτΥc}²΅Γ'.έHXq%ΆΟ―kθεXωCς“w${£™/Υw‘ϋyΝjό»ΔNCΉΉΡNn΄(NΗW:χYξΪB¦Τ’t¬Φ½‘!0v‡‘ΖI’2%uFξ β†xψJψΘ₯JΰD”;9·pο₯Υ‚ό“UΗΟ4xσ"€£2ͺcU)³}yλ_Νι=ΠΠΚ›`κ«„δ§μžg©hϊϊ΄MξαY4z~{―Kn:UφΖξKί€ςjύ4zΤ4}•n΄†ε«Ndχ_ ±ΩεIEΓƒ¦jύGFsšJ„³Sžž`τ}\<±U Οθΰς…Ϋkξ +ω΄_Wφt―Ώ­2²ΞύσΏŽΕ·vUΫ=nU•³—‘±v]Ύ>NΆ‡g¬ό½wΥ;Œ5hΪ­1G{ς‡ΌO“[…0œΑ«›)²r”τζ¬ž”ΰͺΞ†/…ύh_šSz†tΥ, +@‚jψ^ˆhM… Ί@σž=^GΰςOβ3Ϋ‘κ0xτ8nwύvEΕΦ―ΜkHtSŽ<»οΚυiJ¨³4+ ΅υΜ‰#Θ—~ρ?”ϊ¬Β mH‰Š<Ϊ+ Œρ­γrz+ςDŽB°-t~›‰5|(τ/ΜΑ VA<Βγ“ΆŸύ»|ζ7”Πm\βσ₯ͺ~8Β5ZŸoνꉇž~!8$”5Β@μ8χJψyΐβ?oΩΣΜί‘§cc³ΐ“5τ³αAΑΝΪpP™]έ^eG˜d’₯ΰk"κωdφW9o!}E Li½oΒ|–Χo˜Rό?ϋΙ·˜{+#¨lςόEO½sς“«ΏΪΝΪUe +;ώΝων‡@²[Ψ1Ÿ₯v,Yωƒ2{Ύpς©υ"«œIžΌ½Ίšω-Μh[yˆ" ΜμoˆŠ<”TœHD΄(`―€ΘI@€Οb¬!χ)2μ»r-,>‘πš"«΅θ·υ^Eφ|‡~ς· Δ–<΅ξ0‰gρ_ΉS‚^4εŒI%)HΟj™qΖTdΖ`„·Θ·ZΕ ‚ΆgXr‡Ζ5)Κ I‡u<>J–SC ΒΜ}ϊE¦€ή Χ(ƒ€(E9|ƒC‡φwΘmδώΞώί­†§^‘<δώάΌ˜ϊ8Ύ¨…VζO–Η!<γ»·*ŸΑR Ryz8†θiŒuΩ8ˆiΗ9dˆ² Υ ŽŽέτΩUωSόΛωsΎ²ΈMΪO_.—ωoΜΩ3VπTαJ‘΄™eΘάύNͺ{HBd*C*E’ςΛA J΄‘# +MQωc,‘dΞΣ°>f©˜Θ#Ξ‹xF Biϋ²jΤ―aΛΒ.Šψ…ŸυQ΅ωΉO“XιΚηnŸΧΈBξήzρμ<έS–@Δ#gΨfF³lπ]‹\4Θ|λvƒ7 .ΌΕ*‰ Κ œΐ£,Σe ‡uξ2r’όO€έJNUQŠlŠ6’Oώ}^ώΒ“ο³.*Kκ{˜‡;Ν£ΎΊD"8S&ΕΩΥν=—Ώγk6PWLF7λΌ2T}ςhώ7¨Ν'ςA€ΟXS—.*Πwω&ωSΐόηN`OΑΘλˆ$λ"Π¬€Ίϊ2ωΠϋ ˆγΪΦ‡ΞGlwτ^h2ψ“Λ:ŒΪeWμ­He*ΦΌΧ4«gcλZ¬Γ!φ·Ž° NG!X³nΟO»4Ÿασ#‚ϊ«=$…ς4ΎραΙMώ€yD4‘mπ‹‰όαΟ2Ιψη +ΟβΣτΠH…³ΘύΦΠΟ[υ‘Bš­ίΪg!pς“Ϊ}rtzΧπ”|ΈeΛ¬]VΎκΈͺ +5€ΤξSL9 £Ά‘ΘΟƒŸ§tŒ# »E αN‘`ρ%£§ςΌ―ΈG <ƒBk^ϊ\&ωόζρeω§'οΐώYqψ°κ±‰³»ζM‘Ύ=€e _R›]€|x„N―^ΐ˜ΪX"?'~Α·šΒΔΡ„σ#.$¦ΩΊΎ—9ΰφqw?κΘωββ6 +Qy]žΈ$Ÿόsroίh$x£ελ«Ψκ"Ίϊ6Ν »9«ϋš[ΞΐtΞ/,ŽέD^k!‹ΰ²uœΓ +Fτ^4_‘uoс©ςmg±ίΗ9€,ί榕JWς_sΉ2Τ*462₯¦”³ϋΒo³½bΦμŠ>M#oΛΈjFΗJh…; ;|λlΝ¬ϋαϊhω^έn–ΠnΞZ~t=ό$Έλα–όΠ]=@;₯στGКψΣ"πήΦ’tœ»41ΊαΊOz“Ώ‰;-"] nΝΊΪϋ€θl‹O0•!0"jΩ›Ÿ)8ˆ’ΈΨwυϊ‹ΧξΈIέr%?gh3ZΖ² ›SE ψΙ?{€©.g&=ΎC_‹\'2ΤGΫ)Œ%¦°ΨM]Δη»O|Σ}Φiχ©'LειΚ •Nξ…ΌΓ!+ο±ΰ…Sά¬%nT½WΟ>/Ϋ™σΝΉcΔΧ——‡ov"”;‘ΏC–­%[{I’§_hΕκSυΙMœ›BΚόΨ%QQΘο¦Υvœβ>λ#=ωΟ:­χ͎!ώW l–qOPς‘E¬ξž^cφ=€‰B8ΨvωΪ€οΛ]πί$‘Ÿώ½iηυΫ—|ήΊ/W¬9ΐ±(gψVSt' ΐuCήθ½°ŒSiΑoθ'š€hέ€ƒζ“!w!qψTοΎ‘άaΡεαK}f]~LψύWmεΰ.q‹LΦ_Ί>ζ[ΕVώύίω>€e›Ίfε9A|ξun„oϊΩͺ³ y3”Ξ{.₯dŒ§?±MάΚέ§«Oώ8Χ–ΜgΌ]d“…ΌCΓM"χθC€0L"2ΈχΩ{ι2 MάΈΐή«Χ]Έ˜ΐά΅^Oΰ-£ύ%©Y>ΏΆ'ϊ-€FτΓέ;^‘Ν°Ÿ‹^Ϊr¦ΛŒΗZv•L•|‡& RΧgwζ7όιΦ²;Ή8qΙeόόXΌ”\΅A5Sς‹‰Ζȁ.› 2Ÿ)ί)Άμ‡μ¨σΦ#:oώ@}7€"|Μr/?πJ μkθ{ΩύW»(Γ3C«ήψYΑΈ»ςxF4›jύι¦uk7Τ}κ»FΙί}ΪIΙChρ©…½‘rαα{ βΖNσύΧH±ΜIΉΰω“.ΛΜVxρU―)¨δ‰έήτ±s˜›ΧDƈVόJhθu½φJ~α&Š6—™Zδ±έεΟ_•kήo?dcLfw§ΌIάΨFi’ΣuΓ6Ξό76΅ε2Tˆ„·°n½ΔΣ$Ι;„3ΚRέ†Γ.¨pΛ;:½σ»ΉE«χΎ°ν§άV/d>=Ώβ&;rwxš⺞Γq’œ―£εέ±G ^΅η2Αω…γΒ\+4!ΛfAX<|υκΣN™ΰ|½Ξ§α§­ήΚΧ²β.Ή„&ΛΪ Ζ(†ύŽΌν`ΌBdRΛE/ %ΊHΙ;•^φCζ“5e gτφϊ]ξ}δ¬όΡΝτ{Ύ<ώ%βσϊΩ3Q+X7Έ³+{vrΡΜfœohtwo·œZuέ$ω:Άox2ν€&¨iύΞ"Ξ­Šw˜°Ί%\—Ε|ρΒκΏ¦›N—B/ˆoͺ‚Uο +Hϋ]φχyεΛΠVYυۈσΎrτΝΠΝΠΑΈ6›ΰ¦¦jύi9֊ͺX}{Έi=§G·ικ,ϋ³uoΏMηι–=Θ}Β« Iήτ,ΰ–;κφ"Δ_VG u­¬sŒP(…Κ° ¨{α–…] ΐΗ’zΧ¦ΛΧ•UςAδχό•»εκω5Ύ£Ή}έRγ€ϋ >‚θ‡VηY2ϋI»bl™•ιΉςέ‚qχ z;βωϊΛSΑ₯γiαŸmώ8χ‰―³3?rκϊν—4nJ—UG@#΅+kb\C1Ο;X~Mαή6bι llΝjCΰ3χΌrΞ|nΨφφyε«ΘœNςΫQ_BJw©οaYύͺr—δΫ„―2YύVΚδ[ήήλΞOJζ<•;|–χΝΪU7kγξεΟ‡ƒΪwiBβ΅]³ΘωN γ_εn\΅_ΚεΚ‡b@Η9²†*¦€Šb (q4k“΅ψ•­Ι₯Žυ,Xu-WZΰiξβ-Κ,ϋ±¨Λ*·;aΏΆŠ\Glv… Ω›τhθ&α;T2ϋ,Iμ8$’EOHŒC„ΠΥΔΆΣυήξ>σΓ†ΔnZΪs+ηΤw5i\m+nRlγΚρmψ΄Η˜LW€~:wŠ™Kv}oΗbf΅k ζ_yρΪxEEύPς©xμ]Ή.κZφY3ε’?Ά "3κŠ$ψ‡7·+±?Œ­z,{³hκCΉ#Ά§χ^˜άydlvyhσΆΎ‘MUυ±CBώ΄rݐn‘9 αΧΟ3γMB!α‰"«uπ +”zΘπd˜ οM*UͺΎˆ&?rϋ½ϋ\S›WνΏό}W―ωαϊDε|Έ|Α 8~±Ε€Ρ +(ωšCε―"` LιXΧε"“`βΝ0mj·ImͺζBΟoωcφwšr?ΫέΏŠ_εoΓΚπCψrS‹vDŠ©ΛfΈΟ>Z<σ ό +'ά‹θ·0.ΖS {ƒ˜Β±m+#Σ:CρΥπ + ΧzΘr–RχRφp-}mρ<χΙoσ3?fο»%—ΨΓλˆ:A>{n‘»xΓD>PΑδάB—V½Αό”fώα_ό»νŒ•Κ| = ΒΛ·Ιν<΅‡₯8ΩΆfFz6k<!:°ΞΓ›υ§Οηδ–ΩO7δ~Y΄oXO?ε–Θγ»Οι1Ά£ΤO[εŽbLEσ9bσ™Β Τ‚#]M;hΟ½œ‚D{/ΪrΨ/†? ƒ‘fŠ+”«δ―~<,!J…8읇¦yGmεχιο)ΐόΰIobΛΐήLΤΑ~‘ϊΝ\ƒωCζ/Δ>œm λ6Mp3·Άƒt΅{ά§[vΤΓ³ώŸρ[ϋα|Ύ;d½˜ΈY|Γ•ΩE`[>°/ή΅|ξ3Η¨|k'ω‘ΟΉα’>,― ~Di„JΏ_t3₯Q—$Œ€υ–Ζ₯nΦ€ΒφΔa5ά½u]ΫeΆnΤS<”^_“ΣμΉnδQMdk«½–x*ΰΏΫzΦxο}I-+% #/‹―‘Ρ{Ψ}α7;!vσΥ€ΗψmXxAa_u’xοi}Η € ‹Έρdˆ–ΊV¬KΘPυδ©Όkή£uΧ4Ν‚‹}Τέο«JϋϊΒg~¨ν2GρO~Χl:ΫΏ΅§ΏJέuλ½βe>+Τ9|‚°AΌδ΅ΣζιΧVW‘ΟƒPΌσΟ«°±{Γ{Γπ/ΣeΗCΡω%*M²ϊFƒrfο{”!”₯‚TJεx₯ΊϋhβrυœίwΏϋΤγͺsώΝoέπG5ъ*8τNWcw©ΛeF=&‚κύ76…Uω0@ŸΨ>ΘΖ}[ΎόΙVT_ΉΫ.λΥ8§§°―žΊΥ>΄zΚ~;uΘ΅όLኧγ\Εϊ@Υn)$ΡιΖ‰M_Hγ¦ mγ\½lκϊ3>°ηω ©ΗαωY~L›‚(n­‡ήΫέ2^ώ &uέΊi΅˜ϊ­ά²ισΏΤ'd+œo½’wΤMΕ}5ά’ύsΧzΏπ)\τϋ7kn’ΝJ†ΘJ\2ΈΤο0=6KP0‡Lά΄·‚+.ςΝ,ΑRΡΎdΎΈέ€ ψω¬?ež#,HΛ +™q$YƒΚh›žƒΦνZϋή·*‘Θv~ΈΎτΒ΅ί_£Έήώ ρށωa·ΫνΠ‹­GMχoΟςξrσΖκύ{Λlθϊ·C‰’88ππΥDΆrK―Ρv]€|D-₯›’œF$+hWύKΊΎϋ4α-9ήFάβxψ4‘ͺφ(Ι`l&*j&fU„+;}ΪC/r;—ƒ+žν—―mΌ|1Φ\Έ>ν;u ί@ϋ}_&oَΨ.ε:oσ/¨ΨUxrkΣOΏk_Ÿ½ežηM‘π!)‹h=4M’ά’‹!QΡ–-Υυ?@Š«RŸŸΥ;Ηn²0ι΅Ψ’Ξ–!¦’„π'Ÿ–ΐ ˜\¬x θX\zvρSΖξxΣgWλΎ v_½mόνWmΎt}Γz―kή/Ή 7˚ώ½^žsG½Υx}iŒ²η#Ώώ/Δ;«φ&UQΨWƒe5zlςωԟ, δŸΪK¨‚7ΔB{Ώ0ύͺ>±Α³΄%󑍯σ§_eβ{jΠυΏ[ŸΧπ]Δί.‚β±ΦΪJC!Zε6Z&€$”χk;kuΧ»žκχΦ9eɜ₯΄aŸύ£κ‰“…kφ§ ›•[δξΓχJˆo1e>θΨρ—­κU]Ÿ]°QΥvš«%·ΤX|BξΆ₯žώšΠDM\·V•nΉwΐ‹¦ΆΧέΰΓΊq/Ϋ9Ο7œ fΌ―-_«‰HU ›Υ+ΑΪΪ Z_/AœΪ7π'3’·™)ΛΞfy*j0²Aλαž‡΅wζ€EΧίΣγΘ«˜$……Ι-ζςΡ/ΥΟ|X²ϋ±œΣ†M‚T' .Q1'Μl/h$š5gΌά ][1ύεξ Ί%w1ηˆ >ΚΖΏͺω„nΰ½π6χ5ΪβΉΪόρϊi"½Ζ-₯«[Rg(Cj’ΣΑ]šΐ„~ΦΐΟέ’f͟9½°Ώ©ΏΏ&Iϊrπ‹mo9n­ͺ°twΛ§νΌΒΊΜx~Ά•.X”§&ƒTω¨cl4CΠ$©¨#Κ‡Žg`H`BrxVvrΝ0˜Y₯“1a~»9kρλ°t;$6νη­ΗyφŒΈ„_κΰρψ²ˆΚλ‚+>1ZO‹œΓW5wΑΚ/kΈά@νumW$šδ.Χ—²[‹Φ°ZυΑ Β€·τΏ©ο6\ϊš"FgOΗ,†iTςPeΧ qpˆΑΛ½ΆΈ²zΔι¨?Υ8Η, ·΅»Yΐˆ«σL¨βΐ{§_s5‰9 TٝΥ-ΟnΣNβ3J/δW)D»F“R2΄oˆΌ#ψ_α­f©…²Γό1NΠΤG³€’­‰po`~₯Œy4eχΒΑΘ>ά2ϊ:‰όΔa&” kδΦ¦Zςπ•έz& πτE8Άϊ1Χ›Λn/ΐ@ƒQ―ꟈM™\f«έa»mžŠωEH™CTYσ:TΉ`0.ϋ€κ£εeͺΓπͺqεyϋyAέΨ΅§j‚be·›…³bυΙϊδ/δ? ³ΟΛ~ΚοZœAΰp:Bx΄88h’’Χ³…ΛM‹8σeθ΅O™θ78³nt⚠2‹ΆhΊ&Ž˜TΎφπ ΞΆΉσώλŽΡlϋ[ΚΦ©Ε$ζωΎ²GJΑ ΆGΐ!jΓνζ]”tβg΄u:Ξ–°,Πz@ΙΠ‰)Χφ―6σCέ ϋάrFκ΅g­sh4ρΉ}*VŸ2JώbύoFPώ)9γlΓuόίΤπ !>L5-¦ΘΆ£”tέS‡|ύLΗ •Y@zκm]…TG ¦‚r,b%ίPSύEτ€Θ€NSξ7Εόbύ―ζV*υyΐVηΨΔl?F +KUKWΑJh*χ― )Ή›ΒΚL5n§ω\ƒ*q@ψ¬υ€ %˜ρ Ντͺϋ ςq©μι’Y»¬NΙΗΜ δ?Tlν"|'»ΰ:jκ΅WΒ' βͺeΖ.07S |aSΎ`¦5Ts9φbšε›©ιDΠξίP’ϊΤXB;D™ο >rƒσ UΫ·Ψ²0ιM)]Ήξ3œ_’ΰ‹Ϊc(ηΧq―]Tl—`‘Φ€…œ‹ΪΎ]°Ϊ‡ψ₯Σ<7v4.œω`E9A·6½νΔ›₯£N@ΣNB€―ίΙmήQΑvαθΫnZ]B~ΛήͺOοΟs@mϋ[TρwDΙ9’!¬‡±ϋ ηΞtΐ*²}¨B•΄Ά–²ώZέtpz樬kΓΟ=αtn­«4aΙϊˆΜΆ> ΄ΧΎΊΫβΧ,²}γ ‚mέz\ΟΗ烕‘ύώyXοh:}€ν¨ώηΑω0ͺJ*“ς¦κ=ͺYj|0“*hκ€λw— &n~/MXŠ=~e@ΪΣΌ` σζΑu`: ²¬ΟB +<ρ°ΤmƒΤn΄ίρF *Υ5ŸωΫͺ›α­τλ|ˆwŒΐEΕ"ZΧJ¨Œ²ΌÝZ|ž[VxΕΤυΩ­χ™6σC&>΄αΪ[υG€σ<Aλ—χYύα8τFh-{μ₯ξή-JΖ–―<ήxIOJό―μΐ²Ri™ƒ“o1υΉW‚)6Ž1aοΨ3ΐJθ} „ωQYzΒ‡‘jI;•«•͏ +ΐ)¨5₯gυCŸt[uP½?|xfSuma’;ύށTΰlΗ­ΓxΊΣ„·`πJj΄λX5Ρ?Ό9t{ͺ6|LβyS™[΅ρ”z₯Κ²”VΉS*^*εNΠ/zŠτ+pΏp ρ°”: ž„Yό¦ν₯=υΎ7 gλΩ>Σμ}5KωΣ%Δ΄“Γ'rμoκ ζΣ–-6»nΨ#ξ_·χ©aΚ1݈Ηυλωξ+΄αΤΘ-±“>f’5gU…Bώ¨V] +Ζ¨ΩrΖ™s€ ώW¨}¬X HΜ&Rh2$ψšE]ΧHE €“τ¦Uυ^Τ0Mΐ Ώ€z y‹rύ9Œ#p ?Π{ϋ±ϊmh8²ƒc4(©Ϊπ η~ΑρVμLς…OΏp}Œ•˜LMB!\†ΊeφsΛHmΧΕM—,!ήΚ¨'!_yαO'ΟπσΜώM1ホw½©/Δπƒ“Ύϋ΄U1Aχ±]τΟMν‰:h’3`]‹Ψ^J.δ$σnίΠΨ΄žΣ ξš9ήό-‚eΆŽ nItc€;‘νOKpγ€–u΅ώνoD„ΡGc ŒΡ[΄ύ FnπGRηα›]Q8α`ο-_™ηp9W;^?. M§Aqή±γb-‡0ŽΧέ]«Ζσ„&dgυ[Y΅ώ#9ΔΞx―ΰλ^­ΒρΑ†HξEͺοK±ΌC¬Šκͺ€h?,)'£f‰|•Fζ7dό―BcͺY$6.ŽθD… €Νˆ”ŠψκW³«‰²UGΐMλ‘Ϊ)»ͺςU'HΌ­TfΑͺ·±‚ΐΞoι*ΑŽŠ€^Τ3U―ι*Ω£JΉ‚ύTeΔαŠΛι;|»£₯ΏA9‚Ν·”}]u$³ίΫUύ,€]νϞ›υ–Šxι·―)jÊ$9‰ήΗ5)=₯ΒYΔG +¬]U΅[ʝ(! +›Υ=Ιΐε/ˆ½r-A˜άsω;γξn]1«iVΉD’Q‡<σ ‹ošΥ³uεl¬π+ΦΌ§, Ϋ°4ΑΤΞ`νόπ–¬6υΜPΕ3?LΥΰΠy:ΞqQΟ­zΗtI₯RP3›(p‚…β;τ₯xjΜ-Υ›>/χ\‡Q;[Ǔ˩RЇO ΅{΅έ>O£ρŠ +OΙKΘΠ¦ΧΌόΡϋΚΎT½ι‹Ζ0Ϊ6ώ?ρ•ΧλΞOdVCπΏέφD}ΕTWψ<,΅¬΄›αΛ" ZŠ+”²†κ½±Ω—ΏMͺͺηΝΧ›θΒζή‘mxh41έΊ.xQζ`7s;쏊¦>Ψ~ΘFΘ4βσϊE΄(ΐŠΧMg₯ ›@«υπςOh’ΨͺψΨ:Ι±½dΞS½6ΘeT3 ΛΏT±ϊT‹±Π)ͺC,0ΊeλΚ9܁ΰ:$νξΔ?Jέesν}z™Ml1 ’V³<½O˜DΑ3l£ΤώΜ1_>t5+||°D¦Ϋ‰ΎFγζ/Ι§žΆœιΉβXηiC• Z–MŒοPΩͺsplkμx"ΰ¬MϊϋCρΕ„zβΫβ‰·*ŸΡnΠzpπωcsΓ+Žζ€σgS_j؏ΰλ$‚Ω»“UsjέυξŽΝS–¬«‡υͺ;φs`Fπ–"3τnœ1)@mΎσ" fSΚn|cΚCˆΔ£Δφk°ΙΒVh’*β/yΨ‚»bΫV‚©8ˆΒj·`9ZΆπε.3-wvœ3k—βσ!ΉσΘψά>1™έαΩ‚” ΨVΠSΒ’ΥΣ/Q«¨¨wŸΓ…~AMΣΒ’;D·ι +ω4-‘x“^½ νΐujAͺxΦΡn‹_…”ή~Μ¬δ·¦-@a=w/σŸ§>HπΏHmy©νH5ɈžϊεΐTΛYl+‡΅”ψ~W˜) ώό›V­·Y@ΆΨ\ύb>4Y +lͺ—α(²Α­2nZw―„όώΰUκXv¬ό`Γ$ͺξΧ}ι› m(ΜRΈŽcmρ΅Β’ΗYuΚλ΅ρS•»³(žŽ,­d­νM΄λΚD‹ΊύτZ‹;d"€rJΙˆ\iAdsπUΕΨ΅`hLΒΊaŒ%‹lVBΐ/B]ΝIΊm»›i%μ1‘ Yςu{Hc_dvcR{3Δn€—*ž}”±d‘ΝΐJmŸIeΦxρ :π"φGzο…«O’F±ΘμΔ@€ΓΎΥŽθπ$(ΰzšήOΕͺ!5L]ΙvT‘–/ϋFF^ΨάΜΎUˆΑIlζ”™±QΒή[½ΒH `§ž½p‘S]¬Ϋ[’βptž>1eΩΦ8΄“#‰Μ@,`Ζ>“ΦψvS)°kƒ#Ζ’E6uπ “ ™―Ϋ’UγN K#D†π*©χ?σρš-gLh‘ξ”ΐ±Ο&vŽ@ή轌Ŋlκ"±Ι +uΙ +΄%ΕαψΰvX9ΓΆτ\ω.ϋ`9­\m(ςΠφC71u[Αe؟Ϋg¦bE&΅Θ¬:ωΓΐVğR»­^>φϋ`,Π~θf‘5ΚΞ{ͺζ„gΈ‰ƒ‰4μ8Π`–™Φ[9rΎΪ° +f‹ ƒμ―ΦiΚύ ¬<Ζχ=¬:w˜dwqΥgΐ™ œ-Γ»¦ €7Έξ½ν˜mύš •ΚΩiςSρ Ω%gކn°Op΄™ή…‡=τΜ~±S`¦4qΙJψ4‘ΰgL iƒ2a+E€g|A§v›ŒQ_½ρ3pμ4c³Λ}gaπ΄ή"ΏEι8ΈDfJΡ»ωr:N<„.a¦Α© ~φΜbζ*tΰR£qωΨ‚Ιs]zυόΖεˆk#‡p†Ω€¨ΥψžΝΔ‘XέE΅.†=fα„{MΈΞΣApŒJ%¨LΨ2@€*T»xζfF\βFn+Xtiΰ=ϋ†‹¦<_v‘ΝΫD&aωΡ’dlιάgIeΒSŒ•ιX’9€΄ωcBAτ^qπ"&Oκ4 "\x\ghπΐίv8ΠBβ3υJDΆ8ρCα•±M}Θ†8 T(<{3VUdS¨LkˆύAώˆηk•@δ*beλ’‘oΓ'QΗ7{Λ² ¦dΒΆz-ΜJΉ#ώφΎ<ŽγJs¦§' ηœs&AD H‚A’ HŒ’"EŠ€¨`Ε•DY²$―dK–lΛAΙV •(Y'ΫKΎπνέξνzΧw»w·αΌΆφtp¨0˜iΤτtχtxψζ#;TWWύUυͺϊΥ{»?ˆRH―fͺ~™hJ Τώ^5HΑκ‡Ζ}/"§+£φ‡ό/2.ΞΥΌ€{―θΑ;@˜kͺΚάSι‚D7¨ΗƒΨxeοhPέx<ΠιΩKδνΛήL2¦Cϋ1…4?λŸ$Ά7©šόφUπωη.›½TE +$(‚¬=ξB ηψΟΨFbAΐνμ₯ΌΦ-μ…€”2"P1ͺπΗβΏnZƊ-k³Φ>^ ΑυTμΌ^2! ϊΗΐ«C9€¦‰½π¨4bώ-›ΩKH)eD ΉΒ4,Ώ΅?$Ώλ‡μτ')ˆo%n/`α¨ΗξWΡžJ KEWD •ZΔΨΛβADώ’A„—τjUΨ$3β£ηdπΐxPΉΕ?ŒKιO2kΧbX‰’ „ΗB#¬‘ …ΚΚ¨……‘+"ΐL*΄ϋžΩ,.P―ˆ"αΈΐοΜύr:&0GL[χΚ\·₯?yΐ’½ϋΖ7čJ§Ίox­Όο0|9• Uιό–!CP)€3ˆ°«bε ΰΰB\xyΊ?εκ?eλώ}χ›Fϋ_zbΨΖmΪυˆLΓ\5c§Πσή*ιIΰ;Ϊ2U_‹Ωb3…w„ ΄Βx±S4ˆZ–γm…‘[Š"X¦¨ΪˆNr#`6΄οVδ1π±YΌ|ϊΠΰο)mmnlυ±7=^G§ ϋ._Ν”^Υί«@2g|:,€ν*”[@8’œNX +(|ά―€Ω§]F†ᦑ‡AΒ ΞΖαH2|ΤŽί‘U·.$:%pl@UHaθY―ΐΗ +t͐ΐαρ™`„ˆL)A«b>Ώ;΄‡Ηή7(Ι0›MmΧ**ό1 ΤMIV~ʈ¬₯εώπ6ΰ—¨=™Ήl0,>“‘€žIQ‰½§Ξ{δI§ΪEk²σρμεj8/]§΄π‡όΙQCՍVˆβ†(/C¦aϋ7ςΫΆ%^˜‚Υ"ΨμΌξΗΚ—“ή(ˆm?_6ά4Pί„ΣπSJΛ8Σ_π@ θει,y‚kΔψ4ϋ J=\ΜŽ€pεΞώΫ.²dEiԏH§ƒθ?ΌA¦…7C#·N^±ƒŒz- £η2B’^5(X0€ν_PJ―Ψηω­~™F%dA σΊŠ\Ο£HΣuƒΪΏεh„ί}&π џ +ΰm!pφμ;ύ>Λp¦4„#έ7Ύž΅lk τq*‚Šϋƒ όρ‰Q2ΰ£@t98πφ°βΞιΎ[>dέ”Œπ…$?Τz:½ΰtkz«ρ…¦αoAώ3Ω#ΚE·ΰ +%Ά †gο Šτ5΄ι:! €@Ϋμsi•]―ωΝWA^BŒ"9™-ΜΤ{O„?΅“Μ₯€„BXχ`ιΌΡ$ΞUθyχJ ‘‡ιt‹p#ΫPΕΚΒαc ΅ άmέσ¦J—Ε!`6-?αω%ΠτΤ<`CyψSόψ±ΗL!ΡσnIt`Dΰ’έX· £ΰ,Œ(ν2ψ˜c2ω‰œ|ϋυhΚζ2E½Aώ —£?‰°Ξ^Έ2LύĜQ+QžΩΨ#β +WN‚όΣcμΣ©q8{ ͺžμΖQa*!ϞδΟ9Wάmέχ«{[§_χηQJ+ˆ@\Ύi}0Τώ.Λτe‚…£›~ ΐοϊα•‚5‘‹–ζ=2’ι™Ν ωuΛ6ί‰0"Ζ•{Rpj=p1•φΜ‚5nκoR«ΓyΓΧ_Έ[Ÿχ7Jο[xΠΤώ½χ’Ω§χvuΥ©½¬rΛo|_Ν’2c}–Bΰ䁱¬„rš’ϊ+<A—nΌ μ ±vQιΜ©•όŽοΉϋ³λΐ²ϊ„¨Μθ!Μ¦ζAΣό@ώCοD!ΐ-›π)ΞΣύο㺌_•ςDΠ./šτ'π]5‚zΏ~Ϋ}Kϋα$ςU³Λφ?o³΄ξΗgμΒ.Ν-Y/Ϋ[”qΑκ` +‘'Lψϊ ?ι0Gg,,+όζgΜ ½J('(!"w>D>&‚5'ή©Ή9©ΈE1r~sV=ΏγeοωΠES(‘… + @¦{±Ή&π-»”πAωwι¦rR"ΰǟφ>jπ!pπcKϋΥ&‡rŒ[›#©€uΙΊλ;―}U’ΠP΅θ8ϊύ5b2+” λv₯›‡ΖYΦάβ³CωΏώA₯υ@dš©νΊ`Κ¨t%£ΛβΰŠ:Ǝσ֞·œŸΟ2«mV 4&%»aΪƒώ[>2”ΥVeAΚZ·υlNΣ†°ΈŒ…(οήζT`^υŽp6g7Κ[ Cεσ›5gƒ0 `κ‘?90sόφ…Gξς[ΎmΞNœexƒ‚πΉ€ϋͺ–½O)XP[BXαφž<_7qOnσƈ€<9Ί$Cžf¨kžυΪ|3wΖη-Cn”Δΰ. +ڟu): €ΦψSDJλX ΉΗ‹π?φΈ9­Κ¬₯NŠΉ<ΐΉΛΗ ,1 +Λ=#Ώ*ύϊ‰{s›7ωŠό(uSϋΜέ•–pG½rχΠEsκŸэ@‰q†ίyZ‰Y_ŠλΑFsΟZϊngP—WYόΊϋΜI₯j¨#<ΛR+:Κϋ―nΫχμΪ3δVπ g(DΪjš|€dυU)eνŽΘ547\ω ηΨ;ͺ₯γz5[Οe@.Δa‘{;Έ°GΟͺ‘nŽH~Χ«μ# )1ΝΉ-j(»» Sΰuν¦Ϋ›¬2ΚCέgΥλG˜F±ωžY»6θ‹|w›:Μz?ό°ύsΗΛ&p”ΡŸμ˜MYΝ2†€YχΈΙ*{% 8ΞX~δΧsΞ›žβςΪΤωuΖΫC±q’Κ‘c-{Ÿ†Cχ2œ±‚`dB`ePU > iRi8u«ƒ«υiΨιCηομΓϋίWΜzΩπ’γ2ΌέT>l3³δί5Ϋaeΰ*Φω+]ιω―pK7™μjχΞ°πv¬l‘ΠΘo›¨Ί±iΧ£/5xηηŒbS‹ΙΦέύλžγ?oέχLΝΨiPs§”―ŠHΜΡ΅>¬:›χXχόLL‡h._±φl./Ÿy$­5Γή"έγ§vόLYί‘Ό–͐σ°Γ‡BLnΎΙΫΓŸoιΊΙzΰΡύ«Ω$y©(C?@8ψΞ[€™Z―ργ½”TΜ–‡D½Λ~=Α•υ›¬ς»όKPίE²IvHt2τHˆB L°DΕQ3v +φ0Λ§Ο­8π<ΌΥzNΌ  ?ΐ’‡Ω§λ†ΧΧxωΜ9Ό«fτdy‘’Žέ0ΌεPͺˆΔ\k¨rz‹€Θmk(>?‘N ¬ο}Κ5μ +€τ¬4ΐ\'w₯iΰ‘@g”jiΚCΉψƒ€₯e_€ΓΠωψΎw\Ɯ\ξΟ›5Ÿ.ΜΘψΩB£ΰεϊAιQζϊ%4ΈΓβ3] la1§0έh*`ΖΖ“₯λ˜uφ½ΐ»W·έΏ—SjYΐΎmΥΈx²θž»u$— +­eξτ¬τF₯%b‚Ί\ڏΣZ·ΌΠ’Wπ“?Ρ£ΌnρΫ_²4Ο@ΑxΩ("`ŽJγj·ς›Ÿ“€G]Ιdί»Dςx³r΅¦EΫΟΚK)•¦Υwψ1 ΐΏ˜Μ>½ΰ¨θ%sR±d+΄9“‚s"hš¦‰@ΡΆ κΛΜqΉ\Γ$ΏER±ΉGρΫΎ«9“ƒ 6…Ο—ŸkΟόχ™ͺ?LUήΥ”e³ψL'ϊg1Α“kπQ¦Y z«θχΠƒR"`° ή#εjmήDπ’₯ν 9«ΑΔΫ€,3ε₯8ޜ±ΜvΣ½Lύ̟΄J”€©·ΕBψ»»­|²4Ž3K’χόLμ&09?%8 Ώώnι8­ε‚ΨΜΌ:,ž+°τίρuψέ93Ύg»‹Ύ΅ο]ΌEš>—‚(ϋš\βώξƒOFŠΪRεqκUΛQŸSΐς#†oΥΝ­_μ+βGϊΜλ–ώ3\i―Ι₯:¨@^°†"‹₯e–ίό¬υπ'β›žm:prD+N7ν΅βΪΏΒsŸ»eώΒƒη»²³"δω•Ύ@pjϋ)’£€acAŒ‡¨8ύ―[Κ6ΔΘ"šΑ&]?s…M:ΏS*Rf² ΰ€aŸxA`΄Κ{kP`o‘+^ν΄#βxY*iπL-VsR ·dDΚΞ8‘ϋΕs2Ψπ‰aN_jπ֐Όϊ…ΡήΥώSn½7T°,QBΞΈ|Sλ΅&ή!yM)CY0sΦ™Χέ<~ΰCΨZzNΒΆάiMD«DqνΝΫΝ)ε\εr&ϊώσΑJΠ”σ?ωέ?vξQψq κϋ)‡…Γφ°¨χuχOΣU0M 5š―Ίo4|ΗΒΥmIΥ8°K)=fήc<ŒKΑ*+Dsd2ɐy€ϊQiˆι Λ.°φ!HΊΣΡC15>K»OΏΞΥl$Γΰy­&έΙƒ­ιΎΔ;γu˜ ]Sd·Θ’’’”“"ΨΒ@½₯ΊY`œ9π΄ΦόΠύ–•GΑVΝε΅Β)ΙβΕbΓξ9‚g959m°wγΤήψPΚΩv.΁CςΧn5YI ΧΘΞ‹fς‹&ϋνxΙΪ²Σ“«₯δΛdψy-γ珀•Γ όQ―lρ”NŸ`[΄“πq³t\Η5NAH:§†δrsx’–ΆBγΜ‰Eˆ“εςMSNΞΊϋψ­Ο[gήΠN[|Š%§U?lB$ύ+X±]ό΄Y^€ύvV,*ΨύJπz^y,ΝΧZκ«|ΕΥ#'€‚}Ό6~τΟ4$y|z€‰ ±τ©₯u?x  3Α5¬Ν™uΨ!uΪ’‡ΖJό)·#©’I,†ώŠΛoηΚΐl +&—–»ψ‘G±1 + Ήͺσžξ§ή ώ_ζπ‘X™€Eb" Ž–F¦œe…Ίζ£αBΏd;cβ›ͺ| %=ΞA–rΆŸty»δ?¦€–½OΙώτA–5§Υ°θMζxD_<ψ±uοΟ]?„T†RέωΫώ’SP»~.‘ν:ήωŠ+uϊυ+O͞W¨L’9`@„ίrΰX“ΚΔ…°>ˆΞ€Nςί-6ξ[žΖ(ΟΕ%ϋύŽŠ«*xY˜#ά•  pΛ 57Ώ›_'A¦ή²€"…«ί‹qcΙRaI«²»NU”ό²Yj%—΄ΉΓ·‘όwΛΑœ¨/ζόˆ“π,OύrCqgF„7a@ΧΤ‚ΐ\ωοZ#wν1ƒ£OΆ?ψmΑ’άΉΠU™,’ς(‡ΐž·,νWγΓPΆ^f‚>³ΌοπΠΩKθΥΙ nΓβœEzK•ζ΅ώΌΪ―§”³‡όw|/#€`@ω.ϊ0ΜQr[ΐπc,½Α§.jI S€;ΠKG@Xώ»VMKΦ]ΟΫl2¨†@#ά~„Ÿϊ }h~׏`" εžάJžΉΫκˆ@/…„χ%ό]=yξ#†:V^ν/0 `z3+#œˆ#‚ί•Υsβν¬eƒJ¨ƒζAb†–ΣjhψayγΞ¨pρ¬­"ΝΎΗ} πS˜Χ€ςŸ@α“Σ4Φ{ςΌ€δwέ2μϊv˜ο¬+ΘAΉ6ιΫRΓε`“–ΏΧιζ Œςί5‚ ’Ο@tHA; ΚΊmP)‚H³œ›ΉSψXSr©?·Ÿ$6­Ίϊ•E%ΏΑεν ©A‘π,/ύΛ-eλreίšΫgθx.~ΙΧ8jΪυHtzιάL”>†‚(½škάνά5ž½  +a¨­εΊθξ{.Ί\ύgπ9„νQ±ΩUΛgžd”όF–«3#AΤΙ"Šƒ•ζΦ +ς»h—+ωοMΛgΞE₯•ΘU,φ|Α`—‹MFη~Α†sV£yΊ–δŒB±ƒ νG€°“(O Μ‘)…υχΞ5μgœ ¨I ³ώέφς` v–χ‚/{μÝRJ‹€hωA·ξμ_Τo»O9!–šCMŸ’Δ‹δ7~ΣΊ}ϊ:πΩσjψξfΛ©iœ|X„δ7ζϊjŸͺNνο1)|§;ΫέΎt <ΘχΊkωτΉΔΒFε Ώψ1Δf;iΣ–MXΊnr~ μyΛ?yΘΈNΦh2πڍ=s08ωNc2Υ°ΒχlVX”΄΅ξ{ΖέίΔmύͺ.ΕCΨͺπ΄55ά³Ήι\A$‘ρΈκΘχ²κ†ΐ΅¨`ρE½ΚeN©p~#΄μsςg"~εμ{ϊŸ ΊίϊΌ₯Λς½NeNrΉbfω’ΙωΕζ€mOΗ5?'π=ž2”όοHPΉΪ“B‰ξτ $H(]Γ­χΤ…5B’“%)žr™ΨΒ ίω°“`Ήq·3Φ-˜σ·|[[Μω`EΔF˜H!˜—“‘Ί¬Ζ9ζΨ“UžP­²5OhlZyΥύ·|δ!Γ95ŽόGLΖΏΩ¦j΅ΏλKd{q¬l=ˆ2fB@rωο‘kM“€”―’•JŽ©†'‚ΉψS*œq‹»0ΰ Π6όζgψ―85KrPX όξž7ω/σγί‹Β•-³\έvΩኺ0g!\/b{Λ3p€έ9pkZΥκζι'°΅ˆ¨χϊ¬Aδ?ήΘW‘ͺΗ£HؘFτawΣΣAPIώ» 8₯Λz…'d₯vAxιεˆ-Ϊr%n &WτˆΑ/}©sAξό5\9H―q^Ηq»\‰£\;U½2‚±­bν5ψxtχΙ "/Kφ΄κ<Εφ„2]‹ή"€€άςί=Š‘Ε-\΅Λ/PΊe4B’’@3ΨΆοYw?‘οΐςΏ=-όΣ•κψsKυ‡©ΚtβPΑhWLώ»Ζ5Ζ`σΤγ ’°…’ΣŸ +š?HE°‡Ηf7Œ δœz_3ˆξεbW[ΛζŠYΥƒ'H]^;…εΏ{lb0bΥ‡΅Ÿ#*q^θDΏ@μgΦ… ΰ]ΏrχΕτ-kρ§ύyͺψkNVORΏ*πšKώ»G=V€0ν†j~W‡rPf3h™‹;§AxηDγrwΕτ-―_šδ!cU{ŠΠσͺλ₯F-PΠεάαΏϊ¦ŸUœH)kηm!FmΤ„ᩝ5c§ΦœxgnρXΗς^T RP­ΐχ(ΨζB2ϋTΛ0W•όw  UΔοCόVεΐ 5xΧ/έ­©’½Κ„ώΏiD평Ž v …H +ήT§όŸ+1Ξό’yκ±’Ž©ψΌep.V°νΤπ*|©%δΧ—tοηΪ;>›Ϋpj;Φ₯ό‡ΪΥή\ΆšOoӚg¨†™lePΏόŸ+F°ͺl›ύvy‘”ς•0” ΚXΠ˜θv`•~Pvrηv φc]Κ£Υ‰j–φeƒΩ'(I…ϊέSmɏΑήλG0"Bΰ?X•ΐΘD‘₯ει<ŽΘ„δ²%έWΑt‡%–G3©δTςΏ>)ΥCΖͺωτ[dφ)Ο›«¦εΏ‡`ιΏνγΉΣβΡ*ΕΆϊž›'πετΙυhAYOu&γ<‚g©YΪ/,&,υuvC—HOςίCz άώ 6‘λΆή]Ϊ3›±΄?&s‰5$Ѝν­ςpΔ‹ΝͺΜ\6ΦΎΊ‰{Vy .HκγTOς¨―τhI폹ΰΒ™}zA½ό?{©iΧ£ιΥ½‘1©ΦΠΘ°ΈŒ΄Κ†` ©Vb€a¦mφΉ₯o-κ؝VΩ•ZΔ;ŒΒ@Žι!Ϋ«z`_»ιvhοϋN Ϊ–‚cHέΦ³©K:Ρ―Π»"s ε5œθλIώͺΤ’Ϊίυ!°± &¨’Ž^ξΡςΏηψΟ`γ%G“),.½rθΨΪ3ͺΆ™+FΦήωΊo|Ί#Δn,!j`zŠ=ή₯/VπgBu#L¨λsG‘±―= kL/A96·Κ*?ξΊώ§Ύ’‹&5‹£‰ΣόלΪς―'Κl°U’?•! Nώχ9Ϊ…«b ‹ό7TU%°γΉκκW`ίXΏνώš ·ΐξζŽ`‹ςλ ©0εa* ˆΘ»f³λΫ +^΄°΄L­θΘͺ[—ίΊΨ.ΌεiΨώ ”­γθχu΅«έ;―ϋ‘0=Tx|fίιχύν$ϊ1vΛή\ΊP΅ς+ΗjΙμS€υ1xWω;ސ>‘ ±TΘŒξX‚Šx‹ΈύSD-ΑΔΧψ¦ΐ―λϊŸ`Βυ‚#|uό€ϋ.½G֜|Ο•‰^Uρ- ƒˆΔάE{WRI«@&^oΑxxΡl՟ΰΕΥ9*υ ‹χ―»+“BxυckΜBϋαuΌψΊΩ%(,_kFOP ω‚‘{E zδfΖήϊP―9ψΊΨ{κ΅:Žώΐ―ΒPb]!pφ;Ε_dJu?{iQ‹5ΥŽΣ™ςx―’UύAO/ΥKs!Υ(ϋPχ—¬ΈΑ› Άλ0Ε‡/{©(₯>€32cο‚Ο2{•ατǘ­Ϊ’UΕ‡όΛξ%κυ^K•Ϊπ€ς,Dς–}(Υo»oar\*8ΉtEεΊόšžΨ+B)UˆόοϋRσξ?c/ F Œε‘#Y„•ϋΝ&Mͺύ]ΣΑΕ‘"9`‘<₯E~£πηeMπ£‰Ι(—Ά‹ζζˆˆΟ¨ιƒ»Σκco2–“’)€@σΤγΩ Γ±ΩUΰg€?8X5:―}5χΒ™EQƒw±Ώe%|Ά#ΛλΊZ+αφ»θΠ¦j@•}@ΑSΙ¬bƒ₯ΕΔͺ#ίΓdΔ^lJ)!˜ˆγrjφDΛiΪΰ―SΙά‚­<ό’pPGT’_FΛψŠ\XNυ_™,ΣŠœ(g~”]ύPS hζCαcXμΐt'θΈ!ΞlbQSqΧLγΞΑJ!\fΊ+ΧύΨ.΄ΐϊ|πΞΟEΏ4€ }σΪ»b2+'š=gΕΤ•^K+ϊbEœγŸwiUν?w:8]Ÿ"zPIπέ Ϊφ‘…΅7\zΓTΣ_%Ι%m°h…“2ω³7₯_)!ΨY(r›7ω•­GbΌ₯jψ&Μ#.‚V[X4Z6όΰυH)pŠθσΒσ”’γ‹ύ]αVξΧ‹ηJQνοΙ%Ή‘Β›½ρƒ™qύυΜΕx1»Ω†bΥυPNӘvγU ˆ΅ΰήͺΊ‘₯‘‚β]’’ϊ%σέoΔϊμ|,EU[&hWΰ/,ω/F‹βΔ£Ά^ζ½< –t Ώΐκ€ι£ή{ΎAΊ +]1Φόͺ%FόŒ ½œpV²ήG+c9U•l[qμBͺυ+0mI SΞT_Ԍ=0WίτŒ@TυΡ ₯_cΡu7ΒƒPΛ°ΗYau°0©8κ«{«ωziŒγŸt‘φχ:am-€0jξ}WΚ†5¦΅BωΕL•άRώR!€)`Ωζ;³0‚.½RLt> ΦΌ:”œ²Ω›’Κ#V5|\‹Β¨>ή+΅ΏΗπΕVΟnφF)εCS@Υϊc’Œ_μί΅μ}«`)_…s†Ώ€$u‘LXό³\mG…α*λ=€Qα?–γ!0uvϊεξ₯Β#”ξͺPCHθc΅φŽΟ―!γ₯%eΑ-ΏmBaY€ΧΧάΙ8΄μω¦b  —bΊa,˜Ϊ’EΫa'©3?―:{Hω£ΆNΗZΨ]χές‘΄ώ‘“#ΜΊΛΊ›΅(€#ω/U "K«ΑφRͺ7.š"fΒS €ήΜG<9σ€εL•ΞNΏœTš1&˜-ͺ»w‡D§¬8πό’ΓPDΨ“4O?S½°xyƒΔΑ DDρθ―ΐΫZ˜Q|MμŒR^_Α~†Η°ςΥξ˜{€-Cgή³:{ͺ΅Ϋ:Trp§ͺX{8OΖ± w!Hi ‚NrΨW]ύ +c1( ;²:"Ό6˜ω»nx%“Σ@ηS²ϊ*΅ΉœxΕΔΧEέ«ύ1|9γ…'Κ t]Νΰ+»ϋΖ7Ά‹>κKBϝ^*ΌΞdΔJI]Δ’΅ΣM‚ήS +WNΊγ’₯ΰ^=rbπ_)PG*ΟCΛΨί“DΩqg…ηjY_ʟ/¦IσΟΨ΄‘ 6<ΰδ—pSXXVτώ^ΉΛΗ§LάJΟbsHJ,\BΊ»φΜgπ €Σ‡2P`~{€`uŽ+»Εόρ°ΞΥώ˜ΪΎ ε:ϋ_`₯ŠJ+‘iG@@†ΐψ›`¨H,lb΄ “0Θ“ni Ά}ΟBΏXηUΕΣίhIΧχΚίU»/O¬PάT©ΐjΦ‘Α"XΓHΗΡο/έx>  vπΊL*^N‘ε΅%ήJ‹O ŒiΤΌίcπ­Ο‹6‚πχςωςhy½b9 ϋF·0¬ΊϊeCaŠ:¦ …FήK―Pμ•χfόβSΛ ·ύ~‡ΞΥώW{5ΙΏͺώ.€Ά†D'#rk QŸ#τ +Ν!€=…κΡ“ŽΘ΅uxΡε±qζΦbρO;Ώ’{‰63VΰʘhN”QύBΐeύ1Mφ{Δχ4§Bψ“Ω§Χζ7ΐEψώ@ #ΉΛ°_ƒk˜•BΫ•€Ώ±²6' +4˜FΨ>υΧ}ύ©΄΅y-›‰uS»rXω’Γ^·ςύιz*M›aϋΖPϋc‚#Άg•φBe‹!pο·Ν>§Ό0‘7j0Ε%—Π‡mΧfεΜη‡ +Œ°ςwΥρΛ‘―8ΠEc"“ΉΑβϋoϋXCB‰Š*7pξ/7|It?(ξhLΥ·π?“KΎ¦0%ΆOέwhQ΄πv?7O=¦˜ϋ°άŒς,@„CJ‰κG{¨7+R―jΟF‹°8!δJxχX»eCAtm¬]c-DΕUΨu€ή§γšАτˆvXyδ₯όΦ­ͺ +-wΗΟ·ώύφrύ-ώ8]y]Mg–?Κ_Ο€ΒkΙΰu +pΚiWfκ δπΕ+ο?™R¨ημ­n +2kjΖNw+XΚ€\D•αU=r3 Ίΰρ-WΓk6ίW{su©ωA₯:3ΌΗ}Σl[QΑ΅„€MZΥjΠ‚‘ω‘ΰ#pθέt;ΌtCcR΄Τi”-λφβX½ +_o,&“e{½Ν'`†L.[Ί‘ΖΙ‡Χœx'@αF/Daάvτ8!DΐJ^Š-K|#Τm½{εα—nΤ 2ΏΆ‹p―XΆε.¬νaXΦ‹¬ψ$ξŒΟweλUψΓμ3/’ˆ$ξ0”]Π°…Εΐ5ό“ٍ£˜Β EΨ\ξΏε#ΝM `Tλ8ϊƒ¦]ΒnΫ"Ω ΓΠΫ#`Ί-4*θ8λΎ9‘6]ό»f΄—{ΘηKχ]˜*8¬#sςλρɐӴή¬MΣ£ϊ‰{‘6i?ψ0 ^‰άtv Lλ=u±μΫΎΠ<ύDέΔ=0ΌDI +WνΚiK―^Ÿ·,Y{₯#£!Ά½ +Τkw©ΡΩ<ŒΦŸ©Ύ„!ΐހ޻#le™}²wJICΰWŠυΊώGπ2ƒ5&U— VδE―Βξ Ω†ΣΖΪΨ”Ž 9 δDιUώ·›Μ>η΄4„!0DBΤ«όoK Ÿ_W:#B€ψg:²t)1Zτu%ιˆ B`† +u)w–Ωη‚Ζ¦ „!@ΜAΰ7›Jτ'Ώ£"”'ΆΟ9ΝL‡„!@,@ΰoτκΑλT”.„!@ΜCΰ_v/ΡΩϊί¦*aΤ:―’tB„!°Ϊ₯7ω"λ΅€ „!@žό­ξτ?-)až•€sB€ ό§ρR=ι.ސΩη‚6¦ „!@xCΰγα"=Ι‰βXo΅€k„!@ž|»S?a·­άa!³OΟ&¦sB€ Ό"ps]²nΦ'λΘμΣk#ΣEB€ Ό °© FςS•iadφι₯‰ι!@^ȏ²λCώ?Ϋ‘ε΅‚t‘ Bΐe³L€“ΘμΣW ΣuB€ Ό#πD{¦Φ?>.τ^7ΊJ„!ΰ‘Όh­ΛΝ…dφ黁ι!@>€Ν$β€kw +€ ³έbφQ9ΊL„! „ΐƒ­ιΪ•7Υ& Սξ„!@ψF 6!T£ςό₯)‘dφι»iι!@‹!πΖ@Ύ§€ϋ[«έ'B€B )9Lsς_wW¦ΥΏP«=B€ ˜ψižΆ¦Zό3΅+%"B`1Š£XQke +ψ»νερ~±:Ρ}B€ &Ž/Σ άh~4S•(!@„6ΞόωX±ϊ?ΎΏ&—‘6”„ Bΐ ϊǝͺvai~όhQJJ„39Q_ΜT©σ+KB™«B B€ όCΰ„*7ώ4]΅‘ ΖΏšPjB€ ?Έ΅!EUŸψ$™.‹χ³”œ B@ g›ΣT2πΣ~τ !@b«&,BƒΎx!©}ΔΆ"=G„€Xz³"ƒhτΧeυI΄α+Άρθ9B€ C "ΞΏ€Χϊσ(ͺ{`MGO„!(p ƒQΠ¦"ˆΐΗdi…u ΄ΩθyB€ $BrΣΔύqΊς©U™Dμ)Q“Q6„!@Hˆ@KJΨ›2Δ ΐFσwΊ³Kc•²"B€ΊΔΠ‡ZΣ§±ƒϋDά ς£μ’’2$B€ žην™Ή₯Μ_gΏΨX5Y‘VŽ4ύ2΅eK„€FΫ7ĜͺKy‘+ϋΣΡ’8^ϊϋˆ)ώίm+νxΙ…‘Βg;²_š4”nS’LτB€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B€ B@Π2»8 +endstream +endobj +650 0 obj +<< /Ascent 435 /CapHeight 435 /CharSet (/u1D456) /Descent -11 /Flags 4 /FontBBox [ -342 -238 1247 786 ] /FontFile 756 0 R /FontName /XKEAZI+LibertineMathMI5 /ItalicAngle -12 /StemV 82 /Type /FontDescriptor /XHeight 400 >> +endobj +651 0 obj +<< /Filter /FlateDecode /Length 590 >> +stream +xΪ…TΛjγ@Όλ+fη xFoc°^`Ψ<ˆΝ²W[;[zόχΫ5²lΎ“ΝίεAΆ}YΙη}ρΌqΝΓPž)79Κve&ϊLFΗμ»c¦.ϊ#Ϋ¬«'&9η€UΧ Ω‹›PΆ˜€ΛͺhojΩΪ a±’Μϋ[¦žω…άBρφΪυς²©Ž΅±\²Ε;v}{UΊŒΕk[ΘΆ¬NlώH"m‡¦9KbάX­X!t7ωτ²ΏHΆψΑ;{wm$³T.F­y]ΘΩη²έW'i,9_±e–­ Y_Ξ„3–ŽΧ&.χρ°©Ž€ρz<)βŒO"J„@X`b'€GΨnہ.DΈbΣ LΧθj0ˆrΧΧ`ΧyΦfJ›‡ΞΎ­AΘρΑτΧχς5Ίϋΐψ.‰* āšf# @Uhίςk2Ζς&£ς}{σ”σHY„©Έѝ[*Ž\ΔΡθΪq<Ζ1βDΕΚΑ•oδ8ΕbŒSΔ–Št +wτχ;z°=œHη0Ο‰uŽ^N’σ yͺ}Θ3«Χθ|ίΡ³ω£pοS|β$ϊmΘSν9ζ B­ύBu?·0sθŽq|s^9o[w_Œ|h[Ϊ΅šjπιӊܷ·©T©ŸZϋι_Ωkfό‰_μ +endstream +endobj +652 0 obj +[ 354 ] +endobj +653 0 obj +<< /Differences [ 46 /period 48 /zero 50 /two /three 53 /five 61 /equal 65 /A 68 /D 70 /F 72 /H /I 76 /L 78 /N 80 /P 83 /S /T 86 /V /W 95 /underscore 97 /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p 114 /r /s /t /u 119 /w /x /y ] /Type /Encoding >> +endobj +654 0 obj +<< /Ascent 658 /CapHeight 630 /CharSet (/A/D/F/H/I/L/N/P/S/T/V/W/a/b/c/d/e/equal/f/five/g/h/i/j/k/l/m/n/o/p/period/r/s/t/three/two/u/underscore/w/x/y/zero) /Descent -174 /Flags 4 /FontBBox [ 0 -177 509 835 ] /FontFile 757 0 R /FontName /ZGUGQH+Inconsolatazi4-Regular /ItalicAngle 0 /StemV 72 /Type /FontDescriptor /XHeight 457 >> +endobj +655 0 obj +<< /Filter /FlateDecode /Length 843 >> +stream +xΪmUMoβ0½ηWx•ΪΕNΘW…œ„HΆ­ +Zν•&¦‹TΰΠΏ~3ΪφzΏ™yσœ87?žΧΫφ―nέkυβNύehά€όΉ=77Uί\;?:ΧΊvά==¨η‘oΦξ¬nΛU΅κφη;O^uΝϋ₯u#λ€Β½ν»O +ϊ¨Ϋϋ=٘‰a³?ΏϋkLy 6FΡζ/7œφ}χ Μ½ΦΪ–][φH<Si£¦cγέΎkι₯^Ρ90‘jχΝYVτίό¬H^œΞξ°κv}0Ÿ«ι‹ί<‡rLŸ†Φ ϋξΝ―_/Ηγ»Ck₯ƒΕB΅nη«ψy·§¦WύΧψζγθTHkΓύ›Ύu§γΆqΓΆ{sΑ\λ…šΧυ"p]ϋϞќςΊΉKΟΥ΅ u”/‚ΉA² )`JbD>`΄φΨ2γš™$`€TY'`”(ZqŠΗΑΌBJŌ +)Ǩ%553<Ζ,£θ(‡hώl™ΧwBš6„‹0¦Πa™G„+L€gΔ±θ«cŽWΐ c œrn +œqœψ9ηΦΐ–γ°Mά—8%Η ΰŠCMq.β†5„Sβhr›κ›AƒαϊI‚Φε皎­ϊ\SεώΘ©ΏΗΐ α]8 ι`Y‡7ь1OΚyeδ΅ρΦzlΓλ,d mYΔΈ”S£SJfί-›1i‰:C&e c4ΞRΖΔΙΨˆΛΔ$D&™ Λ +Ζ&+ΓΌ¬bLυΙγaΙjΖ ηΑbτΝy°όœ£‡+ηΑbθΙYBΉό‘ώœυ§Δgύ ρYJυYŠYr֟b–œυ§x(rΦΑθœυGT“υΜ›ΛΑ`F+ƒΩ­L ,C9τ²β?d+ώ£―‘͊Δ1£1—‘ӊπĊ˜ΧŠT_ό‡~+ώCg!ώ£o!ώƒ_ˆΰβ?τβ?εŠΔ‰/ώ?γ«„°ψY +ρ³β?^ŒBό‡ŸΏ\–jς‹UPρœŠ{Επ‘βxᇻLφσ^U}9pQγσq½χ›Λ0ψO}cθΦΗ}Ώοάυ3tμΘ’}ΏΖ!VOuπΚρΛ· +endstream +endobj +656 0 obj +[ 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 ] +endobj +657 0 obj +<< /CreationDate (D:20250604140153Z) /Creator (Matplotlib v3.10.3, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.3) >> +endobj +658 0 obj +<< /BaseFont 758 0 R /Encoding 759 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 761 0 R >> +endobj +659 0 obj +<< /BaseFont 762 0 R /Encoding 763 0 R /FirstChar 0 /FontDescriptor 764 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 765 0 R >> +endobj +660 0 obj +<< /BaseFont 766 0 R /Encoding 767 0 R /FirstChar 0 /FontDescriptor 768 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 769 0 R >> +endobj +661 0 obj +<< /BaseFont 770 0 R /Encoding 771 0 R /FirstChar 0 /FontDescriptor 772 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 773 0 R >> +endobj +662 0 obj +<< /BaseFont 774 0 R /Encoding 775 0 R /FirstChar 0 /FontDescriptor 776 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 777 0 R >> +endobj +663 0 obj +<< /CreationDate (D:20250516111417Z) /Creator (Matplotlib v3.10.3, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.3) >> +endobj +664 0 obj +<< /BaseFont 778 0 R /Encoding 779 0 R /FirstChar 0 /FontDescriptor 780 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 781 0 R >> +endobj +665 0 obj +<< /BaseFont 762 0 R /Encoding 782 0 R /FirstChar 0 /FontDescriptor 764 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 783 0 R >> +endobj +666 0 obj +<< /BaseFont 758 0 R /Encoding 784 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 785 0 R >> +endobj +667 0 obj +<< /CreationDate (D:20250516003513Z) /Creator (Matplotlib v3.10.3, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.3) >> +endobj +668 0 obj +<< /BaseFont 778 0 R /Encoding 786 0 R /FirstChar 0 /FontDescriptor 780 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 787 0 R >> +endobj +669 0 obj +<< /BaseFont 762 0 R /Encoding 788 0 R /FirstChar 0 /FontDescriptor 764 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 789 0 R >> +endobj +670 0 obj +<< /BaseFont 758 0 R /Encoding 790 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 791 0 R >> +endobj +671 0 obj +<< /CreationDate (D:20250515122242Z) /Creator (Matplotlib v3.10.3, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.3) >> +endobj +672 0 obj +<< /BaseFont 758 0 R /Encoding 792 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 793 0 R >> +endobj +673 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 206.37436 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 345 >> +stream +xœuR9rΔ0 λω +}@ΡΦΩ¦Ω>HΨΝ6ω~PmΖŽEΘ”£Ί^Gi+•Ÿ/σΔηύ“ +žqς}₯oσ£₯_³|υ„χ1/3έΘ\r ΘΞ£"ίp βΗ™«ε˜@.€œ' Μy!N€ƒAy) ­δLKμ§Ux¬‘΄υEΒ%Ωv‚vy¨zg ]ͺ­!7gˆzeΩU’m€eφu‘¦—4•X¬H’ύΔ•ι!9@6$Ψ+(Η +A~WΙuέθ»GΆ(±NQ%Ɓ?#ΏxŽKΒuσ6²ε ›xocΜQ€΄UΕmό'GΉͺT5ί†–²ΪBOθ *δ=υ) Fή#Σ΄47TΙ{Ț―&Ί©“χZ΄νϋ!?ΆWu³«ΕX΅Ά\Γ7ςEήξcΤπΌ”·£ΒL5|#ενΒ0ΰΖΉ˜μ€uo³ϋ!σ•ω +endstream +endobj +674 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 206.37436 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 564 >> +stream +xœU”=S1 „{ŸΒπΚvKCΟ€ζ₯‘αϊH»2†IΖ“·r΄ϊω’ό΄΅juχσ3k²€Ξa₯Ν:ϊΞ5~ϋJ΄Χ―Ÿ1λkV{ϊ„ςŒψ;„Τ$κϋτ(yΕW•φ¨f‡ͺλιͺ}°HyjΔρ =?K#M4R—>αЈi%[ά"’phΔςlKi•!Έ‘½βψ„‘υn΄-Ϋn؎ͺiw‘k«Aΰ:†jkΡ΄t+»ΓtLΝ²„¦EYΙqAΨV,₯κ•Uh95Ω„‘tM97 νs‡ΰ†xΦ"ΜΔrΝ3ψ'pδo°xβ%Δqσ –-1%²!ομL,ή¨β φΔrQ)j~ƒΆ”Π–φ€©`Ι€ή1 ˜Gς‘aZ˜›Μ%ω1_LZλ6ŸδkΑF° X?–_·ΧqSΠ"W-wr.’ΣA0:ΉKΙ‰"Lά€ΏδΐFpšιT—‘ϋώΗpr₯/8ΓTΓ0q†©†Q˜3Lε0Œfœa*„Έ/BlŸΕ‘SLε`Œ©:ΖTΗX„sLε€Œέ9ΘTH²d’lŸeq AΫΎ,‹S άζ…YrπΦ/ΝβΏ WηYΙsvœgΏ8σ~+rifFόTφ…™ώ–z•Λ2«ΥVV'Ιθ +ν.Ή {ΠZ—cŸFΊΛΕΨgŒ%μv)φ­θΆbμ‹έλ2μ› +dχ0μt²{v’€ΩύΛ0)τ?Ϋ_ΒqΤ' +endstream +endobj +675 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 206.37436 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 313 >> +stream +xœ-’;r!DsNΑLρ€N”ϋΆ)Qβλ»η ΅[4ΌYu ˜sj{Χ6κιE‹Ίvo₯­:βΧ#”hΟϋ7f=«ΪϋώŽ?‘€Bψh3κ} 6Š΄$ΆS7jβΔ*%ΥΨsΪͺ NT©[«ή΄PΕΔ‰U¦ϊH–8±Š8Nς©NŠκ#“Œ8Q₯yŽJ2βΔ*S«1HFœX₯Λa,’'V©qfQ 69*ΟJ0_2œX nσΉEΆύv"ΐ3pς― !keluΒΖΦ$ +qG3_‡ ΔσΝzb·ΪΚ6S μŠνξIrχΟνMrOŒ#=…0δž1—pqΘ½]ځ‰˜/{Όq{Σ΄BΙή7·;hŸ’½o\oGΡr%{ίΈή.lΖέiG:6|†ύη“Ώ +endstream +endobj +676 0 obj +<< /CreationDate (D:20250516081347Z) /Creator (Matplotlib v3.10.3, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.3) >> +endobj +677 0 obj +<< /BaseFont 758 0 R /Encoding 794 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 795 0 R >> +endobj +678 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 142.22867 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 345 >> +stream +xœuR9rΔ0 λω +}@ΡΦΩ¦Ω>HΨΝ6ω~PmΖŽEΘ”£Ί^Gi+•Ÿ/σΔηύ“ +žqς}₯oσ£₯_³|υ„χ1/3έΘ\r ΘΞ£"ίp βΗ™«ε˜@.€œ' Μy!N€ƒAy) ­δLKμ§Ux¬‘΄υEΒ%Ωv‚vy¨zg ]ͺ­!7gˆzeΩU’m€eφu‘¦—4•X¬H’ύΔ•ι!9@6$Ψ+(Η +A~WΙuέθ»GΆ(±NQ%Ɓ?#ΏxŽKΒuσ6²ε ›xocΜQ€΄UΕmό'GΉͺT5ί†–²ΪBOθ *δ=υ) Fή#Σ΄47TΙ{Ț―&Ί©“χZ΄νϋ!?ΆWu³«ΕX΅Ά\Γ7ςEήξcΤπΌ”·£ΒL5|#ενΒ0ΰΖΉ˜μ€uo³ϋ!σ•ω +endstream +endobj +679 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 142.22867 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 564 >> +stream +xœU”=S1 „{ŸΒπΚvKCΟ€ζ₯‘αϊH»2†IΖ“·r΄ϊω’ό΄΅juχσ3k²€Ξa₯Ν:ϊΞ5~ϋJ΄Χ―Ÿ1λkV{ϊ„ςŒψ;„Τ$κϋτ(yΕW•φ¨f‡ͺλιͺ}°HyjΔρ =?K#M4R—>αЈi%[ά"’phΔςlKi•!Έ‘½βψ„‘υn΄-Ϋn؎ͺiw‘k«Aΰ:†jkΡ΄t+»ΓtLΝ²„¦EYΙqAΨV,₯κ•Uh95Ω„‘tM97 νs‡ΰ†xΦ"ΜΔrΝ3ψ'pδo°xβ%Δqσ –-1%²!ομL,ή¨β φΔrQ)j~ƒΆ”Π–φ€©`Ι€ή1 ˜Gς‘aZ˜›Μ%ω1_LZλ6ŸδkΑF° X?–_·ΧqSΠ"W-wr.’ΣA0:ΉKΙ‰"Lά€ΏδΐFpšιT—‘ϋώΗpr₯/8ΓTΓ0q†©†Q˜3Lε0Œfœa*„Έ/BlŸΕ‘SLε`Œ©:ΖTΗX„sLε€Œέ9ΘTH²d’lŸeq AΫΎ,‹S άζ…YrπΦ/ΝβΏ WηYΙsvœgΏ8σ~+rifFόTφ…™ώ–z•Λ2«ΥVV'Ιθ +ν.Ή {ΠZ—cŸFΊΛΕΨgŒ%μv)φ­θΆbμ‹έλ2μ› +dχ0μt²{v’€ΩύΛ0)τ?Ϋ_ΒqΤ' +endstream +endobj +680 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 142.22867 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 313 >> +stream +xœ-’;r!DsNΑLρ€N”ϋΆ)Qβλ»η ΅[4ΌYu ˜sj{Χ6κιE‹Ίvo₯­:βΧ#”hΟϋ7f=«ΪϋώŽ?‘€Bψh3κ} 6Š΄$ΆS7jβΔ*%ΥΨsΪͺ NT©[«ή΄PΕΔ‰U¦ϊH–8±Š8Nς©NŠκ#“Œ8Q₯yŽJ2βΔ*S«1HFœX₯Λa,’'V©qfQ 69*ΟJ0_2œX nσΉEΆύv"ΐ3pς― !keluΒΖΦ$ +qG3_‡ ΔσΝzb·ΪΚ6S μŠνξIrχΟνMrOŒ#=…0δž1—pqΘ½]ځ‰˜/{Όq{Σ΄BΙή7·;hŸ’½o\oGΡr%{ίΈή.lΖέiG:6|†ύη“Ώ +endstream +endobj +681 0 obj +<< /CreationDate (D:20250515122158Z) /Creator (Matplotlib v3.10.3, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.3) >> +endobj +682 0 obj +<< /BaseFont 758 0 R /Encoding 796 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 797 0 R >> +endobj +683 0 obj +<< /BaseFont 766 0 R /Encoding 798 0 R /FirstChar 0 /FontDescriptor 768 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 799 0 R >> +endobj +684 0 obj +<< /BaseFont 800 0 R /Encoding 801 0 R /FirstChar 0 /FontDescriptor 802 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 803 0 R >> +endobj +685 0 obj +<< /BaseFont 804 0 R /Encoding 805 0 R /FirstChar 0 /FontDescriptor 806 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 807 0 R >> +endobj +686 0 obj +<< /BaseFont 808 0 R /Encoding 809 0 R /FirstChar 0 /FontDescriptor 810 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 811 0 R >> +endobj +687 0 obj +<< /BBox [ -9 -9 9 9 ] /Filter /FlateDecode /Subtype /Form /Type /XObject /Length 132 >> +stream +xœm1Δ0{^ΑΦΒ6>“6eΎ‘&Š”·ηθNˆ(nυ°ω$αF€ςE9ΙG,—;+IΊΥΌ0jjέZm£dΕ΄t†Ώvς.ό‡2œ€,C€μ…@ϋΛ¨ήtωƒ¨Cgΰ­Η ‹χtoψΉE\“1½&'Γ“ΫψA΄ EGb +endstream +endobj +688 0 obj +<< /BBox [ -9 -9 9 9 ] /Filter /FlateDecode /Subtype /Form /Type /XObject /Length 33 >> +stream +xœ3PΘβ2Pπb…\.] Κα‚R\\N\l +endstream +endobj +689 0 obj +<< /BBox [ -10.303301 -10.303301 10.303301 10.303301 ] /Filter /FlateDecode /Subtype /Form /Type /XObject /Length 49 >> +stream +xœ3PΘβ2Pπβ5PΠ5Υ360660TΘε‚3 r€ςpn—.ŠT—²‘ … +endstream +endobj +690 0 obj +<< /BBox [ -10.706339 -9.854102 10.706339 11 ] /Filter /FlateDecode /Subtype /Form /Type /XObject /Length 100 >> +stream +xœmŽ;€@D{NΑ–ΐΐώZKObŒήΏ;MΆ!aζ1ψ$εr4Ύ©˜xtΑ&£†)ψ’R₯ksŸ?b}6 .šφ€Ζ«ΊT΄nΰV9qΜχ Χ%³Œ[―><ˆ6zhε&Τ +endstream +endobj +691 0 obj +<< /CreationDate (D:20250513045445Z) /Creator (Matplotlib v3.10.1, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.1) >> +endobj +692 0 obj +<< /BaseFont 812 0 R /Encoding 813 0 R /FirstChar 0 /FontDescriptor 814 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 815 0 R >> +endobj +693 0 obj +<< /BaseFont 816 0 R /Encoding 817 0 R /FirstChar 0 /FontDescriptor 818 0 R /LastChar 127 /Subtype /Type1 /Type /Font /Widths 819 0 R >> +endobj +694 0 obj +<< /BaseFont 758 0 R /Encoding 820 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 821 0 R >> +endobj +695 0 obj +<< /CreationDate (D:20250515122205Z) /Creator (Matplotlib v3.10.3, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.3) >> +endobj +696 0 obj +<< /BaseFont 758 0 R /Encoding 822 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 823 0 R >> +endobj +697 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 163.62 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 343 >> +stream +xœuR;rΕ λχ\`ΎmšΧηIάΌ&׏€₯ΝΨ³cVXڏUs½ŽΤVH>_–ŸχOHxFαϋώ +ίVΒ―YΌ{ΐϋBN3ldξ 9dηU‘o8ΙW +ΥtM 7@ΚΔIs9'Α „<‹”Vr†%φ’*<ΦΠDX·ΛζEΒ%ΩV@»²«ζΞΊT[CnNΝ•eW‰Ά–Ω]4ƒˆ4=…©ΔbE’μWfvΙ²!Α^A9– ς»JλF?=²E‰ur*1Žϋ1ψ6βΡ/ ΧΝmd‹N)6ρncŒ^€΄UΕ6ώ½\Uͺš·‘₯¨ΆΠ:C‚ +ρ @½k +¨ΟΘ4-Ν Uβ²ζ«I£nκΔ³mD»ΐ~ȏνUέμjΡW­-Wχ|;άΥ}#/Εγ(7SuίΘρΈΠ x»qnζ=;iέmφaτŸ•— +endstream +endobj +698 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 163.62 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 312 >> +stream +xœ-’;r!DsNΑLρ€N”ϋΆ)Qβλ»η ΅[4ΌYu ˜sj{Χ6κιE‹Ίvo₯­:βΧ#”hΟϋ7f=«ΪϋώŽ?‘ΖΏ>ڌz_AƒΝŸ"-‰νԍš8±JI5φœΆ*ˆUκΦͺ7-T1qb•)‡>’e N¬"Ž“|ͺ“’ϊΘ$#NT)Gž£’Œ8±ΚΤj ’'Vιr‹dΔ‰UjœYTƒΝEŽΚ³ŒΔ— g'ˆΫ|.B€ν@Ώ‡Hπ œϋ+hΘZ[0ΔΏ1‡5‰BάΡΜΧ!ρ|³ή…Δ­Ά²ΝT»b»{„άύs@{…άγHO! ΉgΜ%œFroE—v`"ζΛΕoδή4­P²χλνΪ§dοΧΫQ΄\Ιή7· ›qwΪ‘Ž ŸαΥ“] +endstream +endobj +699 0 obj +<< /CreationDate (D:20250515122206Z) /Creator (Matplotlib v3.10.3, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.3) >> +endobj +700 0 obj +<< /BaseFont 758 0 R /Encoding 824 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 825 0 R >> +endobj +701 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 163.62 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 343 >> +stream +xœuR;rΕ λχ\`ΎmšΧηIάΌ&׏€₯ΝΨ³cVXڏUs½ŽΤVH>_–ŸχOHxFαϋώ +ίVΒ―YΌ{ΐϋBN3ldξ 9dηU‘o8ΙW +ΥtM 7@ΚΔIs9'Α „<‹”Vr†%φ’*<ΦΠDX·ΛζEΒ%ΩV@»²«ζΞΊT[CnNΝ•eW‰Ά–Ω]4ƒˆ4=…©ΔbE’μWfvΙ²!Α^A9– ς»JλF?=²E‰ur*1Žϋ1ψ6βΡ/ ΧΝmd‹N)6ρncŒ^€΄UΕ6ώ½\Uͺš·‘₯¨ΆΠ:C‚ +ρ @½k +¨ΟΘ4-Ν Uβ²ζ«I£nκΔ³mD»ΐ~ȏνUέμjΡW­-Wχ|;άΥ}#/Εγ(7SuίΘρΈΠ x»qnζ=;iέmφaτŸ•— +endstream +endobj +702 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 163.62 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 312 >> +stream +xœ-’;r!DsNΑLρ€N”ϋΆ)Qβλ»η ΅[4ΌYu ˜sj{Χ6κιE‹Ίvo₯­:βΧ#”hΟϋ7f=«ΪϋώŽ?‘ΖΏ>ڌz_AƒΝŸ"-‰νԍš8±JI5φœΆ*ˆUκΦͺ7-T1qb•)‡>’e N¬"Ž“|ͺ“’ϊΘ$#NT)Gž£’Œ8±ΚΤj ’'Vιr‹dΔ‰UjœYTƒΝEŽΚ³ŒΔ— g'ˆΫ|.B€ν@Ώ‡Hπ œϋ+hΘZ[0ΔΏ1‡5‰BάΡΜΧ!ρ|³ή…Δ­Ά²ΝT»b»{„άύs@{…άγHO! ΉgΜ%œFroE—v`"ζΛΕoδή4­P²χλνΪ§dοΧΫQ΄\Ιή7· ›qwΪ‘Ž ŸαΥ“] +endstream +endobj +703 0 obj +<< /CreationDate (D:20250516081045Z) /Creator (Matplotlib v3.10.3, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.3) >> +endobj +704 0 obj +<< /BaseFont 758 0 R /Encoding 826 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 827 0 R >> +endobj +705 0 obj +<< /BBox [ -8 -8 8 8 ] /Filter /FlateDecode /Subtype /Form /Type /XObject /Length 132 >> +stream +xœmA„ Eχ=E/πIKEε +n=‚›Ι$ή;ΔΤMνΛγε/ oT +ŒO’°δ4K7 )­ΛĈa^-‹r Υbν\‡ƒϊ 7F·KaJmŽ7Ί™nγ=ω£›bτΓαπJΈg1dΒ•cpψ½π²=όαιE ό!Ϊιc|D +endstream +endobj +706 0 obj +<< /BBox [ -8 -8 8 8 ] /Filter /FlateDecode /Subtype /Form /Type /XObject /Length 132 >> +stream +xœmA„ Eχ=E/πIKEε +n=‚›Ι$ή;ΔΤMνΛγε/ oT +ŒO’°δ4K7 )­ΛĈa^-‹r Υbν\‡ƒϊ 7F·KaJmŽ7Ί™nγ=ω£›bτΓαπJΈg1dΒ•cpψ½π²=όαιE ό!Ϊιc|D +endstream +endobj +707 0 obj +<< /BBox [ -8 -8 8 8 ] /Filter /FlateDecode /Subtype /Form /Type /XObject /Length 132 >> +stream +xœmA„ Eχ=E/πIKEε +n=‚›Ι$ή;ΔΤMνΛγε/ oT +ŒO’°δ4K7 )­ΛĈa^-‹r Υbν\‡ƒϊ 7F·KaJmŽ7Ί™nγ=ω£›bτΓαπJΈg1dΒ•cpψ½π²=όαιE ό!Ϊιc|D +endstream +endobj +708 0 obj +<< /BBox [ -8 -8 8 8 ] /Filter /FlateDecode /Subtype /Form /Type /XObject /Length 132 >> +stream +xœmA„ Eχ=E/πIKEε +n=‚›Ι$ή;ΔΤMνΛγε/ oT +ŒO’°δ4K7 )­ΛĈa^-‹r Υbν\‡ƒϊ 7F·KaJmŽ7Ί™nγ=ω£›bτΓαπJΈg1dΒ•cpψ½π²=όαιE ό!Ϊιc|D +endstream +endobj +709 0 obj +<< /CreationDate (D:20250516055607Z) /Creator (Matplotlib v3.10.3, https://matplotlib.org) /Producer (Matplotlib pdf backend v3.10.3) >> +endobj +710 0 obj +<< /BaseFont 758 0 R /Encoding 828 0 R /FirstChar 0 /FontDescriptor 760 0 R /LastChar 255 /Subtype /Type1 /Type /Font /Widths 829 0 R >> +endobj +711 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 119.88684 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 343 >> +stream +xœuR9rΔ0 λω +}@ΙΊΫ4ΫηIΉΩ&ίͺΝΨΓYZ€€šσθu€ΆBϊχγσe9πy„„gά|ί_αψ―Y,=ΰ} %§6rc:― +ΌαƒLΎξ ςXMΧSπζžψRC,'ςd:TΐPgQ’`3,©ί¬Κc C„Uά6/ +.ΩΆ²+»kξμ‘Λ΅5`sΊil»Κ΄ ¨Μξ¦B”ι)L‹Ι²ί82³[ˆ φ +ɱܐΏ«μΊNτ3#G”Y§Φ¨2γΊσ…o#ύxάF΅θ’R“ξ6Φθ Θ[]lγ’·«NΥσ6Œ5fΒdθΟ4»Ά‚ρ¬LΫή@Π%ž%kΏΪ4ϊ¦O<Χ’Ρέ€ΰ<ΤΗνUμΡ―Z·\=7ΚE<ιπ`Tύ²O’> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 563 >> +stream +xœU”;Ϋ0 D{žB †_lΣ€Ο’4r“&Χ° …ΙΨΓ±4Ÿg—»«Ά>Ϋυ*χjuŠJ[Σj_mŽ]Ϊυνkͺ—Ώ~ύΌŠ½Vσχ―οΧΣ§”»\φώ$;jΡλ1₯ί¦ωaͺήΓτi©w»p|(·Z€ΫƒEšΪ‹ΈV‹Η="–‡E<Οφ”0’^Ω[΅(ŽOšΕ.νNΫΊ=α†νl–vWΊVρsš¦JΣ:ΌμΣΉ,‹ +M«%ς4R.…°½"XJ³+ZiΉ,Ω‚‘ KΉ6 ύσ€ΰ†Dή"ΜΔs­3χ'qΰOςxζ%ΔqσIž-3%²!ο“όΜ,ή¨βIώΜrQ)j~’΅”Ρ–υd™ΰ9€ή1 ΈGŽ‘aZ˜›ά%ǐ1_LΪκvŸkΑF° x?žίΆ7pSΠ"W-r.rΠA0ΉK9ˆ"Lƒά€ΏΐNpΊλTΥΡ}ώc8‡ί/ΓT^†a SyFaΑ0•—a4 S!ΔC ±~)ƈ‚b*/Ƙj`Lεε‹Ž©Ό cw2’,…$ϋη@YCΠΆΛΤ·u`–€ΌC³Δoΐ΅ΐy5ς\η5ΞΌƒίŠš™?•}`¦Ώ§ΦzXf΅ΦŠ’ŒΠΚ9ϊΗ€TΗ11ŒtΧƒqΜKΨύP[±₯m!ΔΨ»υ0› +dχe8θ>dχe8ˆrdχ/Γ€0ώla8}I/δΔ +endstream +endobj +713 0 obj +<< /BBox [ 0 0 72 72 ] /Filter /FlateDecode /Matrix [ 1 0 0 1 0 119.88684 ] /PaintType 1 /PatternType 1 /Resources << /Procsets [ /PDF /Text /ImageB /ImageC /ImageI ] >> /TilingType 1 /Type /Pattern /XStep 72 /YStep 72 /Length 311 >> +stream +xœ-’;r!DsNΑLρ€N”ϋΆ)Qβλ»η ΅[4ΌYu ˜sj{Χ6κιE‹Ίvo₯­:βΧ#”hΟϋ7f=«ΪϋώŽ?β!|΄υΎ‚›?EZΫ©5qb•’jμ9mU'ͺΤ­UoZ¨bβΔ*S}$Λ@œXE'ωT'Eυ‘IFœ¨RŽΓ?J“\ +endstream +endobj +714 0 obj +<< /Differences [ 27 /f_f 35 /numbersign 38 /ampersand 45 /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon 61 /equal 63 /question 65 /A /B /C /D /E /F /G /H /I /J 77 /M 79 /O /P 82 /R /S /T 88 /X 95 /underscore 97 /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z ] /Type /Encoding >> +endobj +715 0 obj +<< /Ascent 685 /CapHeight 657 /CharSet (/A/B/C/D/E/F/G/H/I/J/M/O/P/R/S/T/X/a/ampersand/b/c/colon/d/e/eight/equal/f/f_f/five/four/g/h/hyphen/i/j/k/l/m/n/nine/numbersign/o/one/p/period/q/question/r/s/seven/six/slash/t/three/two/u/underscore/v/w/x/y/z/zero) /Descent -237 /Flags 4 /FontBBox [ -1082 -268 6171 893 ] /FontFile 830 0 R /FontName /PNRYLF+LinBiolinumT /ItalicAngle 0 /StemV 80 /Type /FontDescriptor /XHeight 432 >> +endobj +716 0 obj +<< /Filter /FlateDecode /Length 881 >> +stream +xΪ•UMo£HΌσ+z‘2Ζέ`Ύ"ΛRFŠ΄“M’ΥάF6΄³H6xςο·λ½η¬΄ΪΓδ`«ϊQοuUΝέ§οΟ‘νΖƒ γ/ZύpσxZV_χ—ΰξΫλΩ Λ“sλnWηυ}Ϋg·¨ϋκ±~ϊε³'?νιΪΉλI₯{ν‡)ΨGέΏΈŸαύPφγ©η—p9ΓΕ„αa?»πtX†_Q²΄σߑƌ—~9ωޏ΄)ΟUΏΕU΄ΕŸnšϋqxPζ‹ΦΪvCWgd1+ρ£V7‡Η~θ&1₯°˜Hu}»ΘŠώΫ³ΝΟoσβΏΓq 6΅ϊα/ΞΛτFŽ>«oSη¦~xUχΏ₯Ψw<_/—“ƒ:₯ƒνVuξθ7ςΩ>νΟN­>Τ{λΛΫΕ©ˆΦ†]΄cηζΛΎuΣ~xuΑFλ­Ϊ4Ν6pCχŸk&ζ–ΓρΖέynό_€γbl šMDS‘ r!φ…Θ£ ΅ΗΎ{œ4\Θ}!#£Ι:££(P0v©y†ΗΑ¦£ζ–Œ™ΠT؁Ρ`hΓ- †6hi*. ₯©QΰΓώΝgέ|·ν'‰Θ‹Γ`m—k`ΧQ•Η„k„ ΧΔ±ΨF'\―SΖ8γή 8η:ρ ξm€-Χ‘΄.y_βT\/kΞ3MΜ½¨ΦPζ,cδ`xfŒ} ΟLSό°ήΧό™šΦΩϊΆζ΄γΧ Φς$ΐs„Œ’uDχ> +endobj +719 0 obj +<< /Filter /FlateDecode /Length 678 >> +stream +xΪ•TMo›@½σ+Ά‡HɁxYl ‘e‰#YJ“(Άͺή*Φ)’ ”ƒ}χΝ`§­zHΐΫ·σfί »{σεeγΖE½7/Ε«ικ‘͍›|έ5ΞΝMZηΓΙTύ“1…).³έƒxiλ|czq›¬ΣuUφw6x]εΗ‘0—¨iσVVο!XGάnΝwχ±¬Λ½iϋ²2Ϋ΅Ϋnίy»ίuΖ=ξϋκΗ0ΥgW"ΝΆμVώI₯°αβ£α‚ϊfΪ¬«αέK)-±ͺŠ€>‘)3 “K©‡²*Ϊ±:±G­Ž§DQζύ8’w~²έ…xsξzsZW‡ΪY,ΔδΥNv}{¦ΊξœΙs[˜Ά¬ήΔνGM[Ρfhš£A!εRζ`Χ²}~ڝŒ˜|²cWυφά‘hμq-y]˜Ωε¦έUoΖYHΉ‹,[:¦*ώ™›³bCΓ© gφ₯€’Kg+‹γΉ}Ii±%"1‘%4²k ‰†DϋDψhδӜΓbK$ VL$δλβ`¦.ŽςŸ»ΦzΧZ™„LͺT§δΡ‹9Sμgo«]x’c|`Εx{ΜzcψTSڏΕlΧ~,F΅Jy„ζ`^W!§Κ(Z1jΎ‚g?e-ςϋ+ζΡ΅)ρεŸΝΐ+φƒZfcψˆΤ”S‘•!i₯ZΦ +Ή. mHžΉΖˆϋΓg4em +0ήF£Šηœ>όDšx>£Ρ?jΉ'1zΌ΅Θ©#ήΘ£yχhΤ’5χ‡0χς€Ε€θOΖ8#‚ρAlύΝ‘ΏYό‡±~“†=3NίηHΛ50ΞΖΉq—Δ9ΒMq=ΌωΠΆφ\ΣuB‡ΗўαλΣΤ TτΠUuΉi1zΜί–ζ +endstream +endobj +720 0 obj +[ 633 ] +endobj +721 0 obj +<< /D (subsection.2.2) /S /GoTo >> +endobj +722 0 obj + +endobj +723 0 obj +<< /D (section.4) /S /GoTo >> +endobj +724 0 obj +<< /A 831 0 R /Next 725 0 R /Parent 608 0 R /Title 832 0 R >> +endobj +725 0 obj +<< /A 833 0 R /Parent 608 0 R /Prev 724 0 R /Title 834 0 R >> +endobj +726 0 obj +<< /A 835 0 R /Next 616 0 R /Parent 6 0 R /Prev 608 0 R /Title 836 0 R >> +endobj +727 0 obj + +endobj +728 0 obj +<< /D (subsection.8.2) /S /GoTo >> +endobj +729 0 obj + +endobj +730 0 obj +<< /D (section.6) /S /GoTo >> +endobj +731 0 obj +<< /A 837 0 R /Next 838 0 R /Parent 616 0 R /Title 839 0 R >> +endobj +732 0 obj +<< /A 840 0 R /Parent 616 0 R /Prev 841 0 R /Title 842 0 R >> +endobj +733 0 obj + +endobj +734 0 obj +<< /Filter /FlateDecode /Length1 885 /Length2 9111 /Length3 0 /Length 9745 >> +stream +xΪ}seXœ[–5 nΑέ +ww ξZh) p(RΈ,Έ»{ά5Έ»»»Λδvίξ™ιωžοyœ³Φήλ¬w -%P“UΒ b–…Ψ;±r²qœάμ¬@Ίn6NZZ-+'[πΏ0Z0ΤΡ +b/τ7+ƒœώά₯AN‚΄,j¦N.N'—§ ‡ €‹ƒλ߁¨@ΕΚΤΆhZ‚ `Z)ˆƒ;ΤΚΒIπ·φ_,9 2 +-@l†ώΡ6˜Έώr( Z€εΰδδΰ0X:99±³›‘Μ’ΨΝΩμΑNμŒή”±7“‚ΨفνQ89fV¬™€-¬μQΨ’rw8f`σήU@NP+7€'€γ―οί'Γ?φΝ φΆξ +²Ψ΅υΥu΅˜.ΩΏIIIˆΐ“•‡ƒΐΚΕΓ ΰδΰζπsΌ·dυ·ŽNV°7‡ώeΨΜΩα_¦]ώß1ώ·*ΔΙΚ `ψΟώgœ¬³­ν?œ3όΣςαAvVΆξΏˆΰΏΊ`P›Y9ΫύŸ|Νω7 +N [+S { ΫCV޲Vn`3 •“©%ΐdλώ'mo†ΪZΩƒG«ΏF +ΐΚΙΑρœ–₯•©=ΨΡΐϋO +loφdμM!fVφ.^> +Ή£pόι2//ΐ“`υGΙ vϋσ6;›=ΔιO +ΐΑΩΙπgxPώ*5'€όΩdϋόδ »³½ןaώΖΗρ7φηπφΏ,˜:C‘fξέϋγο_ws«?Eƒέΐ¦(σ3Sαλͺ€ζ»J WΦν‘χ“΄Ϋ’Y=η‘-Ξογ€ω―Bo$β:1—6eΕ(ž=jƒcΥ›½žŒ£5Ζ·›PζΖπ{Gσ%ͺ{ȐIY΅ΔwΌž?{ιψΩΐ6ΌiS€Νϊμ,π˜ƒsηΪ-ηVέS²84³­ΎσƒO υ©d‚5B;ό£_αmΆIϊ4!‚+φ™ΖΤυΝ$vζθ+…b43ŠχQwΎ§ώWδύ΄Ηr™—c; ‘>!μ5φΠ8§δ^‚"Α¬gQζnSG[΄+];=†Π’]μkΌτ*g\9±ρΈΉ< Η¦½«₯ +Ά—†οšΝš«φ]γNω―e-^‚ζΑ΄¦e›!Ώ* +9S‚;nυ ˜dΨ²¬“0|ή9©`5NFˆ&©ρGή;εΰyaΐ8‡.W-―˜ΫyπbQΤϊ₯™ε³c¦ˆ+ΜΛ#§}²Π<ξ^vuJψΛ²Q΄ϋ;_‘γ&/ugΣdXξρ)’˜44Τ„ΌΙώ&Μᐂυ“vΜΙvL"k<¨ρΘΦΊ “8ύEž„αDΙαaœ;nkΓ6Kο²7؟!lΘ§^"ΦM0-BQι[sJ{ …\gwΦ@~G€±ΡRδ]%qδ<ά ΫNΰ™υE/€mΙϊ#†―ψYΟdΦ'?ΦαΈO]Oiά†H½†i{Ϋ9υ4Ω憆Y'2ϋTχΛ8:­­oK΄ +Sη΄ϊI˜ΤV†μaζ(EbNήzšs@/dŒd3jΤIωΣ3~ ˆ]Ύ|υχš3\$ΤΫ:Ε$Τ(MEžχΡ*ρ±Ή–£ Έͺ6y—KεβiXπ­?οydœΑΓ{Wτ˜D©ζΒ-l―ΩθΌο4Νs ΟMΙυSm?R`4ή?‘œ½ΰψFiη’P$Ξ©:vΔRCp}b’K”θw4xΫΜ>ςοW‘ ‘>:΅σσμS(mqb€V«2Κ‘zdΫ[ΪM9­―ˆΏcŠε,ΡΥMλCν―1Ig}―‰¦λ„Ϋ9Ϋ‚/P5Ω^UψnπΌ3V%™{lή!u›8ΰ(΄*ε㐾ψ§ΧτŸz²-\ΤhKnΒ 8ύ\cϊ©ώ^ΨΕ> Η ŠσAΓŽ€νό7Λ,‘ΧSΎΰ§mA0³BgίΨΆε έ α…YN“¬σΕ©·X­W‡΅˜WΒ£Γ°‡›S΅΄νeωM΅£CΌq‰h}”:t·r7}>†sgΦΫ΄QρoμbϊΛS§βσ,p1ΐrΡ†ΙςΚSU ΄JΡ ΐ‰€μZθωιφ„Λ|Fuƒi6η&€Ÿ’ΝΫΒ•ͺΝ δ4$Οο’~ˆΛ΅έž ?\PΧύ Œ₯x§ΎΪZHθΞΙ2˜ΣL|9\Œ0 +ΒΦΔΈ[_'Dυι§«v‘(¦Ǝ;φ”Φ«–έkΥλ Q’δΓ†ηͺCϋrjΗςK­yBΊZδ5,šUKŽ΄΄5ι™΅IϋN*'q%i7OΌR_Ψώr}978΄Ϋώξ­ϋh€!Af£mί'έ“>νvπ Β`φLήSdo•—ή^Έ[ Αbτ•β/ςΒ{ΰ©Ez~ όE›–πή ςW½RiΉΧ“ΆψίPˆΛ—0Ο‘Ÿ3ΰΜώ[GΩ+γχ±ŒιΧG«<8ίg†3GZ³Π‰”ε +I‘Wš&¨―"Κ΄5nχ„ΗΥ +iέΠζΦσ6υ–‡XC}ψŽ!±7”ΨψψF!DC ’\1-5ψˆΣ-υ­gŸ°ΘξΩΏES‡LHΫΛ%œϋ‚ΚaΏ)zτ2d~Έ_.u„#κΔd6‹'τ³ƒΝŸϋAˆ?=`@—oQt’]°ƒbNš¨ΕvΥ{ξΪέ§Ϊ49Νi Dί OsΜ`iR©¬!OŽpΟΈΎχ+o’«₯ό}z&8‚g78(fΌήμ_&όvXoD/žc”2Ηζ1JΓ’žκ«P/Ikšΰ‰Z ΐ~ΒΌ³UXΰ$a¨r‡§œfwd0B>£¨αjυƒ€ytzC΅τΪBΉ’OΫeωlFÁœd@ϊ]ΦA‘’2mV\Š lΪu¦ςˆ| Rκ’έs _OΏ3^Ύ}Υ¦±†7ΗΡ§d*«Y|΅,2‹8*ΊΣΑ΄a‘Πwί}Ud1>°’ZE’FΆΧσPΌ Βc-obƒζ‚‡g9¨βž…ρ>ΪΖUκ)bZ +€…eN™&ΈlΏ§C`M“hΗσ»Ήˆΐς¬ΜφWFneΣojΑ}fΕΞυ¦ΛPΣωDͺC~K™•ird·²’uγΦύνέσής‰-ή₯>'c•x—sVπ]·ŽΡˆnΧOΏk-ίΔ&Ό›OζΌ+gνό^*χMm£΅”2ωY Θω}ηkιΊΟxlά…ό㓆 Σ€’ΓΕπΆ/ώ%"ΌκLω‘„šP>Η³mϊχVŸί°šυ]˜ρ(LΦLUτήρήs· + A8²€±ρ1²–― »!θ!ΈEΣώM>@Q“±‚1Ζ¬ήΡΝ2~ŠαΒ–%jΔΑwΨ·\yϊTς8ρŽ%JCρκ6gtΝΊua5Y–B5 ƒF©ΊΩ§RπΏQβ„ΛlμξζDΚj>*qq9BqΫDpςŸg[L3α«Ή%„Όν£9ρΕϊΊ‚r<Ήω‘@ϊEΰ ›ωλ•\ΨJ*†%<=SΟŽt½PƒŸ<… ΪT±ώΊβŸκd tlόu,²Η›ΝWm'/,ΘΚ&zbΛώs­EkG±&¨+”M©©%QΉi‰ΨΛQy) Ζk=TΗ²ώ^'¬‚Λ`ΐΎ Ι $Dϋ%―ˆ±ξXΓkώπ‚Φp€AB„nϋ»Μ5n\ΖΗoβnS6•QΊX”qΖμΜQw-dσyvZU‡ρ?»;t$§jžπΖ4VŒΰKέόduΘΦΤAq« #, §ΧŸ‡Κy η+]9πΕΚ€δnTπύ†ά|‡Φ™Z=¬Η ΣW’‡Ο«τG_©1`.kάVψ *ϊ³'ˆͺ3Φ-εδ³2ΘΐεDςZn&`1ΓH*ςCν…²wγ.'mόώωznlnSΌιTώόβzΐ*A1$ςΚ-Rl2}ΐP—@‘W,†ψ±*:3ˆ˜y*ήU«?ίd΄<δ‹™²O΅½^‡UΞΊΙΝμIiΤΔtJ:0<•;.B”GιbuI?τ9N¦šqωαc_ +Μά,Ίš+ΩN ©BJ/λό2:Ž£v/Y(œ±₯rΥΠkΣ‡Q³3σވFϋή ασVnW“‰‚ϋ™₯‹4©6ϋΒΥ@xμΫ’;°^t{\ +’ϊ½™―GžIΤ›£žυ€΅‹"·σ牝jκ#xςQφ4,jkwjχ˜lρ·‚ k― +‘©5Hσuθφ—b–Έ}γ(Ι‹|ŸtSIƒ‰lΙAι€Ξz»€θΤ‘xœq$ΦRŸΕ›2(nω–`¬•κ3βς&VΞb„ΕθsEz—½aƒ-ιlœvQ›q!¬ΥlΌ‰σ[_“ϊ˜ΤόUζρHξBuJ&_sϊFLΥ„±˜@2N³ ”ΨΌϊ(¬Wmw,δ$46d”ΕRJ© Ϋ_†)©Ϋ·™€_…%V?»ˆqL:%°ζ>W_cP8sΆƒ7ΒΏφvš³8θδ +wάβ”y¦€κ ίD–zωšζΦ ^z’k&ί“‘€έnl*giόXξRν νΗηχT2:t³4ZaΒΟ”Ά͐ŽKi^UΗ΄W.β rB8”{2}βϋ9^1·b—ΘΆΔ+‹μd£>³oσŒλΨb€"b΄Μrϊ Γ;icε‘Ϊ§ΝŒf«=ξ¨?ηcHδfRχη„bŒ€;Š`1½™”Ύ‰)UΡήj’I»΄ρΒ ΄2…εΡBΙϊΗ’š‚r:Qτε˜ΪyηνVˆΎ΅πkΣiςF`φ*άΦΧ\Ήeάr½[Ό’€’Yή;O1/γ|Ε—QF«εΏ Ν.5φ¦SB†‚‰BˆœZ–UΓ‡Vα—IχνEoΫχν(Gσβ¬ε³ΥΌcϋDψΈP΅$hζ^£PIάΛ9ώ ι-Rσ―ά0Ψ;Œ²KΫ”‡Ο^fNu•Έ9ψΫΕHXφβ]DŽ₯,N”ΞsΑB”χ2NψϊδŽΎζh²x?β²ίθͺœ,ήPi“ˆΘΈ¬Ϊ5hŒΎ|όv•gvŠσΉ'Ξ8FΣΛ\%'«†CŠd°•[¨LΩdχp·ˆΈ|©ώC=)ήΦ:_q=,Θ[ϊϋΗͺTηpn›΅ΟG’=“wαΦhΕ­ζά!0©θƎfσσ¦ε]Žƒ'Ij[ӏΎcQήΌΧτš»…Gΰ.Žφ:οhΨit\β:JΖ!W ˜Β  Oƒh$­ΡVuνσ‰#[]quzϋC?ƒξY΅—¨₯›@’w;_o’εJξΡWϋhg5ί μνgεŒζ–cοΈΨ±"1β+˜³Ω<•ψCŸhWsΎ$ +όh‘ZέQ¨'%\»2„Δ_.A•ΆLϊw">—i"ΜSpR¬_W([ψΓρpρ·Ξ!Ρ9ΛΕ}₯γυrJC¬΄A\YkKK+@€Θ i³w/L2„―žj¬4ζA`Β“Δ΄F*`Eβ€wœ€σπtfœuf3lΓ-ήM™›ζ~TR σι-€lηIε›Β[Μ(¨S‘3Š9Ί7ΨΤΡCŒ€΅L?ψ¨‘DΏυϐJΔs£QΔ¬©ΗλU~2Ÿ9t4B_•aαGaρl>v +^ιvμœ~Ύ—{›σ>ΕwjΨ³ΓγηTυ~*I‹HΞΣTmD§‰¦eTžΛυrgnx‡α΅ξ—dΩ/’DΦ-CΗ4Γ•ux‘$Ξd-α γFm|"PΘP[ς— ‹…‘ƒ4π hΣ^Ο”Υ.čYς'FβŒΡξΔw΅Όη, ?^TQγΞ Y4²΅~L}Ν‡|μxΖ½σc"&Γ‹›)όόΎθ0½-šχT‡$Ÿw²…~Οkf(%cγΟ†S=AΈ‡τ~ίϋ9΅mαdd΄a²§ΚΥ½CpIWρS,gΩʍ»{©/½Gλ=ΐ©Ο΄8’_ŽΈ]MQ©΅vA}2CνoΞωlηC™rJBτϋϋ ΞL' + +‚π―žrd»EΨsώnΧλέ;±˜ΟkŠD}wn%ΙoNg>q‘OΊΒθUΨY(ΝlF’ΨΔ"L[Ο13”^w?.δD~Uυ»\ώŒίB‡/Ρμπ—%¦±.1BΥ}³‰(™½g.!uΘ#Άύκ]lΎδ1ί‰_‹°Š6K›luμ8}·›φ…‚x rRώ, Ιͺ·²x“Ύm–Hί―Α nZ@ΣgΠΊηR§Ά9)ω~ήνU{.Χq[EΑωύριμΔΞ/&UxbΜύι΅§šΚy“5q}"„C%―­°άύ[¦ή_­χ{žύΑ'/Γ…\Iw&yhυ¬E4+³RΦΣοOόΊλ΅©I¦:δJ¦g±&%ΰe6οI˜’ΦΨ[Σ-‘˜Α4} Œ£Ήα­Λ'KQ:mα­Α#98!ΨΌΗλ|­IT·?qΠΈδH‚‘¦ ηNΫ9-Θ +Νέ-ύθ&c½έ…ˆέsŽ n b„έΖξΫwδTτ zJθΪvVr?Wϋ…έJŠΊ™4IΧ7; H~%Kzθ|§'Tπ¦@ˆΜžΟσ-ά{† w –xΐϊHΊI‘ζ=2ΥχΧotRγ +Β(Β(εQΗ—V^©Εϊoιƒ=‘Ώ+ΐ!9ΒDͺ~ΊYης6l. Ε œΉ―Ού ς6OάΦτ ˜Τ•fˆύmu1tω¬±λHήΘR­yί‡£κˆUδWn«υΒy'”œ””—Βs»Γ‘;»C€ΰ‚π€>| ;΅jZΛ‡ΒΪ,%+Δyρκ9fςKn¬ξρ|NM’,„‡)™ΦΙ> ϊ™C“σ—Ÿ']r2+Ή·šN₯Ž€ιΦ X¬ζθίDΖb&]γΦ–vζaIΣΈΏμJQWΣYζοƒQL%v`mœΚ―֌ڞ¦ής`LW§|h˜QΪZΎ«u\Δ=K΅oγYpD” O™=Žϋ‡Hλ¬ΩΓ +«½x«ΜU‚kO”ͺ³­œΉ2‘ΕΆ«™ΒΤ>T†¨F[F!B7γ]ϋ]:G:dznsΖΪ‘•Ω^ρΣΣ`tQυe`ŒB*³―oQψΫϋ ”ΎJœΐΑ`δU7!πMd“K6œhi΅bΝΈϊΐBvtœ΅Aώγ{2±ό€ζΓ g£²rN¦lΛ]rͺ:E›Έ+Ξ]’ϊΨqXœ5}μ³ άŽ@ˈΓφ‘QƒΒ• ζΜ3σΟ‘Ο5‹ΦFmΕ―)BΉIίψ EΚ)_ω`(Οw.cQM‚€˜$ ηߊ²hPcάQȎ.{²βŒjW©ΐϊ,™ώPnš1}h­Θn6Νμy7ο¬₯iU0Ω’‹‚³Γθςθ΅)§ά_ψΤ»hΐ‹γ\€P"LŒ΅;ͺR -«Ψj‹\ ξΑ$ƒΩ\Αœ(ώ;±ΖΜ+;Γ +ΞV--²Ÿ―J ³zΤΧt…•Ϋό«_5@$gκ6 +Ι’Ϋφ AυΦvSdΓΆYύχPρoςپ鬍ρό’‚₯§'»x$rσΪ=3ύVΌΡΈϋ³έ+²Ζ=’ΐσδBΥ^;NΕΕGΗυ¬TνEί°Ό yL·νΨό;"‘I…@-} DŠςΘε7`Ωkƒ +;ΐΝ’θr­$™€‹rΩ—Ν™Pt€ <ΰaΓ ΘΟ”ΕGhπDΩ€±Γ j_Fπtζ3@_£–Ό€ŽΉ’¦μΫ@έ]+όήHΟbu›+;΅QΏά$”Xό‘,Q.cˆπ–ολΘ4DnζΕƒ2xB‡ I€=.Qtλφζw”0esnΒ+ΐθw Υ/Xc†Λ4΅„DeR¬»O ιsΥ”›€ vΕ₯ΐޚ!ώ•ώa ¦8d½Pϋχp/ŒΫ0ΟΔεY‰ύ»֎1{ν AΧέ©Ώ|O{f½)αωfΗqCb>aUΚ@ƒϋͺ<ΣG›Χ—κ³(IJBβl½ΞπŒ0k…θ)¨λι _σν +t“˜ T+ŽΌi"x£Φ•Ί’brω!Ί8£ω+ •―u<1‚[­ ΌƒΡΑa \€ΎΚΝήλΊw½ζΊ¦e°{w#o§1€ZΑQHa«7’ΰdf$™φΞ$¬§”΄ϊHΤlΣΣ·@›gx”³…kC!…eUVΤH’ŸQ+)s•XΦΈϊςf”yό±Δl-~Άύ’ϋό¬Ϋ?Μu‚ιΈ1Ύ ύT«’ˆ,œj[0d•r’ΎWξ^-œf=ϊΎ΅@²€Ρ―ζ—›Ό kxΖgηl7Λν”΅‹ςΞJ6/₯ˆEΡ8 -Ζ"ΪמΗsω·΄—Ϊ~ ΰŒV–Z0ΟπGWμT΅:Θ}ίn«Pά8dΪ|χGό™‚vλ±’ekˆΒι{ΓΪ¦’iωYW2‹'ύ3ωΑ@r§‘ »΅<16ι2n‰ΦΕΩw<ΉU3ΐα…Ο‘N{˜Υμ]o–`@L₯£f*J&fD·KMϊeΔU»x<γBΥό1³€†₯K9ά2ΜA ’D·Δ>f°δϊ`Ώο5Lπn°cMt/Kκaδ–₯Œα?2•Δ¨~ϊŽy}WFΊΤϋ-νH‹bXπ5οςU4F£^Œ\yY»:Yί»‘¦ΗΦ{‹Y_\οy*MμvWΈA9ΓΓ4ͺ¦‚hJ…Qβ΅70?―ā!Σ °&H}\΄ρXφk²'|±mp-Ÿ₯«Α&YMKψΉ9Πβq+'ςG±‡ρ"ώΎƒ`O₯ΛΉ΅VξT›u}ΠΨq(n†ς’μ·θ[’€ό™“ΈA₯ άΩy=y‹η+ΤΘϋVΒίί:Α-ρ^Ή5DυeΕ6/A”tΨI8λΡϋ ρ:Ε '»ίU>­$’ηJΜ+Z΅έYjnY‚)γΌί?†ϊ)W»φ?HŽT@Τsω?άϊDΚŸΌ?υœ―υωbΣ…vΥηΝJ›'Θ»,ΉeΡ_Χ?―>5±€ΤφαJw‘εΆΔ<ΫΖΜ’>K33βŽ+4Š•“Λς“HΗε…wijb½ΨΥΌwκ=ƒεœΟQΑ|ω5νύ2|ˆž‰π_tΝΧ»Œ4轩νλ{Ξ'χΠx―Iԏ`πCP쑁ΖTΫ}wT£^°9B π^φh£­Tσ5κΛ«ΎΞχPO-!%άΕhr…ψw/Ξ|>ρ-γςΎ™£ήN•ηF§ 5ΞηϋB¨’ρ.7ͺ‚ΰήΦ“dΡͺŽηΛXκΔήn+Q'ωυυ²“1Χϋ7vί„Υ“B’ Σ“/(/παs +σ‹wΠ#«ΪΏLO₯C°il<ΤΕΥw:{*₯α) Ξ…5ϋ‰ΜΤi‘Λ¬φA’ύ)›jM©7N›€/%°•ŒnTV*κ€W% pSfΧL%>ΖA9ΗώγΖCb ‰Σ‹h‚‚lΌ;ΐωl­dkpφ'Ϋw8ΑΛZΧύ-ˆ€ͺ1;ξh@Ύ`”[Ί2GŠ%M`\€Χ£φ>³«Ιύ–ΡrΒ”i=ήl ςξX Ιtz]ωcΦ7ύφέ4TJR…γ²un€οζZέ§D^bΨγςtŸ€~[c(ΑΨd‰q,Βv–$ƒl½TΔw5ρέθΥΎΎΩϊˆΐoδΆƒFžέξ½ΈzMΒs{-α« — ]#yΆΝ΅KΨΧ/u“$σ\ρTλ½!ςψqΩ{n­±`'ίT@\γΊE©Ύ±g˜βwn‘½]YxŒΚsΈύŠ©:83i3¦€ΉυΉyν­»œr™EύX/ δeΧ«—„k £Έ¦g‡ΗΦΠ‘υjKk zSϋ`§SτΣΏYεφξ·<±ϊ4²yv‚Ά4³‡Ι`»‘ρκJ>L“xgΆ»cΊγT,9—ι^ +ZΏX]aρΛ£Ζ5λh±yΥ+¨F=φΟVNΦΐ ΕΨΨ‚΅·,{”ŸG:λ°@t¬iΊψ<»ίς;P;φyB–:qEίwoααH°Τ`ΆΌ5αά`J»1›΅Z·½U¬΅εΐ‡f΅rΉeaΡ`78zΘ?TŽ’!„Ά~ΕU‘\ŠsT2I’¬η¦œφ0΅^rK’*¦ μue£#°^}±ΙvίΔιHYΝŠΨHI%΄¨iΦg[`TΖc`Υ ςSνγ ‡ί™;iΖ7ΓΝ3ο²οŒ™ύa›φziς D"|“„,Έρ΄κX’遴Qe +ΌoMπŽ, ./ ά{jŠaQ29Δ(/±ZTΉxi)&2%N\'cKΰ₯ΟTОFŸTχ +Kbn%*ΎΎ›ΣEΥWa%‹«εN~Z|GΧθU΅b’cΧϟσ}%ƒb­wCΊ—$ώŒuΓηΕT„!η\ςBFV£GψζOΧsuκΪεΐwXϋΟ€v·Π ϋ€ΰqΊχpua‘ԏυvΤzΏX&IΈΠοΆΉWε[wk³6ΗέfΝΚ’ rά^Ι~Ν­Xˆ$’E”HΣτ—JπfΓIsnό―Ψ+揰I$ς₯ΐ:α^όF1£|—Ή©ΆeM-θšφhΔ³—ΪXΕ”μλΪoˆN–œΪ ΙFΔƜ§vςi’΅Ώœ«π½ΘΊT)μŽΈρ€ΰΫΊh8Ԋ΅ƒY(z.%RΆΛ'ι₯ΉA`Θ2Vͺ ‡²u3F­ΆF©>qšΈψ¨;€nζJ9u{ωΘi½©d žR"ςEι/οXewΕΓ Ά 2‚™ZˆCΛ/ψδ_S(LGK˜txV‘Η”ρΕ6ϋ‘ε@œ=}ͺj# 62(ζΣ\ίρ:Υ³οJ•I–ŠλDŠh]Υx\η˜;> "ϊYϊΘg=ινc4'ŒΫεšuήΈ«{X―¬jΦω(\’Iτ­oS£H$Oξ™±(Ώ`¬ή8ηςœΙz‡kFRtRξΤSςνy°Xm6=Μ)l‹ΑΤΆm.ŒάοΡξ8w–lΪ”‚=~SΫ4“G‡_iqD¬fZ:Θygα|έ©U΅%ό#"χdh΅O"νΈδσœˆ‡ήΊO4ՈQnπR{̞―’:s—΅§Ϊp΄|0θƒΔ…Mΰ\ΨC*©ϊSυΤ«π“Έ'bŒ*Ή§ΦΗTΑνŽ‡†1YEσΠΎ%lΒmbq]Χ-ƒ΄ΰŒ,ω₯ό›Κ%Z-J”ΥA?&zU!Έxρι²β·~opς θ¬}*%Ο?K–'†―ΈQ•ΈCΆξ-Rο¬ΎξχZ‘σ°žεΧ73ž έ―ΝQt¨Όϊ8ΌDϊNQϊ2eCYΨ'ρ«σl°Zzα‰16υ‡Š% s ο΄›U3Χ&.|χHfd‚’+•VξΆΖΥdmα·Aΐξ坍·λΟύ¬O…7ͺ='τIά’λ“kί― „ρ½5γ&Ο§;們&αi8yτH‘%#s$kK,ŸΣ14d3^Εώ έ| +endstream +endobj +735 0 obj +<< /Filter /FlateDecode /Length1 1572 /Length2 7543 /Length3 0 /Length 8410 >> +stream +xΪ₯—P\YΧƒ»{ ΠhΰΠxpwχ  έ@k‚'Έ… I°ΰξ‚“ Αέέ‚»λOf2“ωζ»­Ίu««Ξισμ%ο^kέΥτΤjš¬β–φζ {(Œ•“C 19Α P² ¬,ΟΰΰxΰθττZ˜θΏ-Πιu@NΞ{¨ΰ?l%@f°&e{pΡt€@N'§ /Ÿ €‹ƒSΰ/;{'A€2Δl²h‚Νœ@θτ’φξNk0μ V2―-˜j`ˆΔ fogχNΚφ–+ˆΕ"œfN ω=δηcύ)βΩ₯ώs““•σ‘Y\!00ΰg™d읬AF0 ζ ΘΞώS‘ΥOΒφpazπ•AANx˜»Γώg&~Ξƒ£³cp¦—†ZJΪΏ| ‚ΒœΡ99– ΐd ’³ ₯εξz¨‘%ΘκΟη‡8Aά†EηpόόόύΝψA·₯=ΤΞύ·ΉŠΩK€]FBΙ@R™εΏϊό·™„„½ΐ“•ϋ‘I¬\άόN~~?ΰύŸ!ΥΜ Ώ$qόv–‡ZΩώ’nιβπ—όWŽ€ρ׈0ώ3˜Š= bρPΦ΅ΧFΌΞv܈ƒ“λΦθίNk»%„ώaΪώ?fŒ]ΖΕΞξ~όcΗ?;‡™ΩA,|.ŒΩKˆϋΏ\~zόΫPτg~ ΅‹™ΣΕΡόgΛώΜ&΅ΆX9Ήώ„gˆΘR ³¬ΜμœArm¨%ΘΙξ!­š½3δηž\88ώ΅¦†XΨBAΞΞή?—@PΛI†Z<j ΰβ>TΝΙ̝γa¬ΉxyžœΘC$7Θν!7;Τφΰppy?΄Γ ύηDqρΨ]8₯xΈω~ς?Ο/Δ#ρρB£A ‡ϋ߈λβϊΈ!ξίψ £_αξ!ΰ_αΕ#Ξ_θ·.ΰ_%£Ώ2JύF<ΏΠoυΐ_{δ•ωψώDiώFΏtoˆο—ˆ‡?ΡτΒΒΕΙια΄ωγm}hΤ_ΟV‡yά@θSγφB6 eβδ¬kύ|"=j‚ŸΘηϊVβlο9―fέ”¦RήE 3ψ&Κ›7Hψ¦θz°—^°e +OE +rDd”·Ϋ³FXxuu|σ؝8ŠΡΫσ½TyՊ-ԍ`Ež˜Ϋ\ΩΓԊtQSgΤ‡ΐΔ‹KOόΌZί—t>wV¬\5>„b“.™^–ρŒΊhοnG\ŸΏœœ2m†»p'†ξzx²± !π£ƒšpβ~κ³N3bόά*τφS]μά£’uυΈ ([ι%ψκή )-ͺΜ‚e¬‚3Κ>G›Ιœ\·r†HΨKέ1λX_΅λƒΖWζJ‚9±„‰ŠPΟ•,Νvω‘\Φ­my•JΧ–œz›ω½ίI“-δ1hK^‘%Aυ‘{zΈΘJΟ22Bν•H₯?XΏώjCγH^œp0"’Ίkο*{Αί•2΄XΧ(œHD8£8ΓρMZˆμe„ΔXεξFφωΖη β:μΗmΦ½’ž4яbηLS#’H“yΛ¨Grˆ[ΣςΑό‹_cW°μz3«ˆ―}Χϊζ}'F0ˆά)¬Ώ'pΚΊ?΅> Τi€?ΛΒͺm“%”†§½ί†c&ϊΈ„h{(h1ΖΜ\„!θπΉc5ύ’¨$;5y§~¨~›«d—8βεμ~Lwnκ$ς™%°kUΎϊΊ["θΕfρN:Iι[–utv?υΚ{ωW‰ωοΤΗbnζμίf’§ζΖγ™έeF$Δ]€¨ΚΈi`wζŸ²—¦¨α2vΦvƒ-L™*‘Ζ=‘ DΓ‚ϊΠW¨vΏΟT‰€ ε-™Α‰σΫHι„θΛά0€# OqG΅ρ ΟίΎ<«ͺΎcφ Ϋ Y$§fε­Σš}ΡDΆ(J»?cSπ5Φ!£ =Š~ΟΫξ¬NžΒŽ«F»jΝ&Ίιsτp”^ΫV“ΠΐΔ‚s:X1=qδ‡ΟΚD_ƒΫΟe0τδόx”ήNjΜΒ#SξΪ|=U«τμQδΉ€ΰν6+ζΜ©|₯‘Yœ––³ωέ©‡}FΎχkΒ_ƒΛ>-+%ώ·τ‹|*^ΠezͺλͺA5ζ‚ZνΉ Ί΅\ϊΑ•˜Œ$ŎfΘ“ΓJiΟIΌuό`‹Θ”qΔsk₯‘εwS*¨c(nΩψN$/μ'FE’y +§Φθφ‹%ΓhN½°ΰF‘ρO*v|α… Sx•ξ”lg4pέ!›aŠΒHFp~Β’N€έ±!Ήv9FŒ "\+ϋ)ΩΣ ΜΩ*ςΔ~r4ΒyV₯­\ΫΪ#wSύ7Rl<Έλ<¦K©φOnc*Τ/Ž ^m§‡ΚKc€y4,ZΆ#Ή‡λΰH«yξ―L^―μDp72ι8 uζ ΣφΏ +ΐMβ’>`d0sωXhΘ`‡j4v}›ΐΙh¦  mε*8φάŠCΰohj +g^«[&¨,ΟpLϋ~˜Ο– /ψVa@{τ™Oh(u’ΙS΄”‘ΦZ6οΗlŒPήΎ]\Œί»SΆξ­ΐϋ‰$Ακ0q@"§+Ÿ Lήλ ΅"Φ¬ƒf €_―x–^”ΛQG•‰υ­rΞΖd“΄OΫ$|9‚ƒ—ΐŽΘ’ΥN©)Rπ^ †5ͺ~cν3ΫΛ>λΜŸ·™ΌΩ‚β{ZXς¨–΅ΪΑ”‘+ήtΐ˜ž™….ˆ\›μΗ2Λwtϋͺβ"žϋYΚa‡ζσΰο`«χσ +ψΖφXηm>ξ–`x όͺπίκΩ„B&δεw0Όn‚ΊΉ|G[ΗMη@ψχ6όΗdM-eMΓaw«¨<˜’ƒkˆΙβ·w?ΙγRG`SΡ‹σMWP½2{Ÿ‰Μ“hαC’m‚hφ*Ÿ)8¨€ό˜Ή`qΖΠΏ» ₯EXT›ΨΎΥuƒiΫ不τ|xγN±MπζξθΔ“ +S…fXUΈΧμάLڜ‹mΖ68ΣAFϊXφ3jΐ΄·‰³Κ8χΎ΄|Ν7ΙYsώΆψΕΦU%Μ܈x`IzπꅍwG=ΧΪή%;ω±Κo²«ΚάΪζ”Ρε±N|FΖYo;«οΔnΖ§*© §M#h¨γΰ 1ϋg(†Α%gwΌν:p— Υψyπ£vΠμzP|Z#…έ#sΉ`‹Ϋ2wvΖ]Z³CbκE°:ΚΖYEgdxΟ­ρJqD‘GŸj,‡J +V΅ρθ€Ό«LΐεL‘ZΣU +v…ΞŒ{ν ‘L_Hήe:«Μ—›l¬I!Œ°]‹ζ¨8&=βΆeΟ°°;a‡^ΧΥD0i`aO©+ΆΔέF·›™(ο–eAHkΛ`—ϋ’€%b + Ατε +Ό‚q£€= ¬pέΝϊρΨνZ“θŽgΑ’tΖ2гΚUρZύ“‘ΊΟD³Kϊγz²ω0}oˆZrŸc>§Eb›P·Ξ³hh‹ψv υCš-ιŽ8KψΎK»…€SjxΤ-Ÿ4°})η'§Uΰίύθ /5΄{qόΞΌqjΐΑιΘQ%NV₯θy?i/ΤIWilί Ώ·°Μͺσ…•A³¬Ά„e˜ς5h’X5»R»ω`Œ!9tαΨ£›¦n~Q(Π‘RHWΟHΉ†ύXΝσ•β7Ρε‘νσtw Μ?z¬:’›ά‚&vEi&ΙΥY§₯ωγΘ‘Εw©Ήν³‘nTœ $43Ո‹<Γε<δήΰ…Α9VνqΝ"o8aa°¦γJŸ…ΖΜώ^ο`Žq=Ό5SI-ΌfΝ”YUަŸnΧ’WTekgδmω3{•„#{ωΐxͺάζ»φ!-Œ΄εzΨΕ·r0mΈIΛΦΫ·ΕΣ¬H₯³Œαπ IH-Y–dofžK‚sK3Bϊ ΐχα³% MU±Ξσ#^LστξηDβΎ:{k…ρ€[Ιηοωp΅›‘•H˜§±8lk—1Žž_ξσC·%|Π£,ήf1Θνθw˜5ΰMιΗ$抨:•P€β™bŠB€wΏ ~Qo“ΗΦ=+J₯cΈzsUηY7L Qπ}zλ¬φjlP<Ύ Β¬!‹ͺ»@Ί'nί9zη²Œ’“ΙλcQžΠKΒ±dC,’ψ'θΌίφβΚαvnΛLΘ_{”dŽΠQθoα¬)Κ έϊ!5‰σΨUh#Νχόζ}ίκΕ αφ’Γ­\#PDԝι7Šk +I\Φ[τ›I0ΛZ7^A£ρ’ WΦΛu‡BχΥέΌ’gΎΆ™Ρ½ +xf>'°ϊ†₯aιΪ‹ε +šŸwβm­›υ?₯7Ψ―;DzBžΐϊ6¬FqGΑ_£ž}’―N'„ι"£trγ3žΠžP"χTς]{nЧΩyaΉAς|ΰyόi˜%Φc«bν£υZΈ$XE¬tΏ³Nρxρ8}ώ€€°zΏ5ͺμ¬EŠ4Rχ‘ͺ- f–/Ξΰ`b+Ί*€j΄ H $ΫDΤ…*•ο Wq ·"ˆτχ&Γλ΅Εˆ·Ν/yxψ4'Ά©ςΙΉ―ηζ‹‹ΛI)ΧT+(κθ]ξτ†‹»GŽοΔFΗ}Ώυε€…^ΩμͺT†wHβϊ­Η4ΙM_…Nu‘œαΊ>ς“δœηΉ•±Qπž`*YTΪ Α·n‰Qτ» +R›―„ΨΞ—ρ^σ˜³μs4}ΆaιId ΰ]nΥ4ƒ\O Ό¬ ΤœcΡΞDtϊWάϊXΞ†»π?θ0 ˜9ϋΆVvš>ΐLςjGRšCϊs)_aSnP}ŒPQέBOqJ>œ»B₯CzBš;’Β‘Λ3ξυΫοΦ?΄Ζ΄’ξ‚οmԝ׊κ”ΰXe§O­ΞIk‚3Γ‡ει> ©t‰{LŸι΄ͺ ψΧΟΆ9ORhδ-Zο`JŒέ³F―μŒ‰ž,#HάΆΎ0’(}ώΈ"Vψ ς’ψ>;ν²ϋπ‰:ψ@θ-“HJΜη3σάlΞ­rΘYe˜B¦Φ2D…™4ƒk$,s·»]ι»Α^VΗ Α֏OXɌˆLWΠΥ°†Μγ1iEξ|}Ζ’‰²­©oίθλΝ3y4ΤgΨ²μ\o ½AϊΦd½₯}ξ·wνqOαa,ϊϊ•JŸWΫu ΄όtOμ䛆Ubh“\ˆΐ;Ύ/Έ&ε”o…ˆς9.qΝ« λWYŠ:›„©§ίk»’0œOŽGE<»oΜ6&ϊθ„΅λωΊ‡¬Άβ΅ –ΨεcŸτ6?œ]ΣθSSΎτ‘t3:yόpΥ±ζ0*'ΒjŠ &rƒtΙ:hKηf¬‹~#˝ξ/<’Δ³•ΰBασ¨έσš‘™κKαLˆ*vπb'yηG•β-™Ž(š₯μ “0Qί_†»9ΰrkTΌ†¦__ž>iS 6"Ύ‹3b>T:D)½dIΙΛκΑnΔ©By=Θ‰vςα΅΄ΜΛ4yNaάmk·aψΎλb0Rdœ]ψ#ΊŽ.‡˜²L €hηι\Ύp‰1`₯aJ§Žλ13—ε"ΈΒ^ΉΧφ.“ί›εϊ: nπV³ΗKςN.U•~ aΕ\φJ +˜ϋVΨ½ kͺy²o~CN›ωš°h/šͺXΧ/–Γ™WBBkƒhφSόNA΄ +"‘ΆBŸϊΪKtd“h—ΰδΘϊΥs7c~ΣΊκχά²¨†φ"'ΧΝΥ΅KΓξ# +ΈG!²U€~χgVΒέΞ°Σ4(ύ†#'Aα΅M…#!ΏUσZˆšQΝE'ˆ·[][l±gβz d~ν#hB”δƒœΦΣO―θh‰—w‰Ύ_Ότ+Pν0†+MϊdΡ(βθγΙςΆl’΅*S„ΚζΚ¬‰ΕOΗ£Γ=₯ΫE\ΙjΓ³kΈΒoK-o΄wΉ\Αž^DfΈ]ΠΟͺU qCΑ©¦ Ϋ œΐDYVBT―€X$Β 2β<²½ŠdQJ 2ιEΫ†·ΨT‘›Ž΅@αqΚ6 +QfHεvκΌ Œ{‡Τ°ΦΥλœM0&Uηά«žΤsœ@±ΛIΏQ4ٟΰU¬΄αΌžT\b«’Π)ΎμNΞ·Ηxβϋϊώ»yc’5}γQV’£Ή‰αΫ}o%,NQ!ΚΒΞ£ΰ·Ύ&U)3ήD$¨–‘{αY8/(ˆ‹9¨­ψ „₯ βD?ΣΖ3€Lέ/—’Ÿ7Α?Β Λ}”ΉJ³€»Ύ="±Σd€w€η3FžxeDˆΠzωκj 'AΧψξΣΥΰκ•ϋΚα!κ;υށaSXA”€*‰rŽθΧΩΧ¨Έώ;³εύΔgγ«.Q_ΔύΎ φΦ~Wr€ω +l€ΪRŽTΑ΅;s±”€—λΑ’ρiŒ|uh9£@•B)ΫγΓ½ζ8~Ϋέoρ2­ΜŒQη F+)σμk{—ŒΗΒ”2]λΛ©ΰb»λ,ά`ΈηΘ/lΜPf΅Ίφ0mΤ½ΑŒ;Λ,•@SΣΰϊ§dƒ€bϊΘͺ+G‰\πΘΊmI|όhΰΉwΦG‹PN`„Λ{Εχsέq%ͺ‰†> +stream +xΪ}R{4Tyo+dt,₯ΗRω ›0cfΜ )’&―YΟ3Κ4χŽΉχΪ™;Œγ•C•gή):E[­BmJ²z %R«T,:Ι;²»9φ"{Ά:gΟύηχyό~ίοχσ½ϊ:lw²-„ν‡0'Σ(TK€Λ₯‘ϋ)…JΧί!ω8‚‘;ω8l htΓ„N₯R'%q`;6/g™”ο?λ €najJ'Ρ©B8Ψϋ#(Ιdφ}*ΔΝlž‡dA Z0,‘E€Qt3 JB*,$ξΙΔb~ ζϊϊJζ"βΠ1xÈΏΞ0„ΘΏTY8_ŒlQ1 ¨σ"u@δ0ΔFpωb)<Ο{’,#(ΜΖ€Θl&€L£RΏΠ‡§–ΌBΒΝ‘y°<›§ǚλN³4ήf4σβΜ,›²Ÿ°-SΣ)1dΙρZΕΌΰΎτΔΧό5G6Ÿξθ"eΉ`φœτκ_ XΞG«"{3Ήθ»ƒC΅’Ž}OŽζΤαD½Ή!wΌΓEbλ\+I¦ν¨rŽ+}^C[RV¬ΆΖ »©4¦Z·:³!ΤΊΈ†””Ξl|5άglrΫ.T™H΅vŒΙRžp€„λT»`Έ‹ΞκήΧcUπswFm9Uεt›‘osͺ$ϊν‡ώή¦aΧMJu’ƒΚԌΏˆ½.d{|Δθ₯™•RuGT™ό’lkˆΊ:pθx‰ΒΚδϊ²έ~U;/¨ ΥM\κ…šœ½9–''S\ίwΔ„¦ +πυΨZ™‚ν£² +OSξ]žwξ‘°!Zd1)80΅±@5$μMxζ–^·„Ώxhi;ο|}©Fwσ¨+£XΫκ&eμlKύ½4†b§Ρ>ίΡ’επ[Vηͺ}rΓΞ^₯υ3΅’sσ­ίοMΎΧ•dΫ=ypΙwoΩOλ1£ί­X:S0Τ!^άYχ =ς§%ΔeΣrνvŸ“aχΘ^9OYeΟ{έ­/¬ΤX΅η¬*Ο5τG2.ŠάOœq‘(ψΖŸ[΅–Ro­ΒTraq˜4ίΦJž\kΫτ‹Œ*’F»Τ¬*Κ(n¬ΐ€ˆ΄Θgκ§Wθ–ƒešu‹{rG>ΜМZΛ^΅ΘlΈΎ6ʠm™?v¬{Q€vδΧΟ6U./ΊκΥh~Ϋ³»ž―α°±u$ΎιέΓccL«L7Γ§*Η~fŸ˜GΔ$œJ¬•ψA{‚ ΓC έu«5Σ|’ο: +|ΪJcμ΄―œ½Vœž4:sΐ[5r#Ki›t²Ζο΅½§nΛΤ> C™ω=™M3k¬υq‘CΚαόΒADMrUq-σzVܚ₯Χ:eγ­ƒiΚΧq(Ω4‘rI7»όΚ£)žSγQWSO#6Ώηρ7›«ZQυΕ‡^υ’ΈzψV^²ͺMΙίΞ΄6fωγώδΞ 8Γ· +endstream +endobj +737 0 obj +<< /Filter /FlateDecode /Length1 883 /Length2 1288 /Length3 0 /Length 1908 >> +stream +xΪ}R{<”ωNN.ƒ€#~›dTfήΉΈMΞ†q©†i‘¬CΖΜ;ζeζ}™y'3•rlI-ΕΩJΊ —d[έΆlΙms‰Σ6$%±!—”ΫŠDv”Ϊ­ύ|Ξηύηύ~ŸηϋόžίσϋZ}Ζυ³u`α°†βΆT2ΔΈVȝ ¬¬ό\ Ο·V›a© ΑPζ<θ&…yΈͺvηα*C‡§TPιLƒiΗ4ˆF}OΔ€*Βρ`1πρ€Ρ0ΑΚ ‹VJ‘ΞσΪsk'w#Α +xΒ(,Ui @ΈΜωσΐ€πœ(Ρ© @αx4“Bͺ αD– Ι(ŒSlTg²Q&‘ΐ(.#¨, >Βα%Pζ€ό•Ρ0 ,|WsxΈQ€`ˆ ATΝ}ώBTφ*VώAχαI`@ρχ +b{yΨŒΕΒ`‡­°₯9ΩC€ζ©a0 ξc.™χύ1μ +1πή―@ύήσΆωxHͺ|lΐΗB>Žπa@ϊ4ΒOyr±ψ­q[ǁyD¬ό?„@x Ήδ/γ~Ύ‹7Ξ#|W4Bό‘…Θ<,ΰ"8_„<± ~Χ@°TŒ 0“!sϋl©τ ζ/BψQ(,“»wŒ +>1ΐFω˜A#ΝΞπ€Rž’  +^UفT€¨”V¨Ξ¦Q W€h9T›C˜ šF₯ŠL.‘Όέi"‹σ”dκιΩύ Ηa>~δ‚/—JU;χφωTίΧBD• +`>αA Ζ_·'ςςžΙ‹f±Ά= τ*ϋ€—³r !E¬ΦE^xΣΗ$ͺЌqzάρΘ)wξmΡηηƒΥ9κj‰©fμ‡Ψ('VYΣΠh―αB―ύ¦ƒδο}˜ΊΓZ€_F *Σ΄d“τ¬~7e­ΧΠgη^Y}ξφ³‘_"fΌ›8ωeo_Œ―†™ωΉά"mίΎ§₯S /|½Δ»615šλΛέ‘Θ„}ι:λ+ŠοΎ²^₯Nρΰ„QQ™aΨ½–j–žέξζDO~y8ά,¨ΠYώsmJTΆΑ₯\­rmώlλή1RΈΟςίΰ`£–Ηξ1ˆ“νζOz< ―DΪ³­Ξ{€€ΠΗhδŒ΅]’ ΉfX;€ ͺ%ΒZtωVΙ’vHΰ’ώ‰R·ΠξΌ(žΆ‰q7±έ―<ΎQtόR2pΟJ*½j½!T«½ΩΕ δہΓΙ΅Eg­Λ}ΪW―y]a$‡vyΫ€μ•,Xτw·šΆ[zΔDkΗmΕ«~¬νŽ tΕutTΧ.jNΗ–Ί<―†¬σϊΆS3+½iIΓ$h¬λφθψD^θy‹έ؁Ζ3›~½žwΝ’Jhͺ8ςfU«ι'1CΥ£[³_„NOj3mlΟμ’―>›•-oxZ6:Ϊς`°)ΑΨi;; "YΡ£αƒΧœϊU«οX§*1m”AΦ§/]KΊ§yεΫΘciυ€ΦEŽKˆά5OΪά3‘ φΥfοLgν#Ϋ7ǜζq½]ιΛNž¨ƒv({žGω^Ώ’Ώΰ¦(mΦήG~SΙΦ s~«Ÿt άwpaί߈u!©_?Φ°ΠH˜ … TήΡ«xq~Κ+|ΦνŽόΠ@T˘Ξ-a²vήΠB³#aϋGΎ.`θkά^|dΪΌ—nYc"L=TsqP‡64Žv{τWT½ϋ΄77΅fΝ(QžΟτδDm­;Uυί/¬Y­&υQ·―LΈ;vΏ£?\‚jcžv…Ζ8˜ Ύv˜žιΡμ_ΠυθεWΛ%…―ΟT^ι’sΦ#C½ΰQsα>smοV’Α Žζ3S+*K‹’Kέ>Sό&ϋξ΅ηΕνvγ―υ[gΖ€VΟ ‘ξQ¬Yκμ=š»λα)]΅ϋΖ¨Αβώ]†ςYρίΚ:iKάƒ£[ΊιλΈ†‡‹s₯S­ΠcΜ²¬Z#:—Τ%*iΈ΅Ή#>Ιψ)Β’ƒν9ϊ"ΎƒΞύ"‡ϊ›γ4cΧdΟWvjΓΪ:ΪΟXΦmp*0_ 5i_ρŸn™CMε41ΫΉ¦ΐLzΞad΅sΒΣƒχ§CnδkΥ&eΎ©*\Ά.ΙNMΚ½ΫͺU°½Œ]Ϊv‘΅ŠT<ΏXdΩ:nω4½3.›ΨΎΏΙ~Δ9AΟω‡*¦mvNη&fΥ›{JΈύιS#K ]^fΜ°…¦“΄F΄«ήΐ5°D»ϋΘΐβ}%F•ΏΕΝ©κ%ύΟΖMSv_ΌΌΩνκΨ‘nαU»ίlΛΆ/°QnΨ[¬75ΔZΥ1ΫͺS_w8/σ}έݜGH\Γ₯ŸΘ3g.η3‚'χμΧ?΅°E¨³dt<¦υΘΙtV|Ι{ω†ν-'3"nψ―Χ“΄·1ϋ„ώΜ–,[1Vνkδ³,P;Ϋ”ΈQ―ΈMw°νx‚Ήe-ΉWψŠFΓ`πΞ3ό|ΈηUurB~­NΖ–%‡JΗPwΝ\@YZžσ›ΒήζBSΰQέ:'ώFI…`ΰͺq“,lΣΩ~)λs~Ÿ‘/ +endstream +endobj +738 0 obj +<< /Filter /FlateDecode /Length1 1284 /Length2 1147 /Length3 0 /Length 1893 >> +stream +xΪ₯T TgΆΑ†β"ΘkΛ%d€@x„f+1(C2IΓLBH΅ ˆ― +h ˆ Ԟ‚ ­ *EK‹ψBt9<Κ’–‡ΠϊBAΨ »»ητμ™sζΜούξύζϋξŒ•yP…%ΐb`6†Κ(4;ˆ ΰ€P?@q„ ’•U("“ΐo$«0O@0”ωN‰7σdDΜ‡'#*Ω8ω2@gƒ Ρ™= C4—…B g„/ζΑ"ζαR˜dεI•8"˘ΐΖ9,B‹‘\ $Έ%*~›Ψ8\D‚ŸˆγΔ‡5·’Γ…³!„€aΜ'uέΓψi±η'«X&I”‘NλΫ1O˜τΆߞ7K{Υ£ΨΠuςπ7λΤŽ>ζκnΡΨ`[ς[ρlƒ‘oΆhΧ•X‹ψ՟WδNt¬Γ’ό&[OŸΌέα’[žAΗΦOΌΏώ¨μŸ­Ο«KΟΧορΛ):qΝίt|Cϊμ8βV\qζ₯Φn©―«Q\F +ΙԜ™φst~ΰ‚Žq‹hβΊG²Ε‘%_τn‹,ΜΘ2,`T™w”|_tFμάε‹AmΙυ’:<―Ρ`*u¨½/υ~‡–ΎTτc.ΝOΉf»Ϋ―{Γχu4ͺ=/Υώ…#w-Z±ι±ZΛrž‡q₯ώΘντυ£Ό˜υΦιŽ(k:5ωMειΒ‚Ρ†Ηϊ £:τΚ1ƒŒΈžρ6ΧY··λi¦­ΐ±ud§nκ'―τ¨_*FO­:{hέuwτƚYzŸυdW,gΆΥ]jxΪ0yΗ8―ϊπFώ M‰ΦΗΠΧνΒό›Ϊ³ŸEΥ.3έ9¨ν^iωΧαυ―>e’έnš―I½fϋΊo΅―_“>(Y‘αT©Vξ`ΝΈ«εwΦ«0TΝqoj†΅YtYhgΊ΄ˆ–ΧΝΥ +endstream +endobj +739 0 obj +<< /Alternate /DeviceRGB /Filter /FlateDecode /N 3 /Length 2612 >> +stream +x–wTSΩ‡Ο½7½Π" %τz ;HQ‰I€P†„&vDF)VdTΐG‡"cE ƒ‚bΧ ςPΖΑQDEε݌k ο­5σޚύΗYίΩη·ΧΩgο}ΧΊPό‚ΒtX€4‘XξλΑ\ΛΔχXΐαffGψDΤό½=™™¨HΖ³φξ.€d»Ϋ,ΏP&sΦ‘"7C$ +EΥ6<~&ε”S³Ε2Κτ•)2†12‘ ’¬"γΔ―lφ§ζ+»Ι˜—&δ‘YΞΌ4žŒ»Pޚ%ᣌ‘\˜%ΰg£|e½TIšεχ(ΣΣψœL0™_Μη&‘l‰2Eξ‰ς”Δ9Όr‹ω9hžx¦gδŠ‰Ib¦טiεθΘfϊρ³Sωb1+”ΓMαˆxLΟτ΄ Ž0€―o–E%Ym™h‘ν­ννYΦζhωΏΩί~Sύ=ΘzϋUρ&μϞAŒžYίlμ¬/½φ$Z›³Ύ•U΄m@εα¬Oο ς΄ήœσ†l^’Δβ ' ‹μμlsŸk.+θ7ϋŸ‚oΚΏ†9χ™ΛξϋV;¦?#I3eE妧¦KDΜΜ —Οdύχγΐ9iΝΙΓ,œŸΐρ…θUQθ” „‰h»…<X.d +„Υα6'~khu_}…9PΈIΘo=C#$n?z}λ[1 +ΘΎΌh­‘―s2zώηϊ \ŠnαLA"Sζφ dr%’,£ί„lΑt  +4.0,` €3pή „€H–.Hi@²A>Ψ +A1ΨvƒjpԁzΠN‚6p\Wΐ p €G@ +†ΑK0ށi‚π’Aͺ€™BΦZyCAP8ΕC‰’@ωΠ&¨*ƒͺ‘CP=τ#tΊ]ƒϊ Π 4ύ}„˜Σa ΨΆ€Ω°;GΒΛΰDxœΐΫαJΈ>·Βαπ,…_Β“@ΘΡFXρDBX$!k‘"€©Eš€ΉH‘q䇑a˜Ζγ‡YŒαbVaΦbJ0՘c˜VLζ6f3ω‚₯bΥ±¦X'¬?v 6›-ΔV``[°—±Ψaμ;Ηΐβp~Έ\2n5·׌»€λΓ α&ρxΌ*ήο‚Αsπb|!Ύ +ߏΖΏ' Zk‚!– $l$Tηύ„Β4Q¨Ot"†yΔ\b)±ŽΨAΌI&N“I†$R$)™΄TIj"]&=&½!“Ι:dGrY@^O$Ÿ _%’?P”(&OJEBΩN9JΉ@y@yC₯R ¨nΤXͺ˜ΊZO½D}J}/G“3—σ—γΙ­“«‘k•λ—{%O”Χ—w—_.Ÿ'_!Jώ¦όΈQΑ@ΑS£°V‘Fα΄Β=…IEš’•bˆbšb‰bƒβ5ΕQ%Ό’’·O©@ι°%₯!BΣ₯yΈ΄M΄:ΪeΪ0G7€ϋΣ“ιΕτθ½τ e%e[ε(εεε³ΚRΒ0`ψ3R₯Œ“Œ»Œσ4ζΉΟγΟΫ6―i^Ό)•ω*n*|•"•f••ͺLUoΥ՝ͺmͺOΤ0j&jajΩjϋΥ.«Ο§ΟwžΟ_4δό‡κ°Ί‰zΈϊjυΓκ=κ“šΎU—4Ζ5šnšΙšεšη4Η΄hZ ΅ZεZη΅^0•™ξΜTf%³‹9‘­ν§-Ρ>€έ«=­c¨³Xg£N³Ξ]’.[7A·\·SwBOK/X/_―Qο‘>QŸ­Ÿ€ΏGΏ[ΚΐΠ Ϊ`‹A›Α¨‘Š‘Ώaža£αc#ͺ‘«Ρ*£Z£;Ζ8cΆqŠρ>γ[&°‰I’IΙMSΨΤήT`ΊΟ΄Ο kζh&4«5»Η’°άYY¬FΦ 9Γ<Θ|£y›ω+ =‹X‹έ_,ν,S-λ,Y)YXm΄κ°ϊΓΪĚk]c}Η†jγc³Ξ¦έζ΅­©-ίvΏν};š]°έ»N»Οφφ"ϋ&ϋ1=‡x‡½χΨtv(»„}ΥλθαΈΞρŒγ'{'±ΣI§ίYΞ)Ξ Ξ£ πΤ-rΡqαΈr‘.d.Œ_xp‘ΤUΫ•γZλϊΜM׍ηvΔmΔέΨ=ΩύΈϋ+K‘G‹Η”§“ηΟ ^ˆ—―W‘W―·’χbοjο§>:>‰>>ΎvΎ«}/ψaύύvϊέσΧπηϊΧϋO8¬ θ +€FV> 2 uΓΑΑ»‚/_$\ΤBόCv…< 5 ]ϊs.,4¬&μyΈUx~xw-bEDCΔ»HΘΘG‹KwFΙGΕEΥGME{E—EK—X,Y³δFŒZŒ ¦={$vr©χέK‡γμβ +γξ.3\–³μΪr΅ε©Λϐ_ΑYq*ί‰Β©εLτ_ΉwεΧ“»‡ϋ’ηΖ+ηρ]ψeό‘—„²„ΡD—Δ]‰cIIIγOA΅ΰu²_ςδ©””£)3©Ρ©Νi„΄ψ΄ΣB%aа+]3='½/Γ4£0CΊΚiΥξU’@Ρ‘L(sYf»˜ŽώLυHŒ$›%ƒY ³j²ήgGeŸΚQΜζτδšδnΛΙσΙϋ~5f5wugΎvώ†όΑ5ξk­…Φ\ΫΉNw]ΑΊαυΎλm mHΩπΛFˍeίnŠήΤQ Q°Ύ`h³οζΖBΉBQα½-Ξ[lΕllνέf³­jΫ—"^ΡυbΛβŠβO%ά’λίY}WωέΜφ„ν½₯φ₯ϋwΰvwάέιΊσX™bY^ΩΠΰ]­εΜς’ς·»WμΎVa[q`id΄2¨²½J―jGΥ§κ€κšζ½κ{·νΪΗΫΧΏίmӍΕ>ΌΘχPk­AmΕaάα¬ΓΟλ’κΊΏg_DνHρ‘ΟG…G₯ǏuΥ;ΤΧ7¨7”6’Ζ±γqΗoύΰυC{«ιP3£Ήψ8!9ρβΗψοž <ΩyŠ}ͺι'ύŸφΆΠZŠZ‘ΦάΦ‰Ά€6i{L{ίι€ΣΞ-?›|τŒφ™š³ΚgKΟ‘Ξœ›9Ÿw~ςBΖ…ρ‹‰‡:Wt>Ί΄δ°ήˁ—―^ρΉr©Ϋ½ϋόU—«g9];}}½ν†ύΦ»ž–_μ~iι΅οm½ιp³ύ–γ­ŽΎ}ηϊ]ϋ/ήφΊ}厝‹ϊξ.Ύ{^ά=ι}ήύΡ©^?Μz8ύhύcμγ’' +O*žͺ?­ύΥψΧf©½τμ Χ`Ο³ˆg†ΈC/•ω―OΓΟ©Ο+F΄FκG­GόωŒέz±τΕπˌ—Σγ…Ώ)ώΆχ•Ρ«Ÿ~wϋ½gbΙΔπkΡλ™?Jή¨Ύ9ϊΦφmηdθδΣwi獵Šή«Ύ?φύ‘ϋcτΗ‘ιμOψO•Ÿ?w| όςx&mfζίχ„σϋ +endstream +endobj +740 0 obj +<< /Ascent 952 /AvgWidth 520 /CapHeight 632 /Descent -269 /Flags 4 /FontBBox [ -511 -269 1309 952 ] /FontFile2 843 0 R /FontName /AAAAAC+Calibri-Light /ItalicAngle 0 /MaxWidth 1350 /StemV 0 /Type /FontDescriptor /XHeight 462 >> +endobj +741 0 obj +<< /Filter /FlateDecode /Length 278 >> +stream +x]ΡΛjΓ0Π½Ύb–ι"Ψqή` !%ΰEΤν88jYΘΚΒί;JšBwq43gΗϊΉv6Rφέp€Ξ:xA3ωbZd¬Žw₯3έ·^ehn¦1r_»n ²TDΩZΖ&šΜpζ'9{ †ƒuš}›t\½ζž]€\UξπΉ—ΦΏΆ=S–Zη΅AέΖiŽΏŸ“gΒDθXάFƒαΡ·šCλ.¬Κ<―ΚΣ©RμΜΏώΦpξξ7‹EUJς|Ή©TY ξ„K—Βˆ€{αDΐ•p"`!ά‚Έξΐ}b.lAΥmšσw"YVϋX…Ύ†€-€ύ§ΙΓ­γΗ/ςƒ—‡¦όœˆθ +endstream +endobj +742 0 obj +<< /Ascent 952 /AvgWidth 536 /CapHeight 632 /Descent -269 /Flags 4 /FontBBox [ -519 -349 1262 1039 ] /FontFile2 844 0 R /FontName /AAAAAE+Calibri-Bold /ItalicAngle 0 /MaxWidth 1328 /StemV 0 /Type /FontDescriptor /XHeight 469 >> +endobj +743 0 obj +<< /Filter /FlateDecode /Length 301 >> +stream +x]‘Λnƒ0Eχώ +/ΣEΔ@CH©J‰E*ν€=D–бŒYπχ½vΣTκβ,Žg.ŒΗΩ©yj¬ 2{σ“j9ΘΑXνyž―Xφ|1Vδ…ΤF…«₯35vNd·λxlμ0ΙͺRfοˆΜΑ―r󨧞οβΩ«Χ썽ΘΝη©M'νβ܏lƒ$QΧRσ€Ο=wξ₯Yf)Ίm4κ&¬[€ώ:>VΗ!‘Œ€&Ν³λϋΞ^XTDuu>Χ‚­ώWΒMψύpm-ςΊŠ•y-ͺ’€’Cυ +ˆφIwP€j«%@QχP@΄KΝ(@–cυ +ΠΌ‹z„’‚’vP€1’φP€μ« + + ψnυ;~Ό`|ˆΫβΤβ=v–^+­3ΙXΎ=¨›\ό@βy“q +endstream +endobj +744 0 obj +<< /BitsPerComponent 8 /ColorSpace /DeviceGray /Filter /FlateDecode /Height 214 /Interpolate true /Subtype /Image /Type /XObject /Width 214 /Length 1345 >> +stream +xνaΆ£0…]Kcg.ΝI΅}š4&ΔBLό35ΐ½|ROOί™e™Ηt`:0˜L¦Ӂιΐt`:0pμΐκQnU­8”΅mΔmsˆΰTωC0ˆς‡ΰ @»*oξ’Ό!xθ Α@oSπΠΩόθ Α€Ύόθ +Α@OžzBπΠ‚Wύ xΠ‚Wέ θΑ@/Ζ:A0Π‚)€>Ltΰ7€όΠ‚ί:@0 }sΪG0 yσZG0 uσGπ@ΫήhΑ;M#x eο΄Œΰ=€†,hΑ€v,hΑ2€V,hΑ2€F¬hΑ€6¬hΑ:€¬hΑ:€€hA +€φ€hA€Φ€h A€Ζ€h A*€Ά€h +A:€–€h A:€†l°‚Θp΄΄jΫbνοOΪ|n«_μκϊαXn±ΐΈŽU,½L`’¨e±%‹(jY,έ\@ ο}‰Ykƒ(3έͺΏΈ“&Ί…qΝ„• ί.*θ}>5ϊΝΑψGΗΘ7Ή™δχΕμͺ†@ΐ!~βόͺ˜‚όφω‰ϋg,δ]ω{5ΐ% JΏGΠ©*u%TΌPύρBH•2‚2*?^€”*ΥΗ )u/V©^©ήXb’4?ˆεtͺJA9/T|ΌU₯† $€jΠ¦ +°¦τxΡΰ +α ‚¦[ώι³kC―π(¦ιW - Ÿ.joΤ£‘]*λΓ«)τmc«‚«¨πš¬+ݘΔYΜ8N•…"uƒKƒlΪζŒ!ΩhŒ'I…]ΖDœš΄[αρ‚ ΖR’ED;:, κΆQGyYέ¬Χ„5o Ι΅άο―•^Uu”S΅ΈΛΔ«ˆΒΚφχΫ5Y¬”4Qαͺr ‘T5ΥR–E·‡μcαΒ€­•”ta‘φ· +…ΖD\P)Z|₯μκΎ|”ψ><ŠχlΣ€up+«£*ΈQ…Ο> +stream +x₯W\SWΫ?7χf°Βž2ΒF–eˈ̲‡ΰ"&„b ˆ‹R¬`έβΐQΡ’¨E«:Q‹VκΖ­/ΤRA©ΕZ\X}Ÿ›€ΒΫώήοϋ~_ξοpη9γYσάBΪ[xRi.!”')”…'p¦₯₯³θχ"MδŠ4yό)'..¦ IΎDHΎΗώ^ήD)ΉξBξ5vμμQΒ>Μ:­DPΐΟC›ŒΓ„/•"€2 δΦσ +₯$.¬—“” xΜQ^ bd.”eb>+\Ζ+a…σςςx,wWwVœ,?SœϋV“‹ώ?ΏΌ\9i7ω³€¦^“oW°ΏBΐ !±/ΰC|^h"`oΐύEβ”ΐAQl€…SGΘs’9€7fΚΒ’Ύ+’GxBΈQ©()° ΰθœό(r­ΰLɜ˜Xΐ  ‚_œΨp›HΘ%sfψ‰,?œγˆΑCBƒ„·Έ›4Œ+ ŠI9ΨIά(“v‚.ͺz6/2°`;an8©φ‘FK γΘ=‘O-’δƐΊ‚Ÿ(ό…>Q(JŠΉ;ΰ€BYΉμ‘UfŠΓΈ€ΓοΙ"H9ψKζ*x1‘»ςd‘α ‡˜Π‹eς2ΰ#}—P’LΖ8BˆR0’|4ώς‘u#*@bT€@Yˆ‡ς ±ΐghα0KM3 +PΘ³χ|'ϋδ +r ’ΒX>Κ„ΉΉ°rDΞBΨAΉ’ά%Ω#wξUμΜΦθ +ƒΝΏFr‘~šŠΊ’b°0ϊΑ •ΓXΰΡZ܁Iξ(Na­rœΤ7¬%VΊ”λH?•ΆƒΝT +c€m +ί C‚ML„ζGDώ[‘M3J‹B>Y!ΡϊΙs·ΎZη‚­£½±‘(Ÿ†xΒΞΉΰ‘d8>`Ν;°;gxυ§h*42‘;H₯5+βΉ³κΑ^πΌ\6[ΜΏΌr ½μ˜bέ\~κbνΧj9―π‡Œ «“hžq]½½μΏdυS6Gl›ΥΨΡΌQ0Iπ7ή€.κ5κκCκ Δ‚χ/ΤNj/ {ΤϋπάωhΟ§œƒ\Ι %Ϋψ˜I²‘ΙUŒζA4ΘL y +‡u<ˆoDOΌ#sν ‹± !w=N2B©= φUφ>1ž― !υ“lω{|ώ/'dΤωΘ”¬2‘JgΥ— ₯Κό‘Ή.yƒΚΩΩύμ]μύμμ‡Š((ςΗΎΕώέΙή#Oρ΅ψό8ή‚·βˆ½Vό4ή’@ϋρcπ|ϋqέΨ‘ŒρΨAς“?|Hο ‡98ϊ¬Œ +d>Θ}ΘlσGb˜=|²Gs•Œψh‘±όίY4:Φc+ˆ2ϋŠSΚ΄fΊ1ιLG¦“ΓʖπΈ3ƒY3­˜ΡLC`Ϊ3C˜γ>Ζc$cΉ !D2ο•u/ ¬aιŸ²/ST9ή°Ώι#kŒ—d>g˜œd₯&e Ρ9WE†ΗTΠdΠ$FσΐΔ•¬¨=¬1sΘΪMV-`<6]‘Γΰ(Ν—fO ₯ΩΓZe΅bΡBh΄0Δ’Ή‘rΪZ$`raNΈ\¨z±ˆEp"h“•p2…pΧ(Γ}‘ΰ|i‰Lœ%*dqΰf$dq%|Wg–;Ϋ Ύˆδ=‹œƒΠ‹xΕύ 3θΰΛeEJAΎ¨Hξ`zΘ™#kψͺ»€­^ΘΎ³‘poˆEI( ΝλDKΔΆ -A•¨­BλΡf΄νB ¨BGΡ1tύ€.’+¨έƒ/PzŠΠK4„aΣΐt1cΜ³Εœ0wΜ ΐB±h,KΓ2°,L‚Ι±2μ3¬[ƒmΖv` Ψ·X v»€]Εξ`έXφφ–‚SΤ)z3ŠeΕ›Β‘DQ’(3)Y”Ή”RJee#₯޲ŸD9MΉHι€tQžRq„«αΈ%ξ‚{γΑx,žŽgβ2|!^…Χΰux#Tvό:ή…χγo‘K°ΘM‘Lπ‰ΉΔBb9±™ΨC4g‰λD71@Ό§jPM©NT_*—:šEG­€ΦPλ©G¨η jχP_h4ΰ…π%–M›O[NΫJ;@;E»J{D€ΣιΖt'Ί?=–Ξ£+ι›θϋι'ιΧθ=τΧ 5†ÝΖHgHεŒΖ^Ζ Ζ5ΖcƐŠ–Š­Š―J¬Š@₯De₯Κ.•V•Λ*=*CͺΪͺφͺώͺIͺΩͺKT7ͺ6ͺžS½―ϊBMMΝJΝG-^M¬ΆXm£ΪA΅σjέjoΤuΤΥƒΥg¨ΛΥW¨οV?₯~Gύ…†††FFΊF‘Ζ +35^3u™L.Sΐ\Δ¬e61―1ŸiͺhΪjr4gi–jΦhΦΌ¬Ω―₯’e§¬ΕΣZ¨U«Υ’uKkP[WΫM;V;O{Ήφ^ν Ϊ½:t;PN…ΞN3:tq]kέ`]ΎξgΊ»tΟιφθΡτμυΈzΩzΥzίθ]ΠΧџ€Ÿ’_¬_«\ΏΛ7°3ΰδ¬48dpΣΰ­‘™!ΗPhΈΜ°Ρπšα+£qFAFB£*£FFoYΖ‘Ζ9Ζ«?0!LMβMζ™l39g?Noœί8ώΈͺq‡Ζέ5₯˜:š&˜Ξ7έiΪa:hfnn&5ΫdvΖ¬ίάΐ<Θ<Ϋ|ω σ> ]‹ ±Ε:‹“OXϊ,+—΅‘u–5`ija)·άayΙrΘΚή*Ωͺάκ€ΥkUkoλLλuΦmΦ66SmΚlφΩά΅U±υΆΩn°m·}ego—j·Τξ¨]―½‘=ΧΎΤ~Ÿύ} ‡@‡Ήu7ΖΣΖ{ΟΏuόGŠ£‡£Θ±Φρ²ΕΙΣIμ΄Υιͺ3ΥΩΗYβ\η|ΛEέ…γRδ²Ο₯ΫΥΐ5Ϊ΅άυ¨λ³ 6'¬žΠ>α=Ϋƒ ί·{n:n‘nεn­nΈ;ΊσέkέoLԘ6qΡΔζ‰Ο'9MNΪ6ιΆ‡ΗT₯myzyΚ<=ϋΌlΌ2ΌΆxέςΦσŽσ^ξ}ή‡κ3Εg‘Ο1Ÿ7ΎžΎ…Ύ‡|χsρΛρΫλΧ;Ω~²pςɏό­όyώ;ό»X_tZςλ²Υ=ζŒηdsφsžMaO‘M92εU°oπ‚ΰS!xHxHUΘ₯PΠδΠΝ‘ì²Βφ… „{„Ο?AˆŠXq‹kΖεsΈ‘^‘ "ΟF©G%FmŽϊ9Ϊ1Zέ:•25rκΪ©χclc$1GcQ,7vmμƒ8ϋΈΉqίΗΣβγβkγMpK(KhOΤMœΈ7ρe”€•Iχ’’εΙm)š)3RR^₯†€Iνš6aΪ‚iΣLΔiΝιττ”τϊτΑι‘ΣΧOο™α1£rΖΝ™φ3‹g^˜e2+wΦρٚ³y³gP3R3φfΌγΕςκxƒsΈsΆΜΰσ7πŸ +‚λ}BααγLΜ5™½YώYk³ϊD’QΏ8XΌYό<;"{{φ«œΨœέ9rSsδ1ς2ςZ$:’ΙΩ|σόβό«R'i₯΄kοάυsdQ²ϊ¬`fAs‘όSΪ!w.ο. +(ͺ-z=/eήαbνbIqG‰cΙ²’Η₯a₯_Ο'ζση·•Y–-)λ^ΐY°c!ΆpΞΒΆEΦ‹*υ,_Όg‰κ’œ%?•³ΛΧ”ωYκg­f‹+}ώωΎJf₯¬ςΦRΏ₯ΫΏ ΎqiΩΔe›–½―TύXΝ©~·œΏόΗ/έΎάψ凙+.­τ\Ήmm•dΥΝՁ«χ¬Ρ^SΊζΡΪ©k›Φ±ΦU­ϋsύμυj&Υlί ΊAΎ‘kcτΖζM6›Vmz·Y΄Ή³vJν-¦[–my΅U°υΪΆ mΫΝΆWoϋ•ψ«Ϋ;Βw4ΥΩΥΥμ€ν,Ϊωλ”]ν_{έPoR_]ΧnΙξ= {Ξ6x54μ5έ»reŸ|_ίώϋ―|ςMs£KγŽͺ’ƒςƒOΎΝψφ摨Cm‡½7~gϋέ–#ΊGͺš°¦’¦£’£]ΝiΝW["[ΪZύZ|οϊύξc–Ηjλ_yBυDΕ‰'KOž’žκ?uϊQΫμΆ{g¦Ήq6ώμ₯sQηΞφΓ™vNϋΙσώη]π½Πς£χG/z^lκπθ8ς“ΗOG.y^jΊμuΉωŠΟ•Φ«“―žΈxντυλ?άΰήΈΨΣyυfςΝΫ·fάκΊ-Έέ{'χΞσ»Ew‡ξ-†‹}Υ­5MΦύkόΏtyvοιξψ9ρη{ψžώRπΛ»žŠ_5~­ylρΈ‘Χ½χX_Xί•'ӟτ<•>κ―όMϋ·-Οž}χ{ΠοΣzžΛžψcω γ»œτgΫ`άΰΓ—y/‡^U½6~½ηχ›φ·©oΝ{G·ρ―ρ΅ΎzCή‡ ψb +endstream +endobj +746 0 obj +<< /Alternate /DeviceRGB /Filter /FlateDecode /N 3 /Length 2612 >> +stream +x–wTSΩ‡Ο½7½Π" %τz ;HQ‰I€P†„&vDF)VdTΐG‡"cE ƒ‚bΧ ςPΖΑQDEε݌k ο­5σޚύΗYίΩη·ΧΩgο}ΧΊPό‚ΒtX€4‘XξλΑ\ΛΔχXΐαffGψDΤό½=™™¨HΖ³φξ.€d»Ϋ,ΏP&sΦ‘"7C$ +EΥ6<~&ε”S³Ε2Κτ•)2†12‘ ’¬"γΔ―lφ§ζ+»Ι˜—&δ‘YΞΌ4žŒ»Pޚ%ᣌ‘\˜%ΰg£|e½TIšεχ(ΣΣψœL0™_Μη&‘l‰2Eξ‰ς”Δ9Όr‹ω9hžx¦gδŠ‰Ib¦טiεθΘfϊρ³Sωb1+”ΓMαˆxLΟτ΄ Ž0€―o–E%Ym™h‘ν­ννYΦζhωΏΩί~Sύ=ΘzϋUρ&μϞAŒžYίlμ¬/½φ$Z›³Ύ•U΄m@εα¬Oο ς΄ήœσ†l^’Δβ ' ‹μμlsŸk.+θ7ϋŸ‚oΚΏ†9χ™ΛξϋV;¦?#I3eE妧¦KDΜΜ —Οdύχγΐ9iΝΙΓ,œŸΐρ…θUQθ” „‰h»…<X.d +„Υα6'~khu_}…9PΈIΘo=C#$n?z}λ[1 +ΘΎΌh­‘―s2zώηϊ \ŠnαLA"Sζφ dr%’,£ί„lΑt  +4.0,` €3pή „€H–.Hi@²A>Ψ +A1ΨvƒjpԁzΠN‚6p\Wΐ p €G@ +†ΑK0ށi‚π’Aͺ€™BΦZyCAP8ΕC‰’@ωΠ&¨*ƒͺ‘CP=τ#tΊ]ƒϊ Π 4ύ}„˜Σa ΨΆ€Ω°;GΒΛΰDxœΐΫαJΈ>·Βαπ,…_Β“@ΘΡFXρDBX$!k‘"€©Eš€ΉH‘q䇑a˜Ζγ‡YŒαbVaΦbJ0՘c˜VLζ6f3ω‚₯bΥ±¦X'¬?v 6›-ΔV``[°—±Ψaμ;Ηΐβp~Έ\2n5·׌»€λΓ α&ρxΌ*ήο‚Αsπb|!Ύ +ߏΖΏ' Zk‚!– $l$Tηύ„Β4Q¨Ot"†yΔ\b)±ŽΨAΌI&N“I†$R$)™΄TIj"]&=&½!“Ι:dGrY@^O$Ÿ _%’?P”(&OJEBΩN9JΉ@y@yC₯R ¨nΤXͺ˜ΊZO½D}J}/G“3—σ—γΙ­“«‘k•λ—{%O”Χ—w—_.Ÿ'_!Jώ¦όΈQΑ@ΑS£°V‘Fα΄Β=…IEš’•bˆbšb‰bƒβ5ΕQ%Ό’’·O©@ι°%₯!BΣ₯yΈ΄M΄:ΪeΪ0G7€ϋΣ“ιΕτθ½τ e%e[ε(εεε³ΚRΒ0`ψ3R₯Œ“Œ»Œσ4ζΉΟγΟΫ6―i^Ό)•ω*n*|•"•f••ͺLUoΥ՝ͺmͺOΤ0j&jajΩjϋΥ.«Ο§ΟwžΟ_4δό‡κ°Ί‰zΈϊjυΓκ=κ“šΎU—4Ζ5šnšΙšεšη4Η΄hZ ΅ZεZη΅^0•™ξΜTf%³‹9‘­ν§-Ρ>€έ«=­c¨³Xg£N³Ξ]’.[7A·\·SwBOK/X/_―Qο‘>QŸ­Ÿ€ΏGΏ[ΚΐΠ Ϊ`‹A›Α¨‘Š‘Ώaža£αc#ͺ‘«Ρ*£Z£;Ζ8cΆqŠρ>γ[&°‰I’IΙMSΨΤήT`ΊΟ΄Ο kζh&4«5»Η’°άYY¬FΦ 9Γ<Θ|£y›ω+ =‹X‹έ_,ν,S-λ,Y)YXm΄κ°ϊΓΪĚk]c}Η†jγc³Ξ¦έζ΅­©-ίvΏν};š]°έ»N»Οφφ"ϋ&ϋ1=‡x‡½χΨtv(»„}ΥλθαΈΞρŒγ'{'±ΣI§ίYΞ)Ξ Ξ£ πΤ-rΡqαΈr‘.d.Œ_xp‘ΤUΫ•γZλϊΜM׍ηvΔmΔέΨ=ΩύΈϋ+K‘G‹Η”§“ηΟ ^ˆ—―W‘W―·’χbοjο§>:>‰>>ΎvΎ«}/ψaύύvϊέσΧπηϊΧϋO8¬ θ +€FV> 2 uΓΑΑ»‚/_$\ΤBόCv…< 5 ]ϊs.,4¬&μyΈUx~xw-bEDCΔ»HΘΘG‹KwFΙGΕEΥGME{E—EK—X,Y³δFŒZŒ ¦={$vr©χέK‡γμβ +γξ.3\–³μΪr΅ε©Λϐ_ΑYq*ί‰Β©εLτ_ΉwεΧ“»‡ϋ’ηΖ+ηρ]ψeό‘—„²„ΡD—Δ]‰cIIIγOA΅ΰu²_ςδ©””£)3©Ρ©Νi„΄ψ΄ΣB%aа+]3='½/Γ4£0CΊΚiΥξU’@Ρ‘L(sYf»˜ŽώLυHŒ$›%ƒY ³j²ήgGeŸΚQΜζτδšδnΛΙσΙϋ~5f5wugΎvώ†όΑ5ξk­…Φ\ΫΉNw]ΑΊαυΎλm mHΩπΛFˍeίnŠήΤQ Q°Ύ`h³οζΖBΉBQα½-Ξ[lΕllνέf³­jΫ—"^ΡυbΛβŠβO%ά’λίY}WωέΜφ„ν½₯φ₯ϋwΰvwάέιΊσX™bY^ΩΠΰ]­εΜς’ς·»WμΎVa[q`id΄2¨²½J―jGΥ§κ€κšζ½κ{·νΪΗΫΧΏίmӍΕ>ΌΘχPk­AmΕaάα¬ΓΟλ’κΊΏg_DνHρ‘ΟG…G₯ǏuΥ;ΤΧ7¨7”6’Ζ±γqΗoύΰυC{«ιP3£Ήψ8!9ρβΗψοž <ΩyŠ}ͺι'ύŸφΆΠZŠZ‘ΦάΦ‰Ά€6i{L{ίι€ΣΞ-?›|τŒφ™š³ΚgKΟ‘Ξœ›9Ÿw~ςBΖ…ρ‹‰‡:Wt>Ί΄δ°ήˁ—―^ρΉr©Ϋ½ϋόU—«g9];}}½ν†ύΦ»ž–_μ~iι΅οm½ιp³ύ–γ­ŽΎ}ηϊ]ϋ/ήφΊ}厝‹ϊξ.Ύ{^ά=ι}ήύΡ©^?Μz8ύhύcμγ’' +O*žͺ?­ύΥψΧf©½τμ Χ`Ο³ˆg†ΈC/•ω―OΓΟ©Ο+F΄FκG­GόωŒέz±τΕπˌ—Σγ…Ώ)ώΆχ•Ρ«Ÿ~wϋ½gbΙΔπkΡλ™?Jή¨Ύ9ϊΦφmηdθδΣwi獵Šή«Ύ?φύ‘ϋcτΗ‘ιμOψO•Ÿ?w| όςx&mfζίχ„σϋ +endstream +endobj +747 0 obj +<< /Alternate /DeviceRGB /Filter /FlateDecode /N 3 /Length 357 >> +stream +xuΏKBQΗΏj!˜ ‘ƒCΓ›’αU’A. j EΠΓ +²¦ησW ―Λ{OΒh‹Φϊ2h’ƒ–†† jˆ¨­Ι©ΐ₯δvξ{‰.žΛΉησΞωžsﻀ;¨2VPΦ-#•ŒK›ι-Ιϋ -ΫTΝd1EYέθTΊ{ϋΕΡ>M‹Y­Nύ8z”Ό+_ή¬ξΏg»ͺΡ—Ν™Υ~ΙC3,ΐ%+{|@0θRΔ5Α‡ΟgΎ²5λ©iˆ%­¨f‰[Δr¦/_θγr©"Ξ&ώԟΣ7Φ(Θ'°ˆ˜`(AEDθηl}»€Β Ύа¨'F1!GΌf ‡"‹w¦^²φσϋΙ½άα°Πδœ_χrΛMΰbπ5zΉ©(06ά7˜j¨φ8νξ|ψ:FΣΐψ#υl›ωHΨ»όq`ψσοIΐ{tjœœrή©ž7ΰV)jΌ +endstream +endobj +748 0 obj +<< /Ascent 975 /CapHeight 722 /Descent -217 /Flags 32 /FontBBox [ -576 -287 1987 1160 ] /FontFile2 845 0 R /FontName /AAAAAB+HelveticaNeue-Medium /ItalicAngle 0 /Leading 29 /MaxWidth 2225 /StemV 0 /Type /FontDescriptor /XHeight 524 >> +endobj +749 0 obj +<< /Ascent 952 /CapHeight 712 /Descent -213 /Flags 32 /FontBBox [ -951 -481 1987 1077 ] /FontFile2 846 0 R /FontName /AAAAAC+HelveticaNeue /ItalicAngle 0 /Leading 28 /MaxWidth 2225 /StemV 0 /Type /FontDescriptor /XHeight 523 >> +endobj +750 0 obj +<< /Ascent 975 /CapHeight 866 /Descent -217 /Flags 32 /FontBBox [ -1018 -481 1437 1141 ] /FontFile2 847 0 R /FontName /AAAAAD+HelveticaNeue-Bold /ItalicAngle 0 /Leading 29 /MaxWidth 1500 /StemV 0 /Type /FontDescriptor /XHeight 650 >> +endobj +751 0 obj +<< /BitsPerComponent 8 /ColorSpace /DeviceGray /Filter /FlateDecode /Height 587 /Interpolate true /Subtype /Image /Type /XObject /Width 512 /Length 13388 >> +stream +xν]g€SEΧΎ[¨K[:«¬τ ―„D£‚° % ‚Qy1ΔX( +EŒ(ΎΕ#ν#4 έΠ,ΰβR–Ξ²lϋ²Ι&Ή7Ή=sο$gl¦žsζyζΆ™33‘¨ι™-Ϋ«o {j¬i²eΆmŽΓαtΊά›½^ορΒΐ_Y…―΄*šοΛΪμv9Η·ΆΩ–Ι¦ηFκϊχU·»635Q1J¨v5hwΛέCF›,ŸΝ_φΧΎWδ’ω₯`ο†₯σfOyΩ »λζΆυ +ΆΈnLƒŽ9χžhλάώRΒΩ»Mρ±mΛΎŸρϊ¨Α½:@_P ΤΘRk »Σs–'9r‹ςέ«I―QAOΌ'΄Έν‘—gύφχ9xγτΞάO& ιή\r’NA£OLό⏽W„s’D’έKm―=~KfΡ„ΎΑ™jΙζΜW‚ΕΨuΊCN}τ¨$Δ΄vχΏϊέΖΣ±“ Ό„kΏž0°MJ†€‰ιΩZ“έ}IyήΠZPμq˜uͺ4$%ͺ^™ο‘ρ3-Γ|€m±ΏtGΓDεO|»²΅ζά8}ΜσαZ&ίiΥ«`L1Π]»>mϋλ2 dˆ]mΪ>Ιί +²΄fηΕd ›©η\V]rŽΤ`^|‚ –δJΟϋqBŸZ⟝ρW³ΉΦβš{K.¦™[[βΆκΗ“Β-ΞΦΫ<Μ0$wŽΧnΘŽhόΤθ4vαΙδf˜»υyφ-γ‡Qώ–6ΧΩςΈ[%*πΪtψ#‹ΙΊ‹»¨‚@™Η¦Kˆ ε”›'m€w=!Τ‡Κ^Yφ|[ό/n6 3΄Άc‘ζ@@^›ΆΒη©&¬Lθ±|lŠͺrώ§§Z`L3½i*σnQ…J΄xΜιqΖ155Ηz”Ά³G²#mJΛ±‘•P•ƒΦΌ'ŒRοό\γ>λπ”.‘W6q•ωPάγ π˜q$ΎΞΈ5°K Λ\FΌζŠκY γ{²φ­’ƒqΉρ«mIνΓ!+ο$eΗ­8Ό +47ξ$ΩAYp2½ €ήϋK‰¬ eœύτ&Εz@=Γk ͺn}5%Ί@;λ *i8nΉVζ:Θ /ό4L(•Tό}w{@Qϋ”j(θeB`έ`™V”Τ1Βμ Цο7Φ”ώ&ΠĜ‹r%J2εΗΝ―6Οϊ8 WjIF—‚O½)‘Χ`c °/ghEž6KΤšΟ‘΅€IƒΐI“«ΙκN‚™}iθ’@κ±g«£}LΥƒSs˜ξi0δ›ψμϋZψύ&b)πμO +ϊ+*fΠ~LO’ΦC3+&t€α€K P>$ͺά'‡+' G’6τΒ  μΣ.)ΰΈ ?Dέ@0m-n‚="°„ς0AZe ?F‘ž`]7~IlΡΉπκ Τ5λρ"πgθ 0Cλΐ$ΙZυ¨ ϋ9IŽ5Ž +U­£q`“τό7€:k 8"πŸ˜p4 l’ φφj,u¬σ=nΐ20J|§ +Β΄―,Hγ©d2AΖΣ2°JφYr¨Έ"Π|–]r p?<ώε€[i`˜ ό±E% [φΐλ?ΆάΘaXα%9΄€\ΈR„«e`—”ό#‡Ё+ηΆγjΨ%‡WΚ‘tΰŠΐš/p΅ 쒁/FΚ‘tΰŠΐ“q΅ 쒁)'εP:πDΰT +ρž–Ur π=Aά%‡Ё'‚H=‚§i`•τ«ά l²τz@žΌ[ΉδZ˜ΐ“Ι­*jYΙ?1CrE K¦ϋι'šœΗ:0Jb.6 πo©xŸσΰ/Γ‹©…`–„䟸£\B= KΚο ΡO6,M£$D`6‰~’ΈJ5Ž’IwʎРvΓ‘%Ιl*κNΎό}αa’©Α"πTύ°C’€3ι£(ϊ‰t§tκ@2^ό‘Ν?Qo#^F‚5R!°!όεOξ 6K₯δβ„ΐΆ†dΦIα&œΜ[€A`Op؟Δ|U0kŸ4*A*>μmΝ{(₯Ρz| K€@`SΣΩtŒί₯P +2qAΰΟzt¬“ΏΒΕT°=ίW#QMLy½Zˆο‡Άό§η>ϊΘE<¬+Π"py8λ€ΌŽ»Π*i8 °?ςΜ7γΑz q0l@‰ΐβΜ’Ω’©―’Τ ²”FΰκkL§~3tƒ[φ(m2θG‡€·'ΝΜΙ΅¬ΰˆŽe%Ω3˜yfΞΉφ†R–6DΪ ΄Μ³ζ4±#²Δ(‡@ωκQΏ¬ŒGfφΩ­œα ξŠδTPΌ¦Ξ…GAƒB2Zj +’›¦p—BΆƒΪ˜ψΏΞ4„ +NŒΩ GυΌ†ϋΉϋCuγ9Μ•1!pΡΜλΦ 7ύΎ­₯1•eF Μޜ±ΧζwγUŽΈ Cd¦0u‹oδΗκ yξγW”θ‘‹EPW>\}yRzWεs½τYž₯‰^+δkh‹ΐΊ;ωςωTΥ$Ÿ•χάPΏΔZυδA`#yY?kGH1‡,šΟ~ ž!Ψπ Έ΄Ό?ωκόH2OΦBΙΌΡί$θπ –εφ ΕιΈ“bψ呬₯©™ΩVΨ2‚‘b{'*K¬±‘Q>žφZ¬¨™ΧΌw‡6ƒ A +ήΝ’2Δ«a Φ#ύnΉž΅NDf έ:R]*Šΐfƒ‹—hEΏϋΜ#sDΥvpT”φ€ς«ί.ήBώ;ΛdυM„Θ!ˆ¬·Ž1‰‚ty8jf[ΟICg“,†»IŽFl΅ιςšw°‹„\iΨkb^ΙOG”/­χFf¨ΛœœΗυ9!M“Aj‹φ>Ό‡zBΜ ΰυΈž+δS’Jv--<‚ΤH[κΤΣoαbš.pΝ<ž–]4W§«Ο‘–©wŠžΗTΜm|ίχQ—.Δ‰g8―Ρ–Ζ-1΅ *s"°Λά–γ:€ΟξCοεΤ“8„^Kκή¨ˆΣ(B`‡Ή+ τ,Y-μ‚oΝη^󨴑Ρ%X[¨‰`BΐcξΘΒ0[Vυ—Dωoζ*υא[ΒΤHŽ@™Λ$ξΆοc/Ew@ΈΒ@Ώu’ΨχWjϊΤQ½N¬­‰[οά‚‘μ›v±’€‰iWO§šU8{fZŽΕΈ΄ΘΣ2―U#φ9\IΞΝΖhfΉ£;ΙΉ­ 8^L$—&±ό+­„ΏφE›zυ+яž€΅ϊOέR-RΨ(ΫόΑ=‚&u£―Γφί šš)ΛεΉH Ϊ†`J] < +Ψθ¦ζym:ΑΣ0A€ƒΏ]μίΐΛso ΚΫrΔw“OT’3–έ‘C/dfώcG}ΗuέA–/6œ₯³Ίap€©g{ν•πYh.zεJρ +-οuΡ&‘Ršj-.ΨU ’”zlzΧ½ζΤΑ«"d#‹zM H<ΖΜΈcoΰBZELΑo“ξΰΏƒφΊI·l9o‹ν‹„b|–Φœ{YηŒKA]V=’[~ΨlK‘Τ0”9ω―1‘°MIλϊτη[}§Hέt΄ς―n±=uƒθv:4sς¬ΜΩ6šky:λXUzKnZxq–vΦe5δπڞ΄ˆ¬ϊcΆΛΧδ"‡Ε{*΅ ­MϊΙ+Ε‹«|Έpj*χ.xcP+j»QΔΤΆ œΊΡΨcθ,Ξ―™ΥU:³Γ#Ο} - \ςΎ‹^„Ϋ7n 2^ϊαf;΄H_€†ΦV›²h?Β¬°Υς‡Jφ2ε‰n΅IΝCLΝ±)wv³Χάe["dUΛΦ,wά.6-ρ:m&:ΖaόP¨Ρo”ΏCS4zL Pΐ#V­έ€ηgώΊ#ŽΌ ΞnΥ:n@;ξΓvx΄ž₯HŽVe.βοϊ6Χςί\Ώ"zόΌyϊ³₯ΦΧcδbUδΈρη KΣkwμϋΔ§Ο[γΕδΑpωΐšyΣΗνΫAͺG|4΅X€IγCΟ‚ΛΉzYξd0ͺξφ”―]ο•ϋλ§βuΏ~=εΕaw«˜NΣ%Š4\[kΗσYXδ΅2 85[ͺ χΖ΄―ΨzH’Aε3‡Ά¬XψυGoŒ6 [Kωξvθ4Τ;p^kWκ2Κςτ‹@%*š™έ­—F§cz{ͺmξBηZχ―χίB^žhη υz·ΈΧ:Ξ΅M}Ϋ4Z―ΣτΊ©Σ“’¬A•ΠXŸ‹PyΩϊ‰έΠ’‚°nfΣl_g5ι―s ­i¦$γ3HlOΉyFΤ>‘‡7κΐ ‡ΥL1πβ]H†ΦΖkι.jcWκ2Εβ;οŒ!΄?Ϋθ,ށ«΄ ‘d’!Έ˜‹jςΘ‡$jŸ/k}ΜAΖΤΌΪ>ŸιΈyβ³υ”·%¦E,˜ς#₯Yij“3‘|$/,{γvωΖΗ€€FrΩ΅ϋMϊCφρ,ΆλU^‰ΫͺƒχΦώSO“ΰΎΡεžΟ‡ΕΆ’Ώ8ΞLi―bͺK o9η\V˜=lβ˜]vΣhΜΉ'ρζ ΉuωΉf Ό9F»'Α½ϋNΙΆoŒ·'νHaζν/|»=Aœά)ζ‘‘ŸkAΉ‚ύV‹Gn–Φ”ΌW=m—(\1}TΟ$Έdφ5c₯$Τ΄°ΖYb‘ΛfΤ$θ«a}΅o• Ζήkψt•|ηΜηξΙΊ΅5wv+―ΏgμL'μ} °ƒ]υ»O«€Zc@Cς€,Ώ»rώωΗ²xρξίf½<€;Ώσl‘3(N`‹ΌωΚ4ϋ’-Η`G~κ£4^>²ΩιψόƒWŸy¬Ν$xFΤhΦ±GǞyνƒΟtn>―tQ `•pι€wΫzη‚οl–7'†κjϊ©Υ²[ffr<323―Λξ VχΣ Τ 5ΌόζϋΆο~rίξ= „cΕo,Ɯ)ττ†ώR`T.X‘. € € € € € €  CΰΒΊ…sœϋΡΙIq„@ω‚- ۚώ#³ΑT4lΉ%μΈVηpCDƒjάHω‰κώxžΫΓΖ œρf¨3©­7,<ˆ7c°χpτΤγbU㠁GΓΟώ`(esœ΅ΜΐΊSyŠ㠁Wƒ=ω7-ΩvŠ3šΫ™Μ{(<‘…1§C”S&ŒMΣ"ΰ¦ΠŠ η©Φ5ρ +Χbε”ΐγόμ-9Κ―”’ςS3’Ρ Ο.γ< χ …φPδΏόμ=·_9(%'+»‡xμό3‡βbϊ=4?γ¨V•}p9ΏrPJ>Κ͔㓆rμ&2(ΤWȁΓόμ]5›_9(%e:2Ύp?φΡ|{Dq΄;Osgλy„br!πFŸΓYU—vŠͺ@KY«„3‡΅ ‡!„›(7΅‹X ˍq/kRf[β8)AεΈ‹ζjξΘΎΕPΤ£ ίΡί‚ψRω&ƒavΣΠO„ Π„Κž§VκΐΫ π+‚θM#’Cΰ}*•U±±φ|E>bν!ώϋΨτ&ˆ”²![Nς#— §_Ϊ4;ύξ5\eΓω*ί&…γRiωoΒmWΩϊO&Ύ0υΗBξ’α“*•5MȳƍŒ―Πu΄ό§Kˆ MύΚ&K"„’(]Γ΄/γγ™Ofrs’,tΑɁΎ–Ι}ξd’΄ηΙF~€S{|Εώ!η…~4·›pΙ¬ΊΧΌ.…tY…@Ρ³€ƒ,:βΐeZ'ԟ9j‰Κ~=¨£ζnQυ‘ώΉ5³7m{₯”ΑW―a—IŸ»§VP:qη,3½HεDΰBΧʁ@ΚημuG”―Œͺ%8°τ6’’©μ6Ah†P«oaφ7ιi¬λd­!.sjPxεo-xˆC‘«ΦJ2ΚUaŽΉΩιQUxϊςpΩBΙίΎϋWκS§δB9Qdϊ8.η1uΰι|^‘d0,FD9YL^τΤ¬χaδ"4aε`”€ώςH‚x“ΖHЁO#²@”s4gΛ=‘z·Iρκ_αψ ιπR~б­P=qTŒƒ±Ρ%#RvΌ{GΗ†νϋNό+"MτGΊΫRυίΡ)a 2Nύέ.‘DΘΨ*†jAΤ–δV£D±ΡωP$ƁΈ²SξkλΠ[EΤۈ p bH„cNχ‹J6oCέ QΏ™°mZffGA\™Π­a6g}θL«ΓΧoX˜Ξ€-}ŒξE‹xNA@­2©ε­‰fŸ "Σνaβx Ζ›”k±š£7gΚΨ©”M₯"¨¦φεγ§€TβMοε›#@N™§TN…G#lŠˆΆ†Οt'Ο³DoΠ‰&iKλš™£΅Ύ&J³ pεωja€UλYJJš5‡:α6‰6dPtˆBR δΎTΐΡ:-η[₯ή­.hifNlc;JΩ¦3Ώ]ΚwU&BΕU’6Άgfš!'έt½ Q J-ΥHfMΎuŸΖ‚NΤ’„b₯>YΛΜΎσjCAž”X™η{8ϋ@Χ X"eD`+ιdNΊ£ €ΰ, ΩB­κ’)ςhˆhŠ9RΏ0Τ¬Θ&oaKryeχΫ*›Α !»π’—»Pͺ>‘Y JNyέϊω d˜aŸhYXC₯€ΨΚξηΌθΫφ Ω[}ΝΉt•ν 'VJη΄ag3˜{Ϋ‰Š+O#Ώ­mJ ^Η +G’Υ/ύ\sΜΔ¬ΘMΉ™);2½“‰αί—œέ"ycŠ«θόš4AΙT6~γ" V’γiYΈ8ί„‘ΠτΩ‘fύ؟†±()£‹¦…BΈα(Λ₯n6Bβ-*ؘΌ +ωhΟ¨|Ζ„VVπ ΐxΏ=ΕφŒ€Eeά|†­§zT5Bcσ)Jeˆ`€ΐyλ5τlΡ¦’fφ~i@[>1γ…—0b8V&x‰ίχ~€ΞΊσiŒίχzRs0 @ƒ’"I.―±Ύ “7ξ§΅ςςsž>YYπ ΕQζΔσΆΘ½\‚<Σ¦Œ«όκ§ύ[Σ–Ύ +SjνQμO@k$!°s\=&vθΣ[/§S•vΙ(μ@,ά›Y°(„¬XΈμΠΠ“Μ˜šbΰΨθk™ΰ)γΊ\Ž…E±uέ¦ιo‘Λ©μ¬POqŸ²6oζ” "P0]ΨSίί%t§ωΨ°‚ώDqΖNεΟP[ ψΘ†2(ΈμΠς²ΣΦn OέW­‚o,>-i;œ$ΑαXŠ•:υbθ©mΎΒ_λ1}ΈΫΥεΒμ”E”,skΙ°β/ͺψ„^ήI¨†@ωΖ#ΠΟD ΔUJWŒ½V+m…ονwe²Ψu΅΅vŽŒ$ζPtΣK]ΖζβΘ'κ½'κ’uσμή'Ψ‹‡sgΔ-α„”GΰuZώ'pΆEV}<ΗIVe5)A₯θ&’ΊΛ¬ςΉύ^ -ž9ΘU6œΏ£RΕΛα8„Gΰzλα`kv^υύ—ΏngSˆ3ΎRCΨά,%£τ›ž7Δ€Συύ=μ5I„ƒP'~˜bœ4k3‹³cψ’'…Ϊ‘d‘ šκ  ’hXΦ·Κ?»εۜωtƒ9Αw8F9sρΰŽκFζ23dJ›-βχ>ιͺ_α¨%*{\P~΅Ν’κC%>젞~:…½ξ )”ίuμ•Dεn +»wεΕ(JUW:~-…H_δcv4E–χΕ₯Έύ_Ή€h»M+²^$”Α΄•¬ΒvV‹ͺ‘Ί–΅†ΈΜΧΘjΰ DΞZ?Q +ίΐξΣσIT•w8Υ/πWψξ_©/#Ž!ν£Θτ%|Ο^ρ…ˆ:Oρψld—{‚ϊVBwΒ‘„Ρ(ŜBοœ;˜Cξ,ς# ΥΒQZLφΥθafψ$GΙ—r šΑξΠSQ±χ‘ΠΙFƒvp¨•νcB_‰’•ΨJΛ?Ανt}lφ^ν{><σ›tΡy6:³-*2 pΠρCq™’ηS·‹ΪX-W&ύΙ£†<τ„Ωχϋ·’,©A2…¬Ξξg¦¨Ρρ©\O†7>―dkœ5Γ†D„2V+iXκώ(ΰ@΄ƒ’-]Aυ£XcΎ’¦%žξ½Tx«b\ή\Rβπ3ϋɈi³₯Tž|²{t€ΤνΚα0+U8Κ>˜ +@ΙΞQψΔ0” +Ι*ŸHcNd“°…¨ PΩ ˆ„—h¦ΨŠ‹+OFC—Πύ({“ WzD@\έ% 6’Gθ––DXη6]…Tor +€Ž5Z+šΠqM›–>M)#Po黀υy*ΤΒς©Ύ΄Ό‡>­‰¨φδ_›†C_9υ`˜[^‘k`c”]1νΟz”›b_–Ε‹tr‘Γ%”€,ε(2q~υ“™†»(8N‘X‰§ykx7‘ ΅ό~«/&ΙΦ’Λf¦ι>ΰϊ?’ Dkο*ϊe<ΈΡρήK ѐK„φœ6„Ιx3Q°ρ7e‰€D2Ά‘μΛFdŠŠvSμ«5ICΧζυ‘CΟ’Ψ―¬€υ’³ +$Ιƒ@žžΧ­Ώ―NQέxV«A .˜™½ΌΘŒ«/jΙQζpΣEh,)#P4η\Ο=η*JΗ1“NΙΉΦ +ŽS‡@ΓUΫ5β˜#£ό#VΎ£ƒΧΩ”ΑF€Krˆ(sΠo!έ RΜUˆ,ds ₯Τk=ϋΪΥδ€γV–|׎BK€ζΌP;6DmRΐX―΅•οή’!ι +vΪυΖ΄dΆάH²κdΪ2΄‰MΜ°a :|‚η­fyοψ—bxΉ…~Mm¨kTΜ…‘b4DH˜ι‚£e0Ε5¨»²9mIϊΔ#ά$ΥTMz!³|M–Ρ\p'=Χ ©j;Χ*v$Ε IτΙ½‘΅’dyί ϊͺδΤζ&p§RήΔ--Ι¬p†«™Ώβ·Σ%Ν"°ΖΠΥμM#/|ρ­­|ΥγΥY’ΙR±=Ή―Z„έ’™{3‹ψ†cλ[»Π0Μ–”nβΔέ Ψ_$Mγ`Ό‘`Œ]ά›VζΤ ½X‰Nά;\~‰οppΈŸ]3qoάΓg Ψ;‘ο ˜¦΄ςšΓΫ,Ζo@eɏ3γΨάSΆ^“ϋaκ+Cέ6πlrΉητ!E~jŽνOP,ΞΩ΅‚–rU‘”i0uSh0ξ5΅vX4΅άU―δꃻφ‡aηJΡ τγυπR[kWtw#nγ·Δε\}ΰΔ„S‹tΎ|ΩμfTΌc΅΄φ3ρ 2–_ΜΥΧαM΅`3[Τh?ŸV^΄ˆμm‘–c…ΧA>σ,sΚ8Μξ&Ρ―e§LBfΒ*+Ci½§lγΩ:(ƊΐΆχrD½ωω¨f Nτ²jŠΞΜ3ˆWMMυΡ}/Ϊ–dLΉδ4 +έ§^„)Ί±’Ά™aSͺ"ΖXzŽ…mΐ9VλΊΎΧ¦­d€Vψ¬ΥŠl wˆμQσΰˆ9Lόοι6d E„5Β_ϊŒάε)ΰ7=Ϋΰ€x#“/:MjαƒπΤ’’εκΤΛ[/fΜ‰jRz―7Vρ€f±#᳊V½Ρ3v¨SuΘOυ>hŒνYθ ιj£γdΒ“(²ηf Ώe[Τ +2VMΏG€¬Υς&dFjΟΦېwOVΛγ 3ίaŒωž`£αΛ’ydΩΕn%ΥQ}tΊ Vψ;ζ%Χ΄Gω/ΐˆB’šΠΞ*νž=X?H榩τVWR=Zκ±sPQγΣqpWg1±lοΌρ½‘"YΗ ΣΉ2…3=ͺΊBυΚ;΄7.*δΞ*ΌκΕNζP.Rδ«œίΧnjϋ}3΅,Mπ•EG—Ly€cΜƒ)$ΪΑZ:§άψΌ]Ψ’ƒ(›κ«υ–\oβ= +]6£¦)C£cKVΫ”qΉΨej›αΜ΅λέ:ςƒΕϋb…ΡΥ}‹?xςVΰΜ­“Sί€d_άν£hώ Ρσ<›ž­1Xξ8ύNΌκuΪL:5Š&¬jάοPzDυŒ]‹ώyFmoϊυχ<7Υ±1¦‰lqύ[\­7:¦>wΟυ±γRaˆŒ₯ζX:υ‰kg­c֜HΫ$‰ΧκΤτ{s]pdε΄acΉοξί‘ύ„5TΘ¨Μ^„ΆΗ*jη«mQ5Œ‡œL•Fo²:\^zB‘Ηi·u9ΩR_ο$`ΪΏŽίΊΗ"Ο]€QύΊξχxωΓ9‹7εΙϊ’p9ο―Εs>œ0βΎξΧIωώCnj8¬2γκQsΘ*f]JΈe1…2ΪάzχΡ&Λgσ–nά[€xXΉΈ`ο†₯σ>³˜ CξΎ΅5a;aV™%™Ϋ‹υώͺΔΪWκΧA~€Υj˜έεΝΰΗ γ'Z>΅Νs,r:έnχ`aa!Cί(φeτzά›œΞEŽyΆO-Η¬Ή₯KvCyι\ Kλχq< vZp5‡ό™Uθ&`$lV#-n>€*ΚάŸ² h•ΙwkμOΨu’׏(3–j3΄Άxέg§ΤmΡΔΕΝKβ}F₯«MNŽ ;Bo^˜.!pfΕ•iνςΉL'Θϋ•--V‰&ύϊQσπΫEvWΙύݩ=ΡX¦oOΆΑ~μX Ί€ΘΑ™·H­tOπ“—‹ΦΌ§²Υl°Κ― ΝMqΕω»ο›M~Y‹h1?pρ.•‘c΄γ7‘Γ›MqύΎΟςϞ`Φ’άώ†ΟΖτPp:EΙސqΫΫFY§-Ε]¨Χͺ|ΔΕ”ͺξ’YyΓΗΑyAznykΘ_:υ©ξυP!Œ«œϊ=žϊpΩqή $]AΏ›΄˜ν'q%­dτ―Œ­[€·ξ«σλή„XΎBLj2i%‡WΝ1?ΩΏKΓΨΨ‘v£.ύGΎ5gυaΈΰ₯ν EΦόoϊKχnΗϋάRι8χνxάΎΟΠρΣη­ρ*½GZΠ±”^”οsΎ·š Ϊ•¬ΎF5³T9:£Ω·Α‡ab3.Ω±zΡ·3ΜΖαƒϊt½.ιΫBzf«}7šgΜY΄zΗ‘ ˜4Μ`A ΈπΗν\4ΟφΡ»&Σhƒ^§ΣhzͺoΚφύ5 8ώVmk™ˆ5¬ΜΉIέS£Ριτ†1&Σ»Σlσ~uΊw*„9œ! @@@@@@@-ϋηN΅X—Βz"΄¨Ζ‰΄’>n΅Ψ'&ƒ™θψ³UΨΓ1e¨2§ k Hˆΐ'Τ-7U8ν -°)P\8σ#Οgn …£·5<Ρnπχΐ2ΓΈ₯S°αχ…Ÿύ‘Π|ΑR Bœ"°!D:)Π5Nf Fΰ%να ‚‡B nTˆϊ5?ŠE$ԍ.„―yrhdό΄,<2λαπ}玠ετ[(ψΩ{f3ΏrPJN~»!t)·œΓ₯ψΖPYr`6W΅@ώžeόΚA)ω(K&’xπ"»κI”U‘žΫM9§± ‡\Ω(Ahϋ3`7uς'PΉO³?|˜gA(& Ÿ †±«~2ͺA¬c―Κ} +„¬£aσVΛώiUekRf β)AεθE&A¨Ψ· +vE~τdb„ι%( ÁAh' ύρ'»eKκPjυ?Γ^<œ;• `¦( ‘Ι&ƒ‘η9,ΫΧ'X’ κΌΓ~· ‹κβ«Ά…œa…&’Ί™ΣͺίΞ(ngpJμ¦J\}‹S5@ˆ@Wλα Ÿ·τbχΟŽί…yώ=W©‘NBσATŒ\&ʈQ*}υγ~/ΠηB*2Κ7½3rΰ£/όΐγ½μfλα`kd¦ (¨Ιs¬\ΒόΈj ^ΤΥΫΟUνΑ0ι€POjbςσ‚G3σ-£%ιλμ­Z™γ§³Ϊ<>%±ΎΖQKTφSAω©kDΥ‡J|XqΞα0φ―³£©AVΘΏωhXfuXS6Η“@ΙP<ŒΐΑΖd+Γ g…†E–χΕϋΠŒ1νR[’’cΥ( Οδ‡ΰώ™‘l ω`ΝPΑ` uk q™Ο₯WώΒ@ˆœ΅>'£\nΟ~>ˆ=ͺ +Χ;§4V†οώ•ϊZόCS’bE œ΄.7Lλ\v±ζpIh΄‹ΉŽFΑέN/agETξζ*Q.Ÿ‹Ή‘ά@ατ’4³WΊ=Μ0œ½δŠAΰ=Zώλq]ΠGF}zRή#F/W'h삉`.Ԅ珑ΑΩ—tšS‰oτ}»τ~lφ1Ξ’b +ΌOgVΚWbDA6 š v±Υ‘>ΟΉ_@ΐΚΤο₯Wd§η?OQμΤW°‰i°j11ΡΎœ•h§πuΟBlM@άOΜ'£U_$‰ΖδϊCψβ"…Ί( ΘBΆS΅ΣlJš–xΊΟ‚ύΊ‚ ύ†ωκχߚήQΠΆT=„tΩƒι’|ρο­ ŒΏ#ΩG'ω©RUμ£Ή(7έ^ϊ #ναŒϋaϋ@„έΧΆ*tbώvηGC—Πi7Βφ'»¨ς‡" ΞPΜίzWΗS˜’uLvΦΆΏ„2ΡJ\σBΩ‚DύΚ„τ”Χ؝TιMϊΒί‡'S‡ πΞG +\ι+τƒ~ χ€;Α'όWΎΰw΅lϋNtB…IΚ»h¦δL †0{ιβ£nοov!βshΐΔ3sΊPΌ>¨‰η Μ,³δ΄ZS#ΐ±¬ .?`αš6+EJ¬N¨‡ gŒLΣ}΄œS›ΫqiΨ!άk¨Œ +έ{Hœ^¨…ε;ͺ|ΖΤbZ6GΰŠ%ϊdˆ(~ΉΪ:„«†Κ#{=7·όJτΫͺ|kΐal¦ΫHŠέΡ₯Rυp’0ψ.ozG“)*₯ΞD‡―3ξΤΎNέLγ‘•κšΞΖIiπ‹ˆΡήHΆιβΜΰ‚}Ίdή”ŽLQiΝ­—° © Ό8]Bφ+»Lc3χβ₯€f@ΙƟ³ΆuY ͺTΗxDΙ6‚n&Ž›ω{ψb<²pu½Β ؘHζτΖθC"‰CO½oΧζdζBφΆ—/Ή7†Y>Qέ’­όCdη™^αy›J…1Vͺkψ›ήH•ύ¦ˆζb€U@υ”ώ?]•³© +βw ςλ@.Ώ’™†‘6A\.φššςcIRj"*γ$=E²—~ΈSΥ³>HΤH,_;F¦ύ0Λ¬‘–FΕΦ΅I6nv™Ϋ±’‘L¦Κ|7 ўcΦeψε֚ΪοKπ΄Ο~q»ά=ά΄“K€εXσ%E ‰…Ÿ²k#Oϊ#CK85ΗΒyΜE³(²ιGlZφ­{p‘ίo‡Κ SD"‰¦«vКƒΛ·οnΦqόrX9@G¦ΐ΄β?ΗσέΈƒ772¬­±Β>ι¦/°λπϊΞάsT&'ΜPIε+u[βοOΧ?κh,nδΙzU1―MאΛxMk¦³†@ς–>ξ0ˆ]²uχθπΜΕφΎ‹—ήT°ΰ™φX“£qΩz›žτ½1ίaTΗέgžˆώΠLkqΑ§!₯”zlϊ„Όη3u:šΧbψη—Χξ”`½ς₯giΝΉI½œδ‚ΛͺWΔwŸNΪyΔ¬uIθ@taν'Γ;γ=—'c'©Όx“ε½0ίι»κϊ¨ξΥXσίοΆ*z6εL‚HΡΦοΖk’N„Ž"©²4F›+α$/φ8Μ:βM9·Ÿ€^―5}»ώ€— μ"OΦ€½nχb:k΅ΞdsΖι‹A‘Ϋa1h"ZC²Χipσ£―ΜώέsYφkW”ΒKžίgΏςθΝmΒ’Μ}‘ι-ŸωλvL 'Άj}ι‘››$3Cς΄½z–Zk0ΫrέŒ y]«I―QΥ•§ν …Œ@FΫۍ|uΪwKΆ•ρΓρΚΡ-KμΣ^©½­mΩ+‰@έl΅ζαQ&ϚϋϋΊ]ωHΏ /ζοZ·xξ¬Ιž~X£Ξ†+]Išyλί<»[Ξ]ΊαcLo[fΪΎt8~wt»χz½ή“…ώΏͺ;Ζ•@μ€/c―Ϋ½ω»Γρ₯m¦εmӘẻzuΛnηx¬€ύ?Ι[M +endstream +endobj +752 0 obj +<< /BitsPerComponent 8 /ColorSpace /DeviceGray /Filter /FlateDecode /Height 512 /Interpolate true /Subtype /Image /Type /XObject /Width 512 /Length 5305 >> +stream +xνί•Υ†Ο„/$ή¨LŠΰ•IΕΏ …»ΦΞ1©AŠ‚zÏϋJ!`œQΠΞM΅Π‚c/λ%3Ψ*`Jβ€E‰Œ…3EA<Ώο;ϋέϋέΛ•}^.ϊ㜡ήο]Ο3΅ΆsZ-ύΑη'Ο͝_υΤkgηΞM>7Ό1ΌkϊκΥ©ΓΑ ‚»>ψΘϋσίύκμ +»τW:·6N [h΅go-̎.t +lRόΨ²™[nnώγၐkήΌ³0³4d‘ΥΎύυ2?ί ϋ0θTΌ†Žή±9?Ώ)δΰΡ…Ώ„, ίώξvος’όBZτΕΜΊ.›σ§BN>Σ½±&`cWχŽ€ƒN-ϊcδΕn9σ«{ύθ‚…m½Z'»7¦ :΄θ‘ρn9σΏμ}τΖ ―φ^h]ιޘ X0θΠ’?F&ΊεΜ·{έ^°0Ρ{‘΅`a>`Α S@‹ώ1`-ŽΏ”δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±ƒjςoΩρ#δί±œj+Άμόψ«oώ=ύ»ί|υρδžΡε!ԝΜ<τ©˜C΅ΣDΰΓW8ΡۣƚƒΧ›ξΠ{±nωyτήΎwχΨϋ΄Χ‹@η`ψgΥό4_ Ο|Υλ½ŸBΰβo~―aO½k_ΚmΪ !°'πσΒŒQ§-δΝ€xχͺ4^ΨβχΣvχσœ“†τέζ/yκέ»ˆΪhQϊ³?Ylhΐš4^ΠΒO` ½DsQ~ΛσFJΊοBΤ!ZŠ"p1θ Ijƒb^‹ΊCK‘I±ZwηΓT#D ς—vϊ[‚Ϊk8™ΐaCΉ½υώ–O²Q,ΰΊ«Ώ!όV^ΣιB>γΆχ7.iβtϊ=Jΐ|@RLjYUΧ4ƒΐƒ sœŒ-Œ{”ΨΜqΗHΩƒ5Χ4ƒΐn†9NΖ»Œ{”8ΖqΗHωkiζ8χ(#pžγŽ‘2‡5Χ4ƒΐ5†9Nγe 8ξ)hsΝ30Μq2Χ(%ΐqΗHA›kžA€aŽ“ΑΈF(Ž;F +Ϊ\σ  sœ Ζ5Κ@ pά1RΠζšg`˜γd0QJ€γŽ‘‚6Χ<ƒΓ'ƒq2PwŒ΄Ήζζ8Œk”ΰΈc€ Ν5Ο ΐ0ΗΙ`\£ ”Η#my†9Nγe 8ξ)hsΝ30Μq2Χ(%ΐqΗHA›kžA€aŽ“ΑΈF(Ž;F +Ϊ\σ  sœ Ζ5Κ@ pά1RΠζšg`˜γd0QJ€γŽ‘‚6Χ<ƒΓ'ƒq2PwŒ΄Ήζζ8Œk”ΰΈc€ Ν5Ο ΐ0ΗΙ`\£ ”Η#my†9Nγe 8ξ)hsΝ30Μq2Χ(%ΐqΗHA›kžA€aŽ“ΑΈF(Ž;F +Ϊ\σ  sœ Ζ5Κ@ pά1RΠζšg`˜γd0QJ€γŽ‘‚6Χ<ƒΓ'ƒq2PwŒ΄Ήζζ8Œk”ΰΈc€ Ν5Ο ΐ0ΗΙ`\£ ”Η#my†9Nγe 8ξ)hsΝ30Μq2Χ(%ΐqΗHA›kžA€aŽ“ΑΈF(Ž;F +Ϊ\σ  sœ Ζ5Κ@ pά1RΠζšg`˜«ΟXχΒψDθ/Ζ5Κ@ „Ϊ™ίΆΆήsυ;ˎ’m4ο™ΐ‘₯՞k^}dΖσ1κ†˜YY£ΊκεΑχρhΓ7ιEU¦«_{ήχ)jC`K΅λͺW'cς΅γ›ΐρ*ΣΥ―}ιϋ΅‹!πE΅λͺW―ΕδkΗ7―«LWΏφ™οKΤ.†ΐ§Υ«^Ι׎oͺLWΏΆΎγϋ΅Γ tž¨v]ωκ~<_Ύ μ­]σβΠaίΗ¨JΰΠPκκ—6FŸ yΏN=;PνΉαΥUΪ‘Ώό^r³P;ν «43ή*™²ίΫζ8~•άŒγŽ‘R2eΏ·1Μq2ό2*ΉΗ#₯dΚ~oc˜γdψeTr3Ž;FJΙ”ύήΖ0ΗΙπΛ¨δfwŒ”’)ϋ½aŽ“α—QΙΝ8ξ)%Sφ{Γ'Γ/£’›qά1RJ¦μχ6†9N†_F%7γΈc€”LΩοm sœ ΏŒJnΖqΗH)™²ίΫζ8~•άŒγŽ‘R2eΏ·1Μq2ό2*ΉΗ#₯dΚ~oc˜γdψeTr3Ž;FJΙ”ύήΖ0ΗΙπΛ¨δfwŒ”’)ϋ½aŽ“α—QΙΝ8ξ)%Sφ{Γ'Γ/£’›qά1RJ¦μχ6†9N†_F%7γΈc€”LΩοm sœ ΏŒJnΖqΗH)™²ίΫζ8~•άŒγ6eυΖΰ0R2eΏ·λΩΈΊVrέ£gόf(3›±Ÿ4τ&ϊΝϋ&πτσί^ρ}ŒΪαφΥύV_ρϊSϊω―8`η'+DΧΌτΊσ[T/‚ΐXμŠ—ΟFΔkΕ9ΰηΏλσœ»Œ©w­β½ζ₯σ1ωΪρMΰ\μŠ—ίρ}‰ΪΕ8V!Ίζ₯ηbς΅γ›ΐhμŠ—Oϊ>EνpSΐη?ΆVκσ_qΒ7>ΈβϋΌώ₯₯o»ΎFε@o=PοΊϊ5[Η‚?`,£q +`=c[―vΜz•rŽB@,{ι9`qS€{c%PΞQH€e/=,q +to¬Κ9 + °μ₯η€Ε5N!ξ•@9G! –½τ°ΈΖ)½±(η($ΐ²—žΧ8…@Ί7Vε…€Xφsΐβ§HχΖJ œ£Λ^zX\γιήX ”s`ΩKΟ‹kœB έ+rŽB@,{ι9`qS€{c%PΞQH€e/=,q +to¬Κ9 + °μ₯η€Ε5N!ξ•@9G! –½τ°ΈΖ)½±(η($ΐ²—žΧ8…@Ί7Vε…€Xφsΐβ§HχΖJ œ£Λ^zX\γιήX ”s`ΩKΟ‹kœB έ+rŽB@,{ι9`qS€{c%PΞQH€e―2gxΧτU°ΖΈ:΅sΈRrύ‹νY§·¨VΩ‘zΧο΄υQ˜ύ.u/€a}χϋ5Ωμς’Šοσš—vE>CkŽ 쨑]ρ²~ό―c±Υ¦*DΧΌt%φΪσK`ΆFvΕΛϊγί―Ζθf—+DΧΌ4ύ-Ί%όώΏΣν*M`{Ν7{ΕΛϊλΏhΚn/έ_!Ίξ₯ύχ?nEΖλ<]ηΊςυ‘ΛqΡ–O—0ύ­Φ’'τW>]Β­œΨŽόζ_ωBγ‹p#-4*1}“pŒ"`¦ŠWׁ@£Σ7 Η(&`ͺΈρapu-4*1}“pŒ"`¦ŠWׁ@£Σ7 Η(&`ͺΈρapu-4*1}“pŒ"`¦ŠWׁ@£Σ7 Η(&`ͺΈρapu-4*1}“pŒ"`¦ŠWׁ@£Σ7 Η(&`ͺΈρapu-4*1}“pŒ"`¦ŠWׁ@£Σ7 Η(&`ͺΈρapu-4*1}“pŒ"`¦ŠWׁ@£Σ7 Η(&`ͺΈρapu-4*1}“pŒ"`¦ŠWׁ@£Σ7 Η(&`ͺΈρapu-4*1}“pŒ"`¦ŠWׁ@£Σ7 Η(&`ͺΈρapu-4*1}“pŒ"`¦ŠWׁ@£’δ7Χ½0>ϊ‹pŒ"`‘v&Ζ·­EΏ–…λhΑ1#K‘―€Gfί’jfV_ƒοGΰΟ€§τσ_c»ήι<ώπΊλKT.ŠΐXΈ³QΠ’kΐΟΧη?Έ6WξZψχωΈ'hΛ3sαώίρ|‡ΊΕ8ξΉΈ'hΛ3Ρpƒ'=’n1¦€Οl­ΤηΏΖ0vΌσωΓαίώ7'—ΎνψUƒ Όυ€ζπš­cΑ0Χс@°ž±­£φ±yΒ1Š€ `ŽrNΓΥ΅@ Σ(–M8F0ΜQΞiΈΊrΕ² Η(&€9Κ9 Wׁ@N£X6αEΐ0G9§ακZ ΘiΛ&£˜ζ(η4\] 9bΩ„cΐεœ†«k@ §Q,›pŒ"`˜£œΣpu-δ4ŠeŽQLs”sœF±lΒ1Š€ `ŽrNΓΥ΅@ Σ(–M8F0ΜQΞιλpw-€ΘiΛώOϊ1J€ `ŽrNξ…t9bΩΏJ?F 0ΜQΞ避Γ塐L §Q0ϋΎιδk€e_όϋ9΄Ύζ d +‡ίϋΜK‰χh#+Κ½€ΥΧt"ά:αόΔ{΄Ž€ύδ^ΐκk:‘@np~β=ZΗΐ~r/`υ5H ·N8?ρ­c`?Ή°ϊšN$['œŸxΦ1°Ÿά X}M'Θ­ΞOΌGλΨOξ¬Ύ¦ δΦ η'ή£uŒμ'χV_Σ‰rλ„σοΡ:Fφ“{«―ιDΉuΒω‰χh#ϋΙ½€ΥΧt"ά:αόΔ{΄Ž€ύδ^ΐκk:‘@np~β=ZΗΐ~r/`υ5H ·N8?ρ­c`?9†wM_Εκk:‘@Nhv{6ρ­ΓPGηΫϊΐpX_ςBFŸ`τ°Ύϋ“mβ €Œγ»πςΪH&Q(­ M–JΚ8~%’½VR d +Fλϋκ2b()γψňϊZI$p>£P0ϊ“ΔS΄Aΰί €Œγ“υ΅’HΰXF‘`τOΡz—AIΗ·DΤΧJ"M…‚ΡΛOΡze €œγFτΧJετ‰fΏ˜tŠ–#όu”s~…~x„Β”•λΛsϊ„³ίHΉE»8C°’¬ kυ?ΐ&lάx,«N<|,α­ΒώˆΚ»±D–ΏpαΎΌ6#Gβ―Ρ&JΰιAΉWφ Gh>–€£κχ‡―ͺ‘ΏΖž£=ŒΐΡΑ¨;ϊW‹ίΓΞΠtάνHzw•»υ;@œQhkςžnζώυώ3€2bΈσ²ΟίόoŽθ/#€†―\ψ΅«oψ—Ήwχπk4‰θ\ςcβή^yμΰ°«4FΰΖ‘Ÿys]έgωΆΒ.T8nυυwόͺΥꃛwθ‚ώα~k'η.|tόε͞ώΧ>ίKΞχΟ h΄{?¨½`a’χBkΑΒ|ΐ‚A§€ύ1bΐZώ)ΙΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υδί²γGΘΏc9Υ^]πΣω~Ρϋ‰,θ½ΠšνήΈ°`Π) EŒ,ό<ΩU½~΄ΫζόΆή ­ι€ƒN-ϊcdm·œCn>έ½ρxΐΖΞξ…ν ZτΙΘ‘.;Ο†άΌΉk᭐…α?.έ²‘ΏSH‹ώ˜Y:sΗ瑁“~ψβΟYhάωΘΪNΨΗιt +*ήC+oωάΩ;vοΠΎΫ>§[h\Ύυ5v)L«eΠ)°zŒ-ΪςΞΉΉO_}"όΤ'Η>›ϋορΡEΑKvœΈrεΔφ ίόΏ 5θ\^ƒ" " " " " " " " " " " " " " " " " " " " }Iΰ‡Π7 +endstream +endobj +753 0 obj +<< /BitsPerComponent 8 /ColorSpace /DeviceGray /Filter /FlateDecode /Height 512 /Interpolate true /Subtype /Image /Type /XObject /Width 512 /Length 8514 >> +stream +xν]y`ΥΥΩ a ,ϋ&•E‘΅pܐOEKΫh]šΪVcΕOS—6­kj£­K΄.ω΄¨(AEDA‘”UDΘ";a_Hζ „χς–™χζέsξ½ηΜχΟ›Ή3sΟοχ;gξΜά5ΰ+ΐ +°¬+ΐ +°¬+ΐ +°¬+ΐ +°¬+ΐ +°¬+ΐ +°$ΘΚΙιΠ₯KΏώ}Ίti›“Σ€$F…©@³Σ.ϊYήƒ_θ?ίο΅b»V}5νΥ’n;’g#L³œ—fšύΫ’wξŒuΉcΚΦy“υ¨ΣšhFΞζa +4θ5¦`jy£›ΨXV4 _­C—<8eeuB»9‘jΙχžΗE/ +ΩLλ5xΰ¦·‰#ΛJr{₯αα‹Τ)ΠΰάΛφΨ:#qλΏο˜ͺŽ [JN.Ή₯»0ά7ν₯Ήν“ƒΕg+P ρπ’Υqύ†y°Όx Ώ(pͺ[­o™qΣΏ.ς:0elΆ[x|žLZŽ›ͺΪωuρqhκΈf2‰qή‰8Q—σλBΰΘμΌV‰AςrΘ;η ίEyοxJεδ‹δΠγ\γ*Π«p›£SΤψ‘°[\€|]fΉσΥϊ8΅ωΉΡ9r†N +œρ―ƒ ό‘ώπΞI?v‚Λι˜ +€Ž*Sο]7kΚFq1¦§νςjš·Ζ/4³(— νΌ†•ΦΉ0™V|A°₯° YΞ'JήoΡα$mœΤ. +7οb(Π³Δο –ΚβΆ„90zγ}Ž€0·!mv.6εή>,*‹ω=ΙωFέϋΑΨX–ΎΞ§qΑ €†ύοΘηaΠΠM·Ρ0―‡Γ]1Κίηן» \N·gœζsBθw+5ΠγQ«KZC$πρ΅™WFiiζξ[ΉΟ°@ŸΉάLwΫ ώ’—_rB‘ώ=6žLͺ*Μτ·;“e?jƒ T/[5ρ*CΗQFυˆΠe§“αςώ ωϋϋ„Μ"μΫtœΙα,'ΒO]LΛ]ΠΜι‡ΏΏ₯άS%ApjYξλo/;²ΟφΰWŸmπs³°MτYe+–ηw΄αοσ€λφ{ΡΣœΆqm`dΈ§:(εΡδ#T*Gϋ+&ΨΛΰίΤΓΧϊ)ςόλh'ζG|4UΔΝΊ Γ[Ύ™]6½΄τ΅βββ7K'—•ΝY±]7¦Cψ₯Έ^Χ«_έ²ž»Ψv½HiΡύŒΪ…B½HώRφeΐώ³όWjψπΫ8ύ‰ρέΩΤόΜ[ώφY…½“d¦ξ臩xVά~ aΫ^ϊη™Šϋ€ξπΑdAg«¬τ]ϋΟρ=!-Σϋίώ†ΚUF6{ΎOΨ eM>ϋ?ψMΫ>ζ’ԁf){b­σψΪ’·Θ|€Φη½qβpΜW²―xYΡΔσ‹=½ͺ`φ’zΙΫZ_<*=ζ6†&€ -R»ŸxΈGPΖΗςœΜΉβ™Α'~Ό0Ιωͺ‚žΚΟΕƒ`φ1ι]=kfη6’*Q³άΩΑ@“φŸ/•ΖΜο•&Y]Ζ?<ΤE»ΣŸ‘\ATs΅LŒ‘[ν· σ/ž>MrεNMypH<λ¦,σΩY=EiεiκεŸΙ,ΛΆypސvߞMR/X·$g‹=7QX†Όχ¦ͺύΨb·W‰Όx=֜Ω)e•—U%κού +z—Jk4ώuΠ†7ώΗJrMI'­υ.‰X•§ή{ο“#ΣWƒ΅z¨ρα’ͺ47yhύ°&ίHqΊq²jϊ’‰ͺτά­RΨ}‘‘ +Κη¦L–!ΠΎ{T}ο'Άy‘”Υ OdΧ”γΏ“αώiΡ c™ͺš+1@ι'‘Γώ\ —¦ηI˜³ΎΒ2%Ό •[K‘σ‡ψ₯άt +ο7Π»εit]~Ε$εϊρψ]›ς€Ušιpτ*’·ιέόuŠvš…κ‡ŒοzΒ:dMφ{ς‡ίLιωΨ˜/₯ςN3™νΧ‘έ?χδd¬+?wΠwΘ|SNΥΰuΘrS―iŠΌdmυ0T(Ξ¬ξ(š=Œ‘LΉ·2h­ΫQKŠ]λΚά»¨· +3fJ:k*λ'])Mς€Ρ¨B3cvΫ/1yW;;P6ζ"&Ν•ΪπΜXˆ?˜AMiYσS9N f+)˜΅†v€ψ"TqŽc”εr5boΧέ”ΑF4”φΌBp5ΚHNDr‰³‚8nψSΫ~‹ηώy'&Φ›ά=k>zψΥy’^ƒθηΆσ΅sx4 Ž+Ρξ€-ζ)πωOM­i½MƒΏFυύ3Π^€ί3· $η+¬¨€έμhΓ=ή€^γΛ½>₯š “λ35ak VΰΏ‘f]GŒMη` ‘t|£#—`½ϊ|ΨΠ₯Eͺ§5› R©R΄Αυ;$›ϋμͺr"Φ +G7s€Ÿ³ΗsL}σwQΫr167 Ο•τφ“8Œη›χΥkη–ΞλqδΈί.sŠip:ό―2±ΦΟΞ½q&ŒΩiΚRqΟ‘ΔϋŽΫiidΪ987DδΫ£°­2Ίγ[”§Ζ£ά»šGeKsχY ²5?§IN՟04±ώ(h]ιeνP–u3‚ͺ{]SPzν6‘x#ΤKMlρŽ™(½!Žg‚Ζ±aΜοΎΒ{³ wΐθ²—ώ'ΡS·ήž4bΕHŒΩΒA…$!³“0nk$ӟεύwFυΡGH<β!^”₯LAΠζφxτΛD˜ ιK“[όγΉ g-<VnD¨θΨgdoηx~;‘Gόe‘ά(n,„GψxŠΌ0ύ.ΟL$(R²9Ξο)ΐˆdšρ5\ DΈΨΑ€Ώαlmm—―gzΐW|…ΰΟ·QtΩ‘ » \T΅E"#“GΑδ^”‹Rž©Ÿƒ5zˆŸp,YΰΩ>Ά‰,ԁώvpσΨzͺ]’―‡Ά·}탱¬…φkOe6Σk­~v.iξό–]ΆϊΣΪAίώ’Ÿ„gB‡ΖUlό_θνoLW`ΌκN )—§¬Z/wιN)€…2m½¨Τ!³’/:HΚ+ μ'@₯D‡KΙ‹@RF p ±ΪΘό¨Υί…ΜJ½¨ tΎ£Έ₯Όθτ‰%5κςθ ‘“£ϋΜ‡»“c$ΠΩ0΅,rυδM€ύώ^©6ΑΌ?ΉΐΐψρΧν ιe$ΣoΐψΌLŒŽ|8ΐΚςΘG˜Œ…†°*#ήλλV΅Α°¦‚Φΐ%06r«š‡Ξ›“l))`UΪΥζMο W8ΜΟΑΰ吾Dζ}<$ζδ”²€Ω&Jmεΐ~Ώ˜γ5D€7€όoυA„Ν +6κk₯P†Jαώϊ†Ap—{KΟ„uk•t|4 ΐ:LΜ Cͺ9¨ηΟNκcZe }"¨ΞτPcYΈ’Ξχ*PAφxφΌrΑ« έ."#lΒ7?~όΥΉξ<Ÿ"γΥsΙΠPΦeξεx vƒΈίΊΩ!W?$Γή;‘θˆ˜2«© ±Ϋƒ^œoI OΠ’Χ― τΞ%Σ ·N1@ ΡjΗP­θυ/VΦή^‡m¦.nk―E²©M!Λ„V7K֜”σGCόΌHζd:"²σ― +TΗ²ͺŠ ŸCΔ{@ΚΈvf(μj7kο<2Yώ‡τIίπ?αΙliϋ>@½έ&μ `]©HeΊfr!ςυ&ΐ RϋSιΧ¦ΏzΏu„ψ¦ϊ|΄mAΊώ}¦ 5ΓίΰI4ζπO €_7„§ϊ•ι€B^(ŽcW-ιΕoR 6Φ^όTg2‹₯)/₯1dB8ύ3ψmyͺ”3d(θΉΪyήπmΪΡSπ@Α;΄€τaλ§=ό―"˜Š£?ΰΥ…>’‹ͺœjq g'g +μ4@ζηψpŒΜqΉΈwι&|²8vλQέΰ‰Ψ'@Γ–š9Œ`ΏJ3v*ζΠp€fμν5c§bΎΐ£5“|ΌTh†NΖ| πo5³xYQ+pZΖ'‹Λψ ­L šί­8-㏉ϋ?_+“ηŁ_£8-γwˆΛψ­Lή>D+pZΖC¨υN¨ώνNΛZΡζΟΥ»nbρϋ_wΟ­2~ͺΈŒ³’²R»»AxΏ'ώˆt` +•₯‘9)ήΫ#μώ sU¦°ŒΦκ°lΤoŠ]Y₯,a‹ϋ…`ƒVV5ΒΈηiΕMΝΈψjΫtRΙvΏυ‰Nάδl―r·N.€†+sΧθΤ.ΒΆψwΤΑˆ|οδ‡­υoΕPi››/,dNb­…a[“uβ&g°΄ΞAtνΕύ9θτ©Έ5βξ*ϋ%°ι™ώH\ȍlΊ‹Γώ§FΨτLΏ'.d+l“ΏΌ¬6=Σ†ή€ξΏ―Σs‚FD€vTΟΐϋ?Ούn€χνh€οξώξροκπlToκ>R•΄½%ΒοZλ…a[3IϋC58CλβSΝW-1i{λ…o$­νρφ5€ύ‘œψJΐzΫw Η­ΦvKΥξMd/KXFΝύΔΛ­ν‰ό‘ϊ8 e‰j¬φŠΞzΛvϊˆΛ8S+|ΐάΕ§hNΛψyβώΧ[φ–8π3iΉ@+š«ΔeΤ»€b±8π±Z§eΣ!❫w˜eHϋ•΅³AXNώΫl)ώ4‘!ΨjH ’ΑAŠ› ιξϋΤμ_zη―RΠυθ>ohό­“ Δ•~^΄K δΦΉNWΤFΩν!aύ:*7?ν>Q¦ ©Δg/­εOε)¦AΜΤ΅/€ΨήδD λtϋL}z!H·GΙ(tˆΗD2 +Vι<\@hΉ5ά +₯Ά‹Μ4Q’3κ }JZ«S£σ€²Ξr,v<ί˜˜/ωψέuwΗΰh™Κ°Žέ΅1™z+!ZDZ[ΡU4ψ˜2;;Πe‡€¬/¨Λ1…(/›5έΦ ²O7χg.† DΊ‘2θqi(υkGpyD“ΰξ§4μ#‚Ϋ±ΐJ–Aiy·+Π αώuˆPJΖ† Εe"„ ₯υfqQ‚W.%1埳*‚@T뷜Y»:’> Iπ›\™wR `?ΐc<½Ω ―ψ³¬-„ύΪΈm»6°ΟΫθΤ«ώΦς k½7)TwΘ4ΙΑ»]$ώ ωΐk-M–Aδ^KcΒΧψePΛ½A΄§β1νhΪ;1‚ΧnojοΗƒpAw˜@Υ5Ζ§AZ/ΎΗ΅='žt ˆς_ν₯I!ξ„(ΊΦŒΫ?( +!†lμ€3ˆQm_|"DθΪ»QAΙΛ¬ J`mh'’œξΉ²±΅±RΤc°‰­B-'ܝ„:=Ά…6~Ÿ„M½§6Ω"Ίx‘ώε-αBvΨβΪXMΎκ―^+ΰΤV!ζρΑSΟΫf«γΨΖU6™SMJ[γΊϊΣΫΫbTϋ•γKβ ‘‘xqΘƒΐχΜ^¦Σ> ΰεΥ†5‰€Φ5 +’>ϊΙ%@,χ[/Eή_δχϊT‡;²ύ™mξh‡Uψ[Ϊ;˜ ›ό"ΔηΧΞ%:ά-‘τ]±^ύ,끄ƨpRE„!; Ν¬θ ›ί9\±υ.“zs8Ψφ·& +›3B±+©έέ.π€'8 `Σ@ir9N₯ο1ή£EΝ%šήπΡ`‘8x΅K£TNΛC{΅¬=†6„`t F@MΟΊΑ‘φL7ΖνnL<'kϋ`Οg€h©ΙΤ hŒ―Œν ε2R½²νΥ&—zΚHΰ°½*ƒΗC½£uυ†Αδ\mθ2ΌΟώ£…ϊZmΏz’?όSCnu˜ΟG{Έ ν₯‚.Α-π,«ΐAWS’txτε‡ θΎgaŒρ +g<Ϝ‡ˆŒΣχ5L”9';˜<΄zΓΠζΎξΊ9Αν#u ibYϋσΰ¨πsHΟG¬ο<Ξv<>Lε96ψO˜λ6?謜F"ƒ q ΛζνDF8ήg8@˜.–u €VΏ°Ζ…菹ΪΑώ-ŒπoBΏŒpΞbJF£¦½‰Υe[·„šqΒ‹±άΰ)ΥΟRΉ=Ί’Vχ‡)³Ύ«ώM„2SΒ+@­L{ ( ‰Θ)<ζ2άΝυ):βu…Šxέ8έ½β3rΑ“^G0ŠΪρJ\Š]1ΤiΐD…Μγ)£WHϊχΚ#ΰO’τ±¬ΩΪ¦ΓOυ΅4V‘Œ=R€~b„Ύ1{ΈΜ{ά)ο”Qπ©άέHα‘h΅Ξ YΑsfsς’¬ττkQ;yΔγν‘Gΐι3C:κ΄θ&•ί9w­uD‚ΐ#%ΐ…ΰ•OβJ»³HΥ©“‹Π†υΕe:θ‘Έ=DHΞFΥkΓδ/Πpτ‡²ΎeœUρH<εΜιΘ†BΉΓ½ +₯~ξ;ͺΰH{Χ‘ ځšYγemχ;¬i-’g덗ΐ¬―’gžόGfη΅Aοο”7[}ΉΖέ%@›υa”$nVΟΚC¬:Oι{B‰`έeν8qTlέΦ<7£…°ΓψΧυ<σ£ιyγ0mNŒh}lφ«η=zδQΠνϊIίΪd«)Ι%ΐYˆ£]9bέ›wI~™–Γ'LEšΈΡJ7'y£ΈΰΘη|?νΡΊϋ0h}Φ-Ο|Œέ‡Ž7J€+δVΖ‘zχ’©“ςΗ^0 sΜ0’”'δŸέχόGΛευζˆΜν!oΐυˆ“#ΈU.κΌ#[ΛΛΜͺ¬¬lήόEεεκήJ£p$»λGΐZ?₯“ΥœΤωή(ς8D£Κ%ΐ/υ?D ϋ:o”Χΰ–νUφ½—~ΡVεK!;ή€sχ +‘η‹,Λ0w’?†7 ΅ +VsBΘΠλ{s'†Τp’‚ćǐ‰;E –Έ&δγH)0Ak’½›Έ&H0Ό<ησw ΏΰΤ5‚ό}™GJ€Σ|οIA<)ωά( €ΐΉ4{ά‰9EεU^ €φsTͺζ![^ €†ErŠJ*^ €ΐ8?΄oΓγ™θ‘=€ρ7šάFh:fۈ™sMz| +ΆG9le±IL>•nΫpU©θj€6ΎvHκVD³›πΦBΐdήΆK>αχkl _‰ Η'χΝ‹ Z‘xϋ©C‹);‹‡ΖCλκ€+™B'e+£ρ&P]6Q`ƒ YρΊθoX^Π1YΨNηs8)㘞:μo5>υΏ+ΰMΰ€€h©ύ Vh e…π‡~_€(A\ξφ*X¦8–œβ[R§q$%WΨΙέo{GU+ρζWo€,*:v“ V·)iύσΛd=P–ί?Ε- ‘σ8DT ]Στ§ζΘZ_nο'Ÿ—tΫN™Ϋ ·J9—ή+·drέ@yIήΠNqΣ90τl}Ω#ο¬ΒI\΅΄τΎ1λb@tΚƒΐI™dΣτSPΊL4 +—O-Χ?‰ž<ΙΒs:ŸΐI‘τ¬žήόΘ+Ÿ―;βς±rΥ'/ά8μdEε½ 'QΐIιΌξŽŠ^yξςςςνΗ§©¨ΨR^Ύτ‹©/=q߭׌θίFκϋ½;ξt‚ž•‘“ΝBΞυrt5&Wc\%(€]Ι•ΐWΙΚ GWcrε0ΖUr€rΘΡ՘\9Œq• rt5&Wc\%(€]Ι•ΐWΙΚ GWcrε0ΖUr€rΘΡ՘\9Œq• §osΩyΙύižY4JŽβΔrνΓ@Μ#Šαp(œš9jQŒ‡@±ΰΤΜqPσˆb<ЧfŽ€šGγαP,85sΤ<’€bΑ©™γ ζΕx8 NΝ5(ΖΓ Xpjζ8¨yD1Ε‚S3Η@Ν#Šρp(œš9jQŒ‡@±ΰΤΜqPσˆb<ЧfŽ€šGγαP,85sΤ<’€bΑ©™γ ζΕx8 NΝ5(ΖΓ Xpjζ8¨yD1Ε‚S3'#ΪS#ΙxœKsœΝρj +τέξ~ξ—gNO₯F’ρ8+ ‘ΈΧΩ!§~ pψ r$³ψ%ΐ‰ζ¬―ΰΐ―|­§qδΡ[§―£—·ωZNσΘc—«ω ΐ¬ ΐ.†™EŸΡ"ΐ?XQΓΐ}Tπΐ0pK€ώ¦ΡgΌ¨p'λiœ˜€7ŒcΟ€1σYNΐ{μ6=CF,Έ +ΨΘpB+ZIŸAc½ve)ΝT©θ`&{Fτΐέ@ %ŒG@M†±τ8Β#`«h°πψΨ`φ =~LbVZ\c4{ύ +hΓhωψE†“gψP Οš―€x PΣΙ|φΜ@Όx—Εσ„’_C<ΑžIΔŸ²r^Q@€8Η+왇Θ;ΐD–ΝC +$]¬Κφ{¦’l PυΦΜ[ +$χx‹·Θ3›δjbΑΌ§ΐι[]Nώf§x=3 +tYι.ŠΨύތ–}ν"ͺnυ&yf€T'Š€ο³PV`δͺΈP]άΜΓδ™Z yί^η˜Ν3>x?F²σ6ΪGΐμQή'Ο kΘύφΑθXQπcΦΖ? +4:―ΰƒ•UuA°cξ 7ςHOψ>Δ4£U—~=:6 νσ+ΐ +°¬+ΐ +°¬+ΐ +°¬+ΐ +°¬+ΐ +°¬+ΐ +ψMΛ\~- +endstream +endobj +754 0 obj +<< /BitsPerComponent 8 /ColorSpace /DeviceGray /Filter /FlateDecode /Height 512 /Interpolate true /Subtype /Image /Type /XObject /Width 512 /Length 6368 >> +stream +xν}ΌUΉΗη8ΌΗ‹Όͺα ‘χjέ+Ša™&υ"Ԍ#/½($J „釀4©Σ,%°@ΚQ!0DPξΗ—κz±ςH%H8δεΰ9sχήημsf­Y{ο΅η¬53λy~ϋsf­½fΝσϋ}Ÿ΅φμ™Ω3ž—šW—s–Ώ°³ζ}Ÿςλψ[+GU€ΖςτwζΠHκΝ'₯ΗχtDrΙuΘ/Ύ5 Ά§$Š‘Ο‘'. | Π”{'>(™Γ‘ΈηΜ&ύΜ>ΏŸοF$@.ο[ί]²†G>2 ΠvΨ*•˜ΌΞΟ¨ŒαRΗ~h³Ž j΅Nζ3@εΓj[ψΤςžfς]H)η`pm!WΥσM€Άa„Ή°TΆ 0·°'¬ήašύŽ((Χ›rAv$ˆvUΘm¨βΉΈ0μΗ‘[»‘DŸU˜ΏΟ1zρ_u"]ϊžW„?ΗΈYΖ_?Ώ’2ώ’ό}~ϋΫeώ“IΣ/>ώ}v3ΐ Bβψ‹v 0KβΏ­5sώΜf€‹©γ/5ώ}VϋU—5δρ—ζΟi /€Ζ>ίFˆόtdΗΏφ_’Ή›A7ˆβŸ€_žœΟ9ζˆό—0δοS#š+1ωX JŸΓ‘?ηψ‰Θ +Kώߏ€E"κΗ~³ι-9­cϋώ9ώlόψsMπoδΟ4ΐ?ϟM΄»πΖ{6ώί5ΉΧQμeχr/ίzMyJB.$Χύ& pΑͺwφα͟ψq€σKΖ—9Κ ΠkΉΖύΈσ§›Γv τ +μωύP‘ω/π'™Wοr5ψ{?:n9*ƒζ› ΅“AνŸ.„;TώΩ#΄U, a.XώΉ#A€f€Ωi‡ίJ3ΐ‡ό„$ώtφ:½..QFώdf€ΫJί<"3ΐ‡€ίwεpΧm™>ό£}Ίe_?&β‰oχ‰|³₯#wτiVŠλ?šh6₯q‰@œ>Ω»…‚aώφBŸώυ·Š<"όO€Šςτ?)8ψ3Λΰ_ŒΏλ πI]~πχ‹ςwό[ΐχ$ώΟ΄εοd[šΛNοn“ψiΦΥΈ„ωΏψψwϊl`§γ"‡Cψ1—˜3ŽΉ;Θχvϋ,ψgPύώ3lL ΖΩ³W‹Γ@›€¨ΖEΜ₯ζŒQ.%@Υ‡Gέ8Ρ+²―?‰όW…ρcώ/=g\sε#`ΰτΗ‰Μƒ₯ΰŸu μωί‘θx]‰‡χN¨όΣ?tυNp¬«–Η‚dώ)O€Šρ{UΔΕΊ1ΰͺwOΩ$’V—ΐ?7’|ώgWLοNΰ½wƒ‹ψ§vΈY㇝Ωό[Ζ?3@…t Gυάώ9φΩ?QηΜͺi<tgaΰ;Ήh&€όό‰y°ώ-柺Έ\σ³?›ΰίrώ)Ϋψ ήžΓ,ώψ§j¨άœίK-ƒŽDŸήΛU–ρ'EΗ&‰RJ”ΐ?GΉRϊΘμ\ϋ\ΣΤ| 葜ύk7,ΌeΒ•Ω—tΏ/πo -"X.Τμάπ/Œm~tσrρ}πo ύwΡ–QeσOIt= +Ι”vŽ ώΌό•h₯»αόLΩ¨xe*φn +αίΨ]ό;ς隷½‘‹βσ ‹όOCΌ,σy•1ψ‹~4–δ½fΥuΚƒ•Ι'€|u―Ώ^~t/ψ‰5-”ΖΝΞΆMo•±ψ·€οJ2^oήρk”ώjžoKΞE{κY3ΐVIEψΡ­ΰ―ζΏDrn€Ί]‰Ϊd S­¨bK8Zπ{’­":ηϋοV7,Q›hœ/‰P|5ΐŠΏIήω‡«Υ-KΤ&™D οuΗ +ώaOr5“Eο2₯ϊ%½ ΄-Z`H·φxD'ψ+LΙV΅ΫJΰάώZ«N.~*J˜§ˆό¦δͺΎ.šΧXϊγόqCŸSήkJ’«·Ο(΄asυ‰Ϋ•oμ’έψ²»Υ‹’{¦K1<=\Ί₯Ώbχό αΟ\Μ―Ά³ΐώ °VŒv˜B«ΖψŸ/φς%E/4«ζ‰Β—vτ΄μ›ώEαηY:=έ·zJTnΌ΄ά²V3όϋ +σΰ?*-’ξ{…˜MzΛϋ€fψ{KƒͺΏ•">ΦCιΏ;(έόςt» + ροσf³ς-ŠΔΨ‘hοg€7›`lι»β ρχώ퍼β-ΆwYμR~ούΆη₯ΫψΏΆό€ΚYΓο„ΜΚίs#―џυΊλol€oμΣώ™Γ‘Ώϊ\F»~ΝΓ¬β‡­e€;ό›ύΰ·tκ[ ώndΣ°-v2όέΰοy― ?&£ε9ώπχΌnΧ>Ό―εΔΕΐίώ™H+ύ‹³~ΎrύsΟG~½ +ώN7μ0π7m©SύΏSΈŒ ώΖ-uͺCπw +—ρ`ΑίΈ₯NuώNα2,ψ·Τ©Αί)\Ζƒγ–:Υ!ψ;…Λx°ΰoάR§:t•Ÿ™Ίύ±I2;…Α:Κ†Ζϋ!ξΎ,…žΊ’›ό›Q7ή%·Σ«“ό― +œ΄QmώTΙκι=Ÿ¨ΆͺdυtΏžOT[?U²zΊΐ_Ο'ͺ­ΐŸ*Y=]ΰ―ηΥVΰO•¬ž.πΧσ‰j+π§JVOψλωD΅ψS%«§ όυ|’Ϊ +ό©’ΥΣώz>QmώTΙκι=Ÿ¨ΆͺdυtΏžOT[?U²zΊΐ_Ο'ͺ­ΐŸ*Y=]ΰ―ηΥVΰO•¬ž.πΧσ‰j+Ξό۟z‘Χ€ŽQσ“/αλj›ξžιόBέζκΚH)ΐ•‡_9Ο\°‘{”`ΚΏ’άνŸ}Ε(LωO“‰b„ΗΏ0½s›wH—DνUώ'Ορ?TrŽHρ«ΰ―ηž•χ‰ηψ'ϊόΏϋςTυσδ-‘ _’q«>χ|KžόJΖ)~&OU?Oώή³Dˆ 2v΄ΡηžoΙ”γ‚s4 +Wδ‘–ρŸ)o"₯‡5€o„OΖΟΊμο4}^ΕΎ―”1κ››ržΧ~μ/ŸyžΘkλŠ‰Qώ™IΎό›Ηη%πηLγŸ7}πόdξZ»~¬6ζSlmΉΨdŒ’Ι"±ΙdETi:€ΟM£ˆ6’`5eΏ¦QD›?Q°š²ΐ_Σ(’ΝΐŸ(XMYΰ―iΡfΰO¬¦,πΧ4Šh3π' +VSψkE΄ψ«) ό5"Ϊ ό‰‚Υ”ώšFmώDΑjΚM£ˆ6’`5eΏ¦QD›?Q°š²ΐ_Σ(’ΝΐŸ(XMYΰ―iΡfΰO¬¦,πΧ4Šh3π' +VSψkE΄ψ«) ό5"Ϊ ό‰‚Υ”ώšFmώDΑjΚM£ˆ6’`5eΏ¦QD›1ζΪ”χyέ9ν¬ˆωΙ–iĞΆω?"eWώŸ¨o"ζ~ιπθ( ΐ”}ξ—!˜ς_)›G‘Ό­’όΰΙΏ7½»gψ\πΧs`,…αΦp‹žϊ`+žγζ°wj–Ικ-σδΪa ?clΕ“θ°wj¦Ικ-σδίω(ά²†ϊ3τ˜[ρδούPφŽByU¬ζ2Sώ]BΈ¨aO?MζΑfLω{ύ_ΝsΏτf€\ω{ζ‘:phaΰ°Φ^fΛίσͺ.;‘ΘkόΕν΅‰‹ σ`Z¦ΰeƒ?ψφ}ρό7fι€ρΟ Έ$ό%C˜ΑŸpI.ψK†0+‚?3ΰ’\π— aVfΐ%Ήΰ/Β¬ώΜ€KrΑ_2„Yό™—δ‚Ώd³"ψ3.ΙΙfEπg\’ ώ’!̊ΰΟ Έ$ό%C˜ΑŸpI.ψK†0+‚?3ΰ’\π— aVfΐ%Ήΰ/Β¬ώΜ€KrΑ_2„Yό™—δ‚Ώd³"ψ3.ΙΙfEπg\’ ώ’!̊ΰΟ Έ$ό%C˜ΑŸpI.ψK†0+‚?3ΰ’\π— aVfΐ%Ήΰ/Β¬ώΜ€KrΑ_2„Yό™—δ‚Ώd³"ψ3.ΙΙfEπg\’ ώ’!̊ΰΟ Έ$ό%C˜γεPΰQ3™ΕΑ +³—‹MΖ(š,›LV4A•¦ρς@—"HπW˜b―*^ώƒώΟͺdΏΚkuρςχ6`€Jψ«\±V3‡ξ.SŠ₯-Ά*cζ'?όΊRψ+m±U7―χ’CΩ x₯ΊB- όΥΎXͺΏη΅ϋdυȁε€AklΌ‘β2ΐΏΈ?†ίΓ†:Φψ;ΜpΈΰoΨPΗΊΗ€ό κXwΰο0Γα‚ΏaCλόf8\π7l¨c݁Ώcΐ ‡ ώ† u¬;πw ˜αpΑί°‘ŽuώŽ3.ψ6Τ±ξΐί1`†ΓŸG<Ώϋ΅΅ΥWy0LΈxwqσo}Χϋ €ξΊPψ«}±T3VΏΛ_ώλ”ώJ[lUΖΜΏ›πϋώΏNV‰•+Φκβεί3wνw>ξQ©•+Φκβε?1>χ@…,πW˜b―*^ώχ όύ³Ίΐ_aнͺxω?*ςͺΠώ +SμUΕΛ­Θ˜Bψ+L±WώφΌu‘gπw’½Αߞ·.τ ώ.P²#ψΫσΦ…žΑίJφb{ήΊΠ3ψ»@Ι^ŒΰoΟ[z(Ω‹όνyλBΟΰο%{1‚Ώ=o]θό] d/Fπ·η­ =ƒΏ ”μΕώφΌu‘gπw’½Αߞ·.τ ώ.P²#ψΫσΦ…žΑίJφb{ήΊΠ3ψ»@Ι^ŒΰoΟ[z(Ω‹όνyλBΟΰο%{1‚Ώ=o]θό] d/Fπ·η­ =ƒΏ ”μΕώφΌu‘gπw’½Αߞ·.τ ώ.P²#ψΫσΦ…žΑίJφb{ήΊΠ3ψ»@Ι^ŒΰoΟ[z(Ω‹όνyλBΟΰο%{1ΊΘGβ]€§Ϊs‡~Ο#D/΅«ΨΜύΏocώžέ˜iχ~΅θε2»jΝπΏEŒωA»1Σξ=ψ@¦Œ­‹νͺ5ΓZ‘ŸμΖL»χΥ’—·ΪUk†§Δ˜ύώvƒ¦ά{Η#’—cνŠ5ΓΏ³ΐΘΤFKVžΉ'­Νπχv‹Qοh«΅q4 +9Pρœθ䱎‘&F+ ρ_&FνΣhŒ:»J2r“eν†ψ_#…]sͺεΈ‰vίc§dδ\ΛB ροU+Ε½½‹εΐIv_΅Q²Ρ?Ϋ²NCό½5rΰO~ΐrδ»oσ μ’υo¦ψ_!Gξ_ΛLў›B&Zߏ2ΕΏΥ«‘Ψχ_―zšp™žπi^9ώ͐…5mλ7Εί“χ³Z^›|’νψ©τί}ΒCτ};Φεγί毊ψύϊmw_•+ρ*κΐ„)ίΓϋ*χώΩΝώήP•ԡā/[Ηοž'='Ί%Β±nցMNροϊ°™t ζCφρ›ήyΗLΚηήWύe1ΰ7ΚίϋRwhυ+όfω{ΧΤΟΌ«±ΰ7Μί»3€™Ό½»Iώ^5φ $@υγΎωμ2ψύ―‘ΛA―ΠΟΌ‹w†ηρXoœΏΧmsz-–ϋ“­coΪ€yώžχιWZlγώ1>†Γ>Vω{m―ΫΑ`‹€οοu6Ζ&ΉͺͺΧ)Οh΄Θς+Χoω―M#3žKό3ΑŸ|έΓϋΘ3(πέ5ί<=ζΑ­ΨγŸΩJeKΏqϋΒϋWΰ•w@ΊΊ{Wώϟ6βŒΦA,±-[ε› +g64Fœ0–'8ψNJόc΅;uΤ!‰5 πΥξΤmΜEώKΕ}– ©3Υ‘€ΖŠ^.MvρV uVŽάfC:φŸ4ω{}Γ»€‘₯§mΕzεZ s)Κ€‹½R—Ώ7Όήͺ‰v~°OΨχ ­FT?2ΌΕDj΄ω{Σ­’lη·…½_g5’Ψ~ί&Φθσχ~`Υ‘D;Y4%Sκ"©7ή‘ν%UQŠ;͚’ގ…vΖ>f3Ίο‡6—~ϋ4Η6ƒκ>@}θ„ΦΗνρ―ηΞ^Ν؊-•1ώ3݌άoΟ–${ή²θkαμϋ\hc V”Ηί;e³5_’μXρ;ΜΧ,Ε³©_‚΄Γ›.“ΏWρUŠ. +3έ +½σџΣό˜(ScnκφqχKΏ +γχΪo7―kœŠ-%Z΅RTωe`:OS=«@μΘ₯fε9 ΣwΦπΤTœρ ί/ŠΌY|·`ιμ9›ŽŠk:[:zG‡Uφ^aξλΞΡ?Μ>« ™IΎq—Θν§ϊ±tT=wρίwϊ΅ζήI'–> +stream +xνw`UφΗ'…$„^€‰ €ΙO"θO EE#"ˆ‚ntQAYΨ»*Š(.JQVEQ@~X£4QW E,€ ½†BB’¨WξΜ›™S¦ώα½;sΟωžοηΞΌ—y3χjšώΕΧhήρΞΏ½0uvΦμ [s +‹ssNύΫ³aωΒ¦Ώχ’Ώυ"Iλ½?φZK·x<-ή,ΠγΒΨώεMqΑ³ή_ω±s'ώπαυΓςE€y@Δwϋ&œ³οΦέ“Δl@°ΣuύήYήΡΩ7χ—GΈΖdϋ―£ύwΎeKωΰ­η8ΟZ­`m:GύΑΞqΑ{nωΦ§‹Z›uυεGε«lwO[Ρ¬zΤ7~ܝΫέCZOΙ±α©Α%DZωE_θyξφ-½ImhπJΈβ³?ͺ>nPHteίθSΙ€96DnΑ •&–xλWί]ŒYΐcέΆΛΠCUζ?pjXεΧx'ΤWοΌ^“†ε@ γ\³Γ;ΘΓ• “+ΒΠ‘›4ΪΝΧϋΒyGΏ[\Zΐϋ7Ȏ6ΥK-ξ8@Xωιϋ½[©uͺ\΄;R_W:κ±ΖUυνΦπ~uWzŒ΄Žά7€½ς―ά­γ§ηš‹ž—«–Η@†Λι΅4 +ŸeΉώ`wH~Υ’ΏίωηFΑζi±ϊΌΓ2Κφw΄hAwoψ+Δjwφ-θd’–joε›o~aCqœL)gjt:f›ή̐‡DL €»ότΕ?|π.¨`’ώ€ος˜—ο ΗύnÙӍYώ¨hΣόΤ²N~4q£ύ[UΛ&y>@čUYζ―ΆΝτλψ–Έύ…Z]ΝΗ¬W~ά΅a~kέ#gΥˆσά=ήvζŽ†ͺϊƒή6ήλ·εΌ ΓŽ χ0ΪWΌA&Œ½ύ|Ω'z ό(7„ €Ξn˜Ι-]Λ·eΒκψ›6ΨΛ5ЁÊ™ΨΧWΣ{ + +@=κgρ‘ZΑ5‚}Λ—ώ€˜ ώoι;μ-EWa\"_ώτ†ωŠ ά +β‘U|υ‘Άΰ‰ ›I τxπΎ_32q­ΗΡΚΔοί υΟλΡs*ϋ{”υΰJΎ¬cκσ”ΥL&;β뉁ΚΘαkLŽπσ `p¬κeϋαͺώ)Ϋ…oL†ϋ—Γ1‹—ŠTςλΓίΤπώξ«μ쬬¬χg͚>qβΔ‘#G>ϊΧ7\Φτμ²^¦Κ—τ(Ψ»>ϋ³™cυlΧΘ‹σ†Δ­Ρ+LΪ-;p`MΦΟ<ΤΉ‘‡nΊήr‘!¦²&fv9ί ?ΏYŒμ`ׁ£«f οζξYε›ΙοώvιšνwpιΈήMέz*˜lΆ +Ωδΐ‘%cέ8ͺU%-93ηΡ6)ϊ{1ΐΣ|Z"‡·sή’'ΪΈηϙ돬ωH‡ηlκŠΣ@kσšeO\6ΎάΙωΛE“pk’h–Θύ¨―³Σ –=dI―μŒξ@Αgύ|Ύ¨/z=Π²…Ÿχ­θΠ—Ο-‹•›}s’C Ζ Šb$¦vύ«ϋxȎPιCδ@Ρβ^ΜΟ™,%ͺDΒΪt`η°Œ':ςӏMNtέςήhΙ6Ρ•!‘ν;0Ώ-ΣXf_£τ€tΰ‹λ8F@ 9ύSBΕ^v5ύΙAˆˆ;g₯Q™ι›!,|ьΊ€# YύΓ‘χ>ϊOΚλאλ—PΦ]Ow + 'ύ˜EφΰΉΜψΒ€žbΧ-4§ωλΞ†'Β[$“ΟάΞ#^²ΐΨL±νΛp]ΙγΗ‘ΘΗ?<”4³±Χ"¬.QΐpΙFΎKπ6.α’ǁΝ-P?Fγ¨’(lBύYX¦{f‡•θhΌ3@Βa,U‡ΝάkΡ@36Ρ’ρcm°@Ν’ с5qΐ׈š$£_’L'χ~02CM5 +γpͺ$ Ζθ@Q„Π‘Q°€Βu`ΒΟΑ2γ3.Φh“α'€‰¬‚%ͺK’/A$ΑxXΎ`7―`Ι†λ@7ΰ'@*‰Ζμΐjΰd²0λ•tΘά;\‹,GΒ1;πŒ½Μr%²ωΥAΰ)d9ŽΫώ ώ―sΛ•|ΘΜρ Y„γvΰ(hωUάr%ΆWBNΫ°ΥHl^e°ΕHt ό3ΡΥH@n όeέnZψω όβΛ‘ˆΜ@ψ˚οΜ°Aψ?H GBς:αW^©’ΐϋτHH^ όeς'^VΩ όo₯$1Y€π—•XQ‘$ƒπΏ”D‘εtΒΏ §PΙEβ„mE”ΣςœB%‰ώρω$’$(£ώΪF‘’ŠΔe$’$(£ ώ2 +•T$€ψO ‘$AρΖ(TR‘8β/?’0α +β/€q’"ΙβߐD’etΔΏT£RIEሿΆžB’ΔdtΖF₯’ŠΒYώ‚ gLy€“E.™œ‚ gLœR%0ώΪI’Ρ ω€‘E* η(4IL>€όe +(>T$™€όεp*|AόςI•Lωkσ 4IH> όη“*™€ςΏš@“„δsΚΏ¬άΐ‹ ”ΏΆœ@”„dsΜ›TIDΰ˜WQ’Ν0rΗΩ΄J"|ΐό΅Eψ’$"›pώCΩ΄J"|ΰό/Ζ%Ω€σίΓ&V‘;η―MC%Ω@ΰ/Λ€±ΡΒO„ΐ?υ(Ύ,‰Θδm6“VIƒοžψ²$"“ό+Θ%@&Zψi0ψksρuIDPψΛ,‚,(όS(“δΦ.ι:…Cͺδ p`k Dΐ««”IHV¨—tΫΘ‘Urΰ;°€"δΥ|e‘Á· ΤKϊ6(β+9Π\ΒτJžBGΓ°=ˆzIgΉ ”v’’Š%A―6aK“x ¬Aν<„A­€ΐvΰΡP„ ΧΥς°΅IŠ47Δ!”—­πω]Ž"θtŸ|D ―”e§νAϊΏβŠτ ͺǚ6Ο›έ)¨ΪπΞKx%:iͺΗΪˆ G•–Œ¨ΜG‘Ž£šάθξ‹εΧB•VΪbϊ€μŽ:CωuˆΝ@Ε― %Tώo+SΨlΌTψΫtΞJ7LώχYIk_μ?LδψW:ŽΘBΤ{°{αώrώWβ/Ζγ_υˎ$α―&†ΫŠΗͺ°'ρΛρ―ΖƒΖΏ―:ΎΝΦγθ·¦ΛηΏ¦G•αν6NΓ>όεψW£@β_f:ΌέΦVΒίuΦϊ!ρŸb-k¬½ f%“σΏtώ½•±ν7φ@?όεό―¦ΒΏ1ςšΫJ 5.τV ώ₯ΏG–υO|όrό«aπŸ€m»5―šπ·mžΕŽόﰘ2ζξSπΛρ―ΆΞΏα!udϋ­-…Ώ}σ,φσOYi1cΜέΏ ΐ/ΗΏΪw0όΫκρI85 δοε€ςOWF…4n¦y&Yψ+‘ωΧ; Œ +iBrϊ—γ_ΝΖΏΤΧꨀΦά*Β`ŸΥ0ώ㬦‹½dόrό«­ρΏ™`e—Β_MЦΒ\‚5]α—γ_=~ό“ώ« jν&όAώYν ΰ‚Υ\&φί” όMΨ„·‹}ώ]>ό‹Sα—σΏzΜΨζ_gŸ: ¨υheα2Πrg»όK}i9•‰ΙπΛρ―Άί.ηΤα`­EΚ•^pΖ„\U²±ΙzŠβΟpP+£DώgΣ¬ιΪUI§QψγρOψ\ ΪΈ‘쏿“CHψ+ρΨ:?­ nˆs€«£%;ό―ΒYΩ%RΟΡJjr8­Β?οίίΫΰ_}‡2Έq<h(Β_ Θ:„Κ@ΰΖ’Ζ:δpš…Ώ’uώΓ”qΰŸΰpΦ‹"ό•ˆ,σ'ϊπ/.ξ€G§]ψ£πΗZΦ-JΜΊxΞzQ„”ε§,ρŸ*£ 4Π‡Τ.ό•,ς‡2Bγ‘ςHœυΒ%%kόΫb-λ₯εE=nXνΒ?ΚσS –ψŸ΅U‘±°!g½8Β_‰Ι +ψyʍστ°‘΅ %'+ό3•P―Cγ¬Hψ+AYΰ Ϊ²nQJ~!ώγοδ ώQŸj0ΟΏΚfe”ΖτŽZΌvα―$eš?β²nQBRρ'Η”η6˜ζ?P/B;Ω2δ!§9ώ• Μς'όπ/.¬Β‰κ₯π‡π―ψ«²7NγTΜCγ +%,sΗά»ΚΞHB9Q½ώJZζψχWφEj\C±yΤ0ώJ\¦ψ§!.λ­βΎ(V Β?Ϊω“-fψ—ϋYΩ©q +άQ1…Ώ’—ώ³”=±GE‘"iώJ`&ψχSvΔjχei‹½·šΪs’•]N{³Mχ/΅δΦA§uΘΕ:W_“–QzΓώG_ψJΜ¦,ΞS±θόŸ§υγVV:ΙJE[€’/JV{Σ™τΓΏψ·DuZζΦ³~ς"J‰«*¨―M³¬Ϋ™R2ΥiΩ[«pFR_¬¨ͺv<ρ Z7r«¨σς·V£Ώ»ΦJHτμΚ:†? ‰j’ο$Ό4Wύ― ½ώάεs½Ι―+$.ψB@λ₯L~“ΈX·†-IΗ’Z»‰%/ΠIμLsυَΨM{዆θΊM7ΉϋŸRoMνΜ†^Αϋ1pΏ>ƒΎφ”ω^›(Wχ΅5‚―6―ή{~{ΎO΅Ι€›Ϋ± e^χV³EΌ€sΥη€q› bwΏ‚kΓlψVκ/=vνΒο·υznΓΟ±h‚³·~¨«―”§‚ή¨¨6ΰχΦD–+’³u~t0Ζ°©ϋφΘ‘κΓχΏu1t’όΛί–φ3αΤΖςcό>5PވTCs“™ζFΩ‘ΔPυΖfώžδγ1 |λŒΗ2Σ{ŒbU›γΊQOtΔεptžU±ΧVc+~°Κ|W΄Ε§oŒΆΞ-›3b_vΉ’­Ξ`­‘:x'›\‰ΆτΧ»ΪκΑt.9ΕBΣΊξuιώ„Kά±y\’hC†ϊZεc%]ˆ_΅vσpAI!žν˜ΨίΠπ«ο4y·Ϋ·ΏβΌ6β;gΡήŠˆξuαάkL{M~ιχL™ξύϊ:šM’ρώŒd/»ahAΖ―SΘgω;]η‘JΖJ\³΅βƒ«NkφΰΛ2τξπQ| [‰”ωέΩΨjω$Ύο{±…EC_ Ρ‘ +Ϊά’2gwOι2λΈͺ +·εΝιm|‘WεθJ‚²TΩ]έV9cα .wΐy +>½[ηΆ~CIΧφ ­Κψ'(C‘Ξm¬{ŽΞΗ²θΜζΛΊΖ‘ˆ(_―w`Β§XΕ›Ϊ^±ΧΫ΄ΕB]ί3νΆ²¦*Qνt+4»Ωώ¨²{€-ρŠg]ϊΔHΡςα—‚¬afωχ;lηΓΙMΓ£zϊč@°»oŸ•QjΡ[Ψ’t⍇ +uCϊΣΣ©»yύλ}ΟΕ°$‹GxΡbέγœ;ώ½ΚΩ†ς³Η₯£ΝL:ΫSΙΨϊΔ θΠ4€΄ΎRΆϋζ•Ώ€™ϊUΟl­λJQΎ2ΊωΨ¬T—ν—Ψ΄χΈ₯lΟ‹―™:  ώ t<7ΐϊŽκ2π‘r›v˜Όdε‘SΌ}Ρ€Α7]ϋ>žP]¦_σ\ώyΘ΄oξXαβOΜψϋͺœo¦ύ³{Λr€–°<yΘ/R€υ’/}~›τ#g-έϊ‚˜“=gⰌ.i,sd³άτ6ŽΤuO¨Υͺk߁OŽ{ύύ…+yυ°0gÊοM;μ‘>]jς~Tn ύθϊ#xa¬Π]HUR…ΊυΣΪwθpczz―ŒSz¦§wνΠαͺ“­υΞqτδψ=PΝ”`˜pL†ΧS°ΔBu`&ύρ³ι[Q+“`fxŠžΏ;ϋ4cNφιIΞΏύ§ΰΏΣ%^LΞy§K”ό”§ζβ|ƒμ²ΙqΦ€―P90ž˜{£δ²ΝqΊΡς_-ό9ŽΨP@%Ϊ[ά3 “ΛFηΘ¦<δ”qΎ@Q`θΐPJώ# SΛF8p6αΐ‰Ί.(P$;@8ΐlγΜ²Υ NώΪΦ υ‰c’χR}XnœXΆΊΓΗ¨ψλ/7ΰŽΒEΕο”%Ί όή[Ω„¦]ˆώΌΕιΗλΝ `΅ώΌνg{„β€άφgsΟ„oρΐ4ζ$ΐζωΨ`―Ν iEHWϋ Ηζ—}-“߁δ5Έ`όξΟ’ρΤ§WΧλ-2 ‘(})θ‚Έώσ7­υLišŸbγ].ΊέOΎ₯–ΈΓϊ +πtP,σWIspΐhΩœj’>Δ“δ«ΏW‡ Ζ`²ΰχ*~MK~(zFπ{Ι•ΰ3Aζυφrρ’ύ€7.msϋ +oB8ΆMm_ +ώ”eΚ²ΨΘ JeښΫφ`†|τƒ|wOηζ6¦…ž_Η=ϊE ЁĿŜ΅0όRΑΊrπ=wWχr™‡Β ½Ϋ2fjwΩ@5g 3y_ψ‘οΟαQΊο’˜«!η½}΅œωύ‰TUυϋΦ`δ/|Έͺk—Κ~w VŸΧ~R|ψ篜p‹£S >*·½oΜμ₯λsφδlY·dκΣ—€πe—Ldό?2‘ώ +endstream +endobj +756 0 obj +<< /Filter /FlateDecode /Length1 1269 /Length2 1151 /Length3 0 /Length 1867 >> +stream +xΪ₯S TΩVTVÊΈΞ ")ŠcΜΉΞ„C1AB‚πGΔ† 1–L+ C°έΫD\Β^(WΘADΐ_Θ‘ˆб3.–IPXΐψ€Nπξ˜rΝ€―‘bΰ‹‹D€ΌpΚGΉs*€€#AαH~ͺR„ωGΤσg0L%Ε‚9€ώX”eŸάp‰¦B‚3i4₯"Ύ2bA.f$ΦΑΙ"BΆ _ΙΩBπJωBΠH°±+ΖsΖ£’ŒR`πP."ŠQhΚR21`ΐCψσ{ v@dΧa)Ÿw_a€nމdοΣ½9Q {Έ:ngoώΘθwiNNxˆ§ZZΡ•ni `Ί• °±΅ –τε ΏH‚ήƒΩo₯σ’ΕoεΗΜΟ0ύeFΜΐ‡ΕΌqε’mύU―C!Δ%x‘γ‘LoFΏύͺέ‹„ΣφΜΝ-Z$šσcΑ•N6Α‘\ΐXά 7N*’-‚(‹ƒy~?D-βH>ͺγΏΠ²y6GL B¦ΟQ©‡π|Q‚+|ŽHŠΜΗ·aΗR7>7:nμΞX>‘vxvΠ1ΔΆRο·E-οhuύuMŽΡtλ?―6,KΞsV±j—šίβθ|Vv’zγ§ υ²K‘εO·}€YTL _ΏΩ»vEQΦyξζϋUp^ΊΝ,B/¨ϊ‘6kοΦέ%/C|rύώ»ξw՚]_ΘT₯Εχ–ψί`_²ΟΡ φD›°½«cλK―DvίτΏ­{œΛV3,Y‚ klWυ4WŒ§αžΊΉ‚?^‹4Ψmxθœ|΄m‹Ολ>iΫpκDΛγΪ«¬|Ά_:iU飆‰Κ’šjΏ.Cyeγ£7½6Žx¦ΌAYΕηΞL©%‰-\νΧGeμG؟ΜΞΈώYΝ€N­ϊ†λ‚Ρ&‡xƒμ%GΊΎ ++ΜΘ=Ξ8―ίVͺσCΡ‘νγkGzW‹š%ωu:Σ‰}wΊ΄©iΛ6 +nΛawΩη»X/’CΆΥ©L”¬ώΛuvŒ}ΡΪmΓ*ΧΧp6”k΄€t.ΟαDl1I±‡«ήόρδδΩςS…Η― k_T§—ιdDuŽ4ΑΆ]γ™ζ<놁•μ‹ΣwJ ~nπ亊o6?₯Π’vn­~CοŽΙŸ<3*>Όυ~ΞLχ?πo”Β²ckw¦Μ*2δG'‘MηM, ˆΩˆq‰κ“ύ₯Εƒ}ς‘ƒ@³ήLα֘)Χne†`1+E~hRΝ¦|PμΥοvχΑ«gΊΚ³§ΚWŒ}’οΈΫ·=₯ζo—Ν/\œ5qݟώlέc=}*£6 3όϋυ GF^‰azχ F[²Ώe܈*½»ͺξκF$―.ԁ‘³€ήχ“*ϋ¦&R+σ¦#½YΕ̌ΙοZ~wςDάeΣOΥ—ξ•?y”_Hƒσ67™ΆοH.‘­)Ήd#9βΏεΘOM³Ceζι©ζc‘λ›»gGSM-›A9;7΅9γ†τ΄έ{~€RœyΛ#,Ή9«ΣωPΜυβ΄f+ΚΈΌΊGuΫkΙS+mEΫΈc,c…ήLέ1&:œδΈοφιΎ}υ>F‘wTκo”Ωε2ό:“Ηύj‰”ΜaΡ‹f[‡ŠWOͺμΰ7N7ΉMξ#-yβΨPMέS.}Fs«?Ι@‘³'‘Ί«ΆρΡΣu@ιgEφ"ωͺtυa-£α³[YUI5gT­O‹^Ό`hfΗwc³ΈA|ABη-V«bXυύαg|θw +endstream +endobj +757 0 obj +<< /Filter /FlateDecode /Length1 2227 /Length2 13588 /Length3 0 /Length 14726 >> +stream +xڝvTœ]Ά%ξξNαξΑέέBp(€p·ΰξάBΰξξ¨rδ6px,¦©Ή–{IΡ΅—#εV⸁]+&¬d‚Ϋ †RSUρ2ΊRˆ2ΚΑ‡χh&π)Ά<Εp8UŒ©υσ3γ Z6δΜύιΖΈεψAi‘ߐφ u@žΥB¦JPFsъlΦΖ u*²ψ­€ν_Ή=πσγ|pω ²Dές]Σβ-lu”²?νψjυrΎΡ#ορΨ ΐn³ΜΓ/lφ8ŠN  ₯¬¦Λ0‘yLDγ3Ωλ²ηΊΓ3]Όη~£“0”Ύ05ΰ•rf8%ϊνΚ)CSˆνE:…ί–ΤχΫRkξD#ϊΣΦ8ίΉΚoΣSΒή$ε Ή›kΚG [«)X‚Ϋ”Ÿ1^΄ϊo`FϊŠ4Iϊ9ς1Μ8φKw”/΄v–LΥ“El Ω⦨2gσΘL  °Xy:tμlΆΎ.—Pȟ-‰άQιε aπ½Ϊ'G WBχ]HUYfτ―€pوCJpέΦ&ω'[)F2QιKΙψίDHΟPμ©@ΆΚb(€FΣΝ"‡>ᑝLk‘ͺό*t3δ4¦»σh.iΑ. Ω‡iη˜—‚¨,²΅ή oK†φΰώ!n`pΪ­ίπι׍I γlŽΌβήI^λΞF3Z{Χ‰΅nK}hg–€l߈"ι¦zŽοyΛλw·UŒΝ€sΘo};Žz€GPj«d5p€*6Υπ?7 Ž_ψ¦k[ΜτIuΌΏαίƒZCΊ~ΗΥηΈΏrοt ~έΰ@f 0 …ϊΤΔ`“:§4ΓΘI’Ύ2NζΠ` +δFΙ!ΘΧά^§Nךγ»*œz.LπJuͺ =ςΙξ*~,–VE‰…e&V\κgZSC^«#E\Ξ—ΦUtΑΈνš’šόLŸV‘p₯—Η₯L Jέ`>COHeύεNήΑ–υΑΤiχ,_…EˆΡσsˆΑαTέLxφPρqŠΪΙ²shRΖ1$Oœp5Ί%7ΐΔΓΜl¬ΌκΉOΘΜΜΩ]5ΜΚΐ θκ|ΈΔbk‡ΐnpΗω‹ ρl|ξp†ΞΟjΔ<Þd;‚$1 ΉtyFžαbο†ίΡ)s.‘Ύ·lϋ<β>π—ΐΰ:z*δ fΑϋ4B+ˆ‘}aOo(ΖΥb֟ž@",,φƒκ΅nIIΘR$ψ±c›5œžχ'Άι‘ε`Vϊ8†Π**Χi&2©%>Ν Fœ?Ώ0κu¬Ι¨IΕ R‚8G ίΑ9U›`M‘²˜•¦ΔΧ*r²τ:Œή’Tά[²Š}ΐ.9Σ’±M;χ—-$ΠrΦBΤ[χ‰KYžEZkζΕ·₯ΘjΨ4Kl‘šθ  εuϋ¨=ρpσ™?5ώ0Μ’ ί/-€j‰Wk7NΆϋ žRT€ϋ겇%βΣyΈ7‰D?γh'+R›%[ˆωš­>ψλΚΤ”±€Ωθ―8&o΄ΧEΫΑ~UdΕ‹a§ΪŸα_½:τΐΔxmt6ο§μ‰IΫEΉŸοWE‰ˆ&Ι·Y!f,Ή'.ŽZ1Όε λcΩ†ξοκ‚ά₯nΟ6γ₯mΜT]Ζ—Eίλ-Ζ,;"LΤ{X'δΦkS§Œ’| +ύιZ G–*+α¨9 +?Ή-ΚY5§m{»ςλŒ‰gŽ8ά;2΅Λ|Έ†  Q₯Ω%O]ψH+œIθwTd;LΣώ²5‘=ƒΟΦ`ΞΈΔκΫΎ‚ΆzςΠε›+5RV‘Πϋ‘œ;PŸ‚έšNΠΰγšθΓάlJQΚι]υυΈ&rνbύρi­•S»^Ψ ά Κ3<dΡ³Φf»πcΈ‡•H2ϋϋθ„Μ υ ͺΫφχΝ—ΰwK–γ±tά̲ۏθ'ΗΛ;,0"ͺ•ΥΚ»W`θθ‰%…|dΦGWΒ4𡫬d”Φ[ΖZΝ›χJ9Νζ$€ά )½άξ(?ϊKXΘMω|4I‹:£Φ֏ =uΌηθQφ 2_•&[cη-δ±R{ω_ΦdEΟΔ4Ι7eΔρ ώͺΰ™U1m Π«_ζ/s2u;:<ύ`H Ή!iWGmτ΄+ΜK_ΨΆ5ϊ} ο»Ϋ˜Z~Ύr»tδ‘Ο‘8χŽƒζŒ«Ν»k²=ˆώFiΰ.4Š}GΣۍ™ΉGϋγψ–”f³τ½EI‰iΏ>~xξ™κiΑ·ΨϊbωχΤιδΕΡι$^ƒυσHb‹nXFžβA “Γ³:„TτΌ‚δ616sώ‘Ν~d‡ΎΦράa:Q§˜uΦNˆ8UšIlε_Ζ6υ²Λ>Q[βΖSˆTΓΌͺμOa’†Ϋ3€Ϊ(ΎσQ³žA@ΰ\ŸΫ‘Ώ˜•Jυ˜τVΊBΨ.Ξ¦Α‡;–ΏόΉγW¦ ½£aλ€=₯7/"ΠΘκτhNο…"3a2ΏF/“zmnΝv±$Nhn<Μ„φϊD©³ζ`χωκ˜:\-7Š—Šι.Ή)›QθI9ΊWΧτδqO…ΦΝ€S˜ο|Q&u–4’wšgϋι“£Ή +ΔβΌ9$Tή€ ν άτgΘ{^ƒIΗ~‹/„θ[@˜%μΦηC–₯›ΎΜΆsγλLZ‡€N!xΪ;ς`£μγς½β€χΟ$؜MFQz™Ήx΅χg™ΟˆψgFG―:U½ϋu!Fͺyθ‘`-θ^vωηitα.π¦Φ8Γz— +dd†ΞgΊ«nRΣ²ράφsΖtυtv?5¨ŽtΞΫΣ:%κ+ρ—U›ͺ&²­ρzͺφΊ‡(ι«εκZF)ξΐΫυ—dZώ˜§ό²Χ†ΣΫ)ζ΅–ŽΥYΒ―[ͺλϋ1ύŒ>«ι" Γν’8Q‚U™ΌΌ^έΊΚ€ααiœύΨͺΤ7ί)rRzωφP ¦d—'ΫŠέ‘Κͺ΅p‡Έnav-ΥΒi%ΡNΉ<ΗΗ.Ao¨žχ’W-ΟT ΥΛ$….α§JψΉω ϋ:μ\a:lΆŒ³!„¬ΆžςX„ΔΜψ{^#"ΈΪϊΟ#γ²Ÿά ΔΣΧ„ϋΦχAϊξ&ςT¨| ϋˆGΉΘ°E^ŽDΖGD’’+j€‡0™Ηm‡,έd£JΘ•‚Ÿ'ηπΔπ#΄V†ΰ²K{©ΞhΉΊjΟΑƒfεUΆ\η¨:?΄©ͺ8ΐP΄yFθŸ#‚Ngΰ2₯3…Γ4Xœ‰ι'fPω¬R(E¬.™Œθ~1yΡαξyG‚eΎΐ₯VhλuS[_σDF«φπ-1ͺπŽ=+Ifχ“6Z •ͺΛόc±αrΐVЧπ‘σ8ότΌ +α_<ο}>_|’!ߊ!šΗ”g©ΖYV…ί5§-¦tJΒ¦B ΒΣ‘ϋ”:Ηyυ˜βΉ/ΉvΰΆ=$!ΌυαΩhL¬ύ$-!ς΄ύˆ|Ξ&Ά +@™τ²‚’%]Ρ<2³!‹Ι‰ΝŠ―χύ+Ϋ¨C€·u†›mQ5φ³¨œΜΫ,mJ|dΓ©›bΌέΞξ%Μ 'Nzτg\4τάυΖ!ψΞΤΈζΤlaσ+ςIΞ»Sό¦Σθ{Lχ܏ω1ηθ`Β¦Ώ”b‚yqΏ=ƒ²ƒψΔ:φw^TΓP7ˆ(Χ%4ΝͺΥ…PΣuυGβx'h‰ `±ϊh›ΩΞ_α†o‡ŸŽœE{i Μδ§Ξ»rοΔΫΫπ§9 šK?ŒOχ*=cι|v ?/ίS΄‘l/θ K‹Νύ9sΙκ_“ζ!qΙ’ΐ~L0™ΕϊMΘ›³wM™+5vκς$Ί\"xxΪΙ°Dgϋ»ίGtG…\‘ ΩΒ€AZρ―[Ϋς²ΣGŠ€‘άδύ2]ΎB9Ω6šί U«dˎ.Gκ +ξ §>ΖΎSvΥ3A#N1=‡ΛU›cΦv–ηΫΒy>.“)πωμ„ιvIzς!­ρP9θ³nΕλδ8x½%ψf{QέΉ·ητH7BapΩΔyJ[Ξσ| ʜ…χ‡5›ȏκ`Qwˆ΅’“~€vFšk]΄ΐP—š­HμFΐ.T‰,oΪdΛ¦ΧρΕKψϋ aΥΣ}@ΝkοEμ'ξΠQes²₯k‘n'΄)ήΨ$ +¨?₯Šm§ς4΄X†Ρur9}ΫY –ŸƒςρΣB»`4³ϋ†ι2­6yžm9Ύό©Q6‘ηŒηζχSŒȈΌ_¬˜M― wBŠΥ’~”XŸ¨Σΐuχ•Ό2§ev‰μΦ‚κ:;Hη…*…:ξυΫ6Wθι"“G·~ΰ@ό8³‰’s\A7ΊΧMήiΙm‘χ„Μ +BΐΑ3αΗ‘Έ­ξ9X]4ΝΙΎΤ<Ώ»x.U *ή η“SdQχŽΥχέ2.αώεΠ0²4[ΔΔ›%μs*owif…θ  9¬Οc‚4$Γ±&j'`hνΘ°Υ!Œ("Eοgwi,ΡΕΨdW.“όMzησ"Ώ°ϊώΰΈμ]T3™Ο‚ +H2}όe oΝ·O†Πkk‘₯_#όŒ­ρ‰₯­…‡J]·Α€Pˆα5™;υ½wAϊΌ ²νΉR6Ϊ‘\²o7γhςGΑζ§`γά…δN(k0λg¬ΎŠΑ»tιΝ5ttΦ/Rσ=αœu’ 2EZ"z6mzAο“w^Ό²π.ςΈ½?Λ‘s›cκΞ%³€uSk/&/Αkϋ³ΠακΕο©SˆΧeX‹κνΙe($a cݎ0©•€ ορ'pδΐRΩ².# ;‘ό;σ΄ΨA’¦ ά^Ν²\ {Ή‘§ΌδKΚτξ» +ŽΩ‘iˆ "7’+Eκ80Υ‘“gΛnβN͊3Ν+³R¨Γw­Œ?$¦GŽ&t0Ίn ₯ζ9Ι_tΌ^λΰΟζ\υI 3­ρ_p£ZήWΞ#4@ϊ΄qψ"bE‘6)¬,UkUΧΉ›§2‘_Xβ›±»Ου·ΩT9φΎΌΒˆΈŠεG.†¦Ϊ₯ͺΠχu3{œcžή뀑ӑr_LΒ$θύzŸHKΞJUόΧ»ϊαλ€˜ί°> ζ΅³šι,wKω+ς§F 2ώσ™YՁλ€Ό‚ΎΗUqŽΊς©βΌŸ) ofςQΣxγ-Ρ‘ιYŽ-Xπ ޟΆ‹Ιηuο”VX~Œ£UΤ³Β²^’‡|’Ί"šE"Υ#ϊqቋfΤ'Χxoβ.Ύό Q#8ω֟\ΆeK4τˆΥ\Δ‡ύTœ±|ψF‘(S©Λΰ΄#£cVΞ>•»οσPΓΈ9§#/ΆΖΛλbέΜ+ΉνŒd ξ%ΡaXZΊ‚ώΌά=CP?1νΥ%Ζ€tϋ< ©Nύ‚A«Λ$ϋΪ„±πΔσυ΄κ>τβ7OŠL ί_֐„vU°ςίSδ&eά]6–;Ν]ωPΑͺΘn“©_NΠs˜γœΣ[ΥΜΊ<«i~Ό㦊'Ρ‘‰θυjE‘ [_Œ©ŽWΤv,Ί-‰]%%°η#&oΤ3;¦ύΰΠ^OΕT>I‘ŒaW­Z—8Ν!9cΜ„1εμέXΎK \―ίUηBΘγωδCc%”;N!$0iΧ{²’Q*™„υ^°0g΅7,–λ~tdig6ΰη@Lƒς“Ν2_μ#D[Ÿϊ+±SΓ”όΩ₯vφ²1}8$ΧTXU}]¨ς΄ΞφaΛ―¬– σUξyTωΆπy²‘ŠΊL™Ή΄ρ— glWΧΊŒ­g9£v7ΈRюΞΰMΒ6z=›<Ρ$¨DRυWι™τΗ_@Cϋ1Ψ7 +-₯ΈϋOΪΪ(ο("X[ Βμ]voΒxxΣ)ϊΑ£·/³‡κyͺυ7W˜Ά%πϊ\±V{Ηέβ~μ>{λ§U +˜6xd±ΟΦP–)©±Ή§­V–€Ÿ!EΏ„ΰΡٜ(Χ."α?κ_’tϋΤN59<~•MRΛΌΏΦD’―†α‘X₯jΜΠΛ=K†·δV”·:VZ>#zPΡXξD'›aΰ{S²Όβξ―@―6C·ζΠ‘ο{τΒΓ»Ύψΐ“uΚνΗOp·6•K»R Ώ_>sc–ϋ^Γo| ε]Χ*κΰ@`‘ΖίoӜΙ폩r&-χΩΪn+¦΄‡cA:ίΣ¨δzMMM,Ηf‰[B:±CAγgδeaMώυ%μ,lžθ{qυ-oœ Šžk‘>+'8ς&ΧΟ3™©¦ϊΤΏΏTb6μΕ-’ ΰ΄£χX™Σ +ΤΘ\NxHSΓ:Q&‘§@ͺy1qίf—;7Γ¬^p‡υή`υHέξͺ%AŽ γκd‡Άp_ς|i—ά„lmΎQ¨Q8ϋŏŸN­η}"Ύ<>ž%ι52¬^fs ‡Ί;vzΚ՟2(€΄RTΐ›PχF‰§€;ξ*`—'v6 LF…4oX‘εkμ~Φ, až;'η‘Πξ›s·½!tec³Ε•xέCΙ9ˆ„‚5€[F¬ΜU*ΖΦΌžκΆK ܏lδ“ε7U²š]Φκ^μ=s―-–€7wΒ’[μX»ΒΛιμ K$κsYD«ρΓΎέ>ξΗGt™T0eΠ££“t bŸίnAγ/t.E X:QŒ¨s@©$ΆR‘ηνΌΡ"λώέfh7,#ϊ›wΌ4νbTΘπ{Ό\ς‚ Νx5cΚύP +δκΊώ»ϋΈ °Y₯|α2μaΡΰΖmY4ΩPφ-a’`d6™|ά¨/…ΑC15q:ό°ΌK/F'ΫϋΫ¨θ3ηA0}ΐκŠW-«„(πi}5Lί·³Y-Ρ{CŒΊC%¬{4lχν&ςaNα₯L9ϋP1QMI/ΥGηΞΎ"‹.σ<+‘mz₯ωΤW]ϋΐ80ΗΔ(΅΄&ߝ˜‡Ι€ͺν_uU'>H7θ„ηΰF\…’NΘBf*€’ΎB}γxovΦ·1ΊxαθCCΝΈ‹S}ψH–μΰU±XΟΊ©H[™ΗπθX–,ο9¦°Ζ­ygΡΑutoβb$”rž]ΌN‡IGJˆ‘›%Ι—Sf7-sφPš½"³$t­Τ©"Š»z}?θ7{jκxV&f‚\#4lƒΜjΏ`8£“ǝ“Ϊ5šΑΐ?Θ{ +]υ‰ͺι‘B2]HΜΘCΩfδδψš’·+ώaœ 9hΛsZϋ 5ρφϊ£v=}u@!ΙV =jά ΨΧζΧIλψ’±ο“#ηb7jyKί + D ېZ?κVVθ/τΌΖ8#IΖ&Δϊ=-α+”TλfKt8Ϋ&ΖγΝC»β/΄»qΩϊ³³KW*ϊh.ZUΕ”Ž”.(q?Fϋθ°ΡΓ§μt‘掍 Έ{žvw‹£/ ‘G­‡‰[Q|χSNμ~τ‰dν O±)…žΔP +ΝLU™$/‘ˆ‘ΑE’Α‹m‡¬o_½ς/TK9–zUξͺπ2yG³J΄Zsj +ώ)¦§€B'Q£K€υβj²49%Iχ„TΚ2aΔu¬\hΨρν•ι³,8\τ{t¨`ΝVU·‹6h’ΗpeδΦSw#Έsb₯oeq₯θ‘ΑDjάξ Lvχ•= +Μ†ž>mŠΤ7…χ8Ηζψ3†4ύπς₯UΨQΆ¨μΈ±Ψ1™΄ Œ5ŠIَŽJμ•σΥΧ3Κ'’A“}‘.s睫V‘’G‡Χ+Mc„ )sR‘?™]:K +Ό0kΜ‚S|±“‡3φ ‘yζ³l(NZ3πφIε‰Υt–JαΏΠξ +Ϋ9~»ΖѝόΉΉe¬γ…Πφ +ο olTͺ}‹CHθ`‹΄wnž}ˆϊ’χuΉ $^UAΛφ’Ώβ1&&6ŒIdόΞά拉ΖΛlEΰ²,gΓόB CX#FΞ%Pd?αωcωOν₯YE†ΥΕϊάμ-•„5²xπ( lΌΐkή~±j¦ηƒ{)Ž_d£ΣΔR€ €s5;Ή†GTΉ'ά΄E©r ·ΚUŽ—ΐž5‘_CzΌvQ†΄ξ6αλέ²-’δWŽ-ψυΠ*»€•άΧόƈΠΌ'lw‡oM ~·,n©|‡KΝZš ԈŠΦJUS{Gxςΰwν½ZΠΓΙϋi;©ΓΒα=J_t5 9c?OB€‹P'ΫsN’¬g›tΖλζšΒ~=Œ’\mΆ:Ϋ’Α%AhreDϊz†O™|Σ-ϊ"φυjθY9M³#)γ˜hm ’ΏΓgv‡ φž€6vEΣ +A,Jι‘πhA€θ‹­ΤκώG„rѐy₯MKΉαx¬0¦ΙWΦj˜f+h{ͺΕτ «νΡμ°=δ9†XJy† Ή[g»Τ΄ΰ„@Œ¬Ψθο -™Σ­Lw_nj§RpοΙ©@,σGp­V―#Mp›©tΉ“9ΆσΥ“¨’μύς[eδιΆΉjψ‘Di‰Vx”TBhy*QΗϋΗ"Yϋ³)’?(ί‰ˆ%„Œ§Ζ²‘±@ΑΗ:f‡”Φ_Θ₯ΫmΫ¨•†,¬N’$γ-ς—²ϊyzΦaϋ8π0ά[ ɚ;Ψθ"ΨΐcY)™ά ά6-χε™νŽ9XF“2",$ ’£3ƒˆ²%‰±^hΞ½! +ζι³ρ<•%\ά­Χ.e6€w™Ρ6π“φ?HU ΰTŠΐμ«ΨςΤEψšϋ‹y{yU”6Δ#ηκ‘ΚΨ!>Σ)؁͹ΡNπFaœΫ‚αΊ–D…U)Υ >ΚΌ<Ώ³"—~χ~7”hJ‰2Ρ;%GF°ο‰ŒR7η«G`ΓΨ¬R]|t6Ώ„ΈpA&>sλΐ"²EU–CK;‘χ8υεŽV‹Τ<ιω‘C;Βv:ΙχΆπ]Δ_Œ»ϊDJJš™€Q>N•Δ¬ύޝŸ†} ͺΛrυζ’°ZσββŒΤΠ#€x½‡(θ%)²ω-ΙβΓ.βJ$ͺ}ΞΣ2ΈjάρΔ_ͺ½{<”….4―JΰΙ»1 谈b9.Ρh(|ˆ8N$WS˜.‘·DtΔΨ…zn1„›:IrΎ]ε!KΎΰT–ήΜ|E‰.r9Žryl*άtΝγ“–'μcTΎ˜§8σίn]²”T‡?D“iC‘ςί\όylί!2ΔΗώte‘d-sŠ‹ζž+Κε‹ϋ’ͺm:°ƒάNηλƒϋ_«ˆΆΧŸIhΕ½‹ΰQ¨Δ ΆΪήά>μJ{•;~%#ΈeΡ¦θ‚.&@Šr™”Ϋφ’Iφ«V‡rNρe›WωΓ:&ΛE ԎŒ­© +ZBš€…D”{|XίΏτnσΧafΠα>ZΣͺ!AΉUΫΖPc‹ž‹}”Mωgυ*€ŽΝΊ§]ƒŽ½:‘ςν’ ρ»+›$bαhS„Ω€λЊΠX·φ«λ£EFχ±&Y:€\Ζ\ΙΔIb±ΙΟΠΫ>/wgγ­γNυ ];Ώ€]Τ3RΩ$*Σ0Β:―­­δΕƒ±OFΞSs€―Gξ—N†+PWWΕGΓTσΡ°υ½Ύe>}&Ž·€ηΩx–iΒ‰ήx ύωBQf4g[œλο0₯_Tάβε:»ε‡΄ΧΪ·`Ζ2F¦¨ϝƒRi{²ΓσPψN±*όŠΊ}ή§ˆςA’άq?π•$0Ψ"šνlˆΞΟFΗΤoΫ 2°`gΔ +qFλ;Ιγu:ί§ +‘¨Nΐρ~μΰ+šώPΎ†ι-’H²νθnηq;ξη\₯u\aπJάR¬©žWΰ°,­¬ΚyιΔ-f&ΩΛ$)‰…ξρs]7Ξό§θ́ h%›pnδ=Χ–Ηη/rEΑ=ΰ4DYΉΦ‚uL[₯ ϋŸ%υ⒏"¨²S &·ΖQ)£Κš7²*ϊέγ[±Ÿ7λlku~`ΒΞ_sEΦΕžeΑ†Aζ\fζθΡτΐ7Ž·€+Ξz<Έ;w‘σΖ·i{„S?vλoδdΙϋ^?vž#ΡͺΫ δΑU~/ π‘‹Υφ’“ŠmUTΖ‡'‹SΙκέNξλEΞαT…x•wυ7’5K„π Υ`Ψ“ΑΡ%bs$θ.@ςƒ…υ;βγ­t–oΠ₯24p‰7‘…HΛ$^½dπ.4so“©hc5qGάHGΰP†£Ό£.(νΊΨY +χ ΎjΜωE6}Ο―wΙEγ3G†λΥgKuU ΄ΆΤ©›΅½ιM³a՝‚(^w£σ eέs&-α‡<;Ε„Ž‚œ3cXœGτ¦qπxαN+«NΊT±:0hά§Όσ#ΰc±ΣΩ-sωιŸ‡’Ψ²ζŸ“5œ3γΔI‘ΑΑηΰ«Ί‚ZcˆΤρ&ή‹pc΅k­δMτ ‚ew}t€‰>‰[/­^Ε=₯ +Œ*jϋ)Δ*Q,Β γ +―²e™φξκr™U¦δ»[ο™ΘZΟν±Ξ7tΥ›Fϋe{΅Gw0+)e/$–Ϊ₯}(a?­α!ψ‘£ζ\°o'FΨ„ϊ UrκGtθR6 ;C7`γ{GL‹9ΈŒ± _vb©ͺ-Ό›ΘShσ©ύ’sWξψκf…΅Δt“οΎο‘όΞφυˆhψeMϋ6Δ©­ΝЁβW…Ψ–βD’»υ΄UJΠΩή»fܜ•/εHδέιθ2r}_σ*D₯kt˜]·—3‰ŸHv“Δ- +ξ +ΞR€{M‘Η^šέœJ7сΑ—JDNrkκ±Wfόrκςβ›ΓΌΓ]mk¨}¦Έ`ςΓ + ½Ž? ι±(w> Οy.Π,' +l£€έ žϊjYd +:‡™oaΨZ-3£!Œτυ0° +IΥv¨N€Ρ₯›0#Υ'°™JœΈΆ~‘ŠεD΄΅,jμ‘ιš%jΐ ,Σ:μ )†p\¬];<“D^•UWΤ²AšŠΆͺjH]-VŒ;o–,r<œ“‚Μ]x!δ_.)Ž>iΟΩKεΊ/>Ρσ…¬·›'Χτ›Μ¬Εf]'±ϋωόΓkΔ’sh£‘Ϊ4©ΥΙbεΙy3ͺ(ΐΏCGXm8kξUAcNžP3lB9Υ%žδ6ω0ϊ9/ΒΓΕΨΦ¬μκKͺ˜ρ™ƒŠW‰ΧAFΈ“φβΡΖ²λIrVΰ-χχ*ι§'ΒNN*Šο€ƒέŒ§!jο›λ,y΅ ΗδΛΗ"žΦc Ÿ½4Š&S>iŽ έ(vNLgP \Όp(Ύ:YΌΝ>PβH·a.°tw rLƍœ¬|` ‘CW‘ICšλρ•&Ό"ςšAVEΏb3Iμ9,RŽϊyΜΫΪη‘2ω‘‰₯"σ4’·6™ ΜεTœW’aJMρJΗΨΕ€ž:ͺς"—ΒΤ{%?(`bp¦ΎίίΑ'Xd".ΫԌ¨κBΔ“kΦΦΞΕLϊό"0F ›£νަ˜xnξ5‘ΪΑψ‘4]ϋdς\eζ„)αΙ0Κ/JrΠ'5~sL©τΥαž•‹Kd?fΆΕιƒ%p%@’Δν ΧίΞοŒ 'Ϋ«ͺτu ΙαΤΛh"ϊσϋpc]φs–a^^ +ŒΪΗ†0Χx ΡύφKEων /_~F· +ρΚΣwgΓΓΎ6qDŽFoDέόM³mg'ΪΈKqέφjψΌMzˆpζUŸ:ΨάIΌ…&qυ1%μ―ά¨Ϊ¦NΉ`Λώ•¬Ί³Ξ΅\Υ0’j¨x2ό:™ŸˆΦdΠ;*£]…8_)½j£xN.fŽ_b’ιίΧ/γ–…œΧ¦™W Ž   `vό>šΪ½ρͺσ+Βb‹§y˜™X™¬°ΫD€Ο–ι™ή‹ςχΐ–PΑ`λ/|₯ ‚ΰ0ΛΨ₯η5‹uιθš>.+gjΨ+Cφ,˜ ˆ ”}μ«`{¦‘ΐUf€…μΟ'έ»Ψ(pςλ©ξ`Ά­Ovξ§‘ŸxALnΉT₯ΫΈηΗξ΅ΎŒ°Λ(θ}1,XΕ'xμΖΤ‹œB}Xk7δx΅Α²{ΗσαSߙȍ]ύwgήΧνŒχ7ΑΔ»η3mMˆMίΨαBύ%lΙteΧS4+2:ω·κhΓ―οF!3vO€ˆ XS{κΒc?D jέΖaεΥΊΑ6z‘έΤΥ/šhx ]§©ΰ”rvΰΟHtyψ$zρF”Ώ‘ύ`{”jO°‘Ψ]ηΗή[S>e "‚E[‰JJ,Ω―Ή–'²όXίΧ)¨ŠNyŸ£°"―2ΑM,]Β#μ +H/πιλλ-4X!&8πϊGH¨·<žWŽH1¬0ίμ8Ω)`|‹ΗΆDJιRvΈ~œ§†Ω«Fz'·Jαm½e^¦˜·¬Σ'4‚³ΏΎ>ŒQοψiηy’BιΡΊWΤ Ψω«ΫκnόφΗ\³bρsΘ(S[‚95ialuηA,t@S- Ν +Um)Λ^λΗX5LωαΥ@M@<$3_ΙςX‚σX(ν QE&ή +Ζ¨μ <4*IWh°λΡ‡^›'VςΔ!―OwΟ>0²ΙΧ…Ο²_Ρ ³ρ§ζlΗͺή8…dvCθτQ‰Ό@ΝG.xr< ( «3<,±žώΒPΤΡ2μŽΣœd„RMΦ?w!r/‹ͺ΅ή@ίι=Αά¨βφ*(ΰΏ‰Θ2Ι9)Κγ.ΛiAχωC—CνͺYί@ε>΅i¬υS§ήΖ+’GЌ5ωΰJ}}~ξΝFυ¨4Ι0ος()ƒ½yΨ΄ςώ|Β8œVv;-^ "όž=M‰MfρSSΓ”·$LΥ ?6’ς[ ±²ΧK$Τ 5εύ6p‚<ΤkΞU\Gy–=Ze”|frh%rƒόιuγ1MλάΥΝΝΧ±ZύιΕežςιVuΣόω)CmϊBy {K^ +endstream +endobj +758 0 obj +/UGSFAT+NimbusSanL-Regu +endobj +759 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +760 0 obj +<< /Ascent 953 /CapHeight 953 /CharSet () /Descent -285 /Flags 4 /FontBBox [ -174 -285 1001 953 ] /FontFile 848 0 R /FontName /UGSFAT+NimbusSanL-Regu /ItalicAngle 0 /StemV 85 /Type /FontDescriptor >> +endobj +761 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +762 0 obj +/PXVYKT+CMSY10 +endobj +763 0 obj +<< /Differences [ 0 /minus /periodcentered /multiply /asteriskmath /divide /diamondmath /plusminus /minusplus /circleplus /circleminus /circlemultiply /circledivide /circledot /circlecopyrt /openbullet /bullet /equivasymptotic /equivalence /reflexsubset /reflexsuperset /lessequal /greaterequal /precedesequal /followsequal /similar /approxequal /propersubset /propersuperset /lessmuch /greatermuch /precedes /follows /arrowleft /arrowright /arrowup /arrowdown /arrowboth /arrownortheast /arrowsoutheast /similarequal /arrowdblleft /arrowdblright /arrowdblup /arrowdbldown /arrowdblboth /arrownorthwest /arrowsouthwest /proportional /prime /infinity /element /owner /triangle /triangleinv /negationslash /mapsto /universal /existential /logicalnot /emptyset /Rfractur /Ifractur /latticetop /perpendicular /aleph /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /union /intersection /unionmulti /logicaland /logicalor /turnstileleft /turnstileright /floorleft /floorright /ceilingleft /ceilingright /braceleft /braceright /angbracketleft /angbracketright /bar /bardbl /arrowbothv /arrowdblbothv /backslash /wreathproduct /radical /coproduct /nabla /integral /unionsq /intersectionsq /subsetsqequal /supersetsqequal /section /dagger /daggerdbl /paragraph /club /diamond /heart /spade /arrowleft 160 /space /minus /periodcentered /multiply /asteriskmath /divide /diamondmath /plusminus /minusplus /circleplus /circleminus 173 /circlemultiply /circledivide /circledot /circlecopyrt /openbullet /bullet /equivasymptotic /equivalence /reflexsubset /reflexsuperset /lessequal /greaterequal /precedesequal /followsequal /similar /approxequal /propersubset /propersuperset /lessmuch /greatermuch /precedes /follows /arrowleft /spade ] /Type /Encoding >> +endobj +764 0 obj +<< /Ascent 775 /CapHeight 775 /CharSet () /Descent -960 /Flags 4 /FontBBox [ -29 -960 1116 775 ] /FontFile 849 0 R /FontName /PXVYKT+CMSY10 /ItalicAngle -14 /StemV 40 /Type /FontDescriptor >> +endobj +765 0 obj +[ 777 277 777 500 777 500 777 777 777 777 777 777 777 1000 500 500 777 777 777 777 777 777 777 777 777 777 777 777 1000 1000 777 777 1000 1000 500 500 1000 1000 1000 777 1000 1000 611 611 1000 1000 1000 777 274 1000 666 666 888 888 0 0 555 555 666 500 722 722 777 777 611 798 656 526 771 527 718 594 844 544 677 761 689 1200 820 796 695 816 847 605 544 625 612 987 713 668 724 666 666 666 666 666 611 611 444 444 444 444 500 500 388 388 277 500 500 611 500 277 833 750 833 416 666 666 777 777 444 444 444 611 777 777 777 777 ] +endobj +766 0 obj +/YHUNPG+CMR10 +endobj +767 0 obj +<< /Differences [ 0 /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /exclam /quotedblright /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /exclamdown /equal /questiondown /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /quotedblleft /bracketright /circumflex /dotaccent /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /endash /emdash /hungarumlaut /tilde /dieresis /suppress 160 /space /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /sfthyphen /nbspace /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /dieresis ] /Type /Encoding >> +endobj +768 0 obj +<< /Ascent 750 /CapHeight 750 /CharSet () /Descent -250 /Flags 4 /FontBBox [ -40 -250 1009 750 ] /FontFile 850 0 R /FontName /YHUNPG+CMR10 /ItalicAngle 0 /StemV 69 /Type /FontDescriptor >> +endobj +769 0 obj +[ 625 833 777 694 666 750 722 777 722 777 722 583 555 555 833 833 277 305 500 500 500 500 500 750 444 500 722 777 500 902 1013 777 277 277 500 833 500 833 777 277 388 388 500 777 277 333 277 500 500 500 500 500 500 500 500 500 500 500 277 277 277 777 472 472 777 750 708 722 763 680 652 784 750 361 513 777 625 916 750 777 680 777 736 555 722 750 750 1027 750 750 611 277 500 277 500 277 277 500 555 444 555 444 305 500 555 277 305 527 277 833 555 500 555 527 391 394 388 555 527 722 527 527 444 500 1000 500 500 500 ] +endobj +770 0 obj +/NZOHYK+CMSY7 +endobj +771 0 obj +<< /Differences [ 0 /minus /periodcentered /multiply /asteriskmath /divide /diamondmath /plusminus /minusplus /circleplus /circleminus /circlemultiply /circledivide /circledot /circlecopyrt /openbullet /bullet /equivasymptotic /equivalence /reflexsubset /reflexsuperset /lessequal /greaterequal /precedesequal /followsequal /similar /approxequal /propersubset /propersuperset /lessmuch /greatermuch /precedes /follows /arrowleft /arrowright /arrowup /arrowdown /arrowboth /arrownortheast /arrowsoutheast /similarequal /arrowdblleft /arrowdblright /arrowdblup /arrowdbldown /arrowdblboth /arrownorthwest /arrowsouthwest /proportional /prime /infinity /element /owner /triangle /triangleinv /negationslash /mapsto /universal /existential /logicalnot /emptyset /Rfractur /Ifractur /latticetop /perpendicular /aleph /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /union /intersection /unionmulti /logicaland /logicalor /turnstileleft /turnstileright /floorleft /floorright /ceilingleft /ceilingright /braceleft /braceright /angbracketleft /angbracketright /bar /bardbl /arrowbothv /arrowdblbothv /backslash /wreathproduct /radical /coproduct /nabla /integral /unionsq /intersectionsq /subsetsqequal /supersetsqequal /section /dagger /daggerdbl /paragraph /club /diamond /heart /spade /arrowleft 160 /space /minus /periodcentered /multiply /asteriskmath /divide /diamondmath /plusminus /minusplus /circleplus /circleminus 173 /circlemultiply /circledivide /circledot /circlecopyrt /openbullet /bullet /equivasymptotic /equivalence /reflexsubset /reflexsuperset /lessequal /greaterequal /precedesequal /followsequal /similar /approxequal /propersubset /propersuperset /lessmuch /greatermuch /precedes /follows /arrowleft /spade ] /Type /Encoding >> +endobj +772 0 obj +<< /Ascent 782 /CapHeight 782 /CharSet () /Descent -951 /Flags 4 /FontBBox [ -15 -951 1251 782 ] /FontFile 851 0 R /FontName /NZOHYK+CMSY7 /ItalicAngle -14 /StemV 49 /Type /FontDescriptor >> +endobj +773 0 obj +[ 892 339 892 585 892 585 892 892 892 892 892 892 892 1138 585 585 892 892 892 892 892 892 892 892 892 892 892 892 1138 1138 892 892 1138 1138 585 585 1138 1138 1138 892 1138 1138 708 708 1138 1138 1138 892 329 1138 769 769 1015 1015 0 0 646 646 769 585 831 831 892 892 708 917 753 620 889 616 818 688 978 646 782 871 791 1342 935 905 809 935 981 702 647 717 719 1135 818 764 823 769 769 769 769 769 708 708 523 523 523 523 585 585 462 462 339 585 585 708 585 339 938 859 954 493 769 769 892 892 523 523 523 708 892 892 892 892 ] +endobj +774 0 obj +/ASBNUF+CMR7 +endobj +775 0 obj +<< /Differences [ 0 /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /exclam /quotedblright /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /exclamdown /equal /questiondown /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /quotedblleft /bracketright /circumflex /dotaccent /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /endash /emdash /hungarumlaut /tilde /dieresis /suppress 160 /space /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /sfthyphen /nbspace /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /dieresis ] /Type /Encoding >> +endobj +776 0 obj +<< /Ascent 750 /CapHeight 750 /CharSet () /Descent -250 /Flags 4 /FontBBox [ -27 -250 1122 750 ] /FontFile 852 0 R /FontName /ASBNUF+CMR7 /ItalicAngle 0 /StemV 79 /Type /FontDescriptor >> +endobj +777 0 obj +[ 706 938 876 781 753 843 815 876 815 876 815 677 646 646 970 970 323 354 569 569 569 569 569 843 507 569 815 876 569 1013 1136 876 323 323 569 938 569 938 876 323 446 446 569 876 323 384 323 569 569 569 569 569 569 569 569 569 569 569 323 323 323 876 538 538 876 843 798 815 860 767 737 883 843 412 583 874 706 1027 843 876 767 876 829 630 815 843 843 1150 843 843 692 323 569 323 569 323 323 569 630 507 630 507 354 569 630 323 354 600 323 938 630 569 630 600 446 452 446 630 600 815 600 600 507 569 1138 569 569 569 ] +endobj +778 0 obj +/SACKHC+CMBX12 +endobj +779 0 obj +<< /Differences [ 0 /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /exclam /quotedblright /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /exclamdown /equal /questiondown /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /quotedblleft /bracketright /circumflex /dotaccent /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /endash /emdash /hungarumlaut /tilde /dieresis /suppress 160 /space /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /sfthyphen /nbspace /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /dieresis ] /Type /Encoding >> +endobj +780 0 obj +<< /Ascent 750 /CapHeight 750 /CharSet () /Descent -251 /Flags 4 /FontBBox [ -53 -251 1139 750 ] /FontFile 853 0 R /FontName /SACKHC+CMBX12 /ItalicAngle 0 /StemV 109 /Type /FontDescriptor >> +endobj +781 0 obj +[ 675 937 875 787 750 879 812 875 812 875 812 656 625 625 937 937 312 343 562 562 562 562 562 849 500 574 812 875 562 1018 1143 875 312 342 581 937 562 937 875 312 437 437 562 875 312 375 312 562 562 562 562 562 562 562 562 562 562 562 312 312 342 875 531 531 875 849 799 812 862 738 707 884 879 418 581 880 675 1067 879 844 768 844 839 625 782 864 849 1162 849 849 687 312 581 312 562 312 312 546 625 500 625 513 343 562 625 312 343 593 312 937 625 562 625 593 459 443 437 625 593 812 593 593 500 562 1125 562 562 562 ] +endobj +782 0 obj +<< /Differences [ 0 /minus /periodcentered /multiply /asteriskmath /divide /diamondmath /plusminus /minusplus /circleplus /circleminus /circlemultiply /circledivide /circledot /circlecopyrt /openbullet /bullet /equivasymptotic /equivalence /reflexsubset /reflexsuperset /lessequal /greaterequal /precedesequal /followsequal /similar /approxequal /propersubset /propersuperset /lessmuch /greatermuch /precedes /follows /arrowleft /arrowright /arrowup /arrowdown /arrowboth /arrownortheast /arrowsoutheast /similarequal /arrowdblleft /arrowdblright /arrowdblup /arrowdbldown /arrowdblboth /arrownorthwest /arrowsouthwest /proportional /prime /infinity /element /owner /triangle /triangleinv /negationslash /mapsto /universal /existential /logicalnot /emptyset /Rfractur /Ifractur /latticetop /perpendicular /aleph /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /union /intersection /unionmulti /logicaland /logicalor /turnstileleft /turnstileright /floorleft /floorright /ceilingleft /ceilingright /braceleft /braceright /angbracketleft /angbracketright /bar /bardbl /arrowbothv /arrowdblbothv /backslash /wreathproduct /radical /coproduct /nabla /integral /unionsq /intersectionsq /subsetsqequal /supersetsqequal /section /dagger /daggerdbl /paragraph /club /diamond /heart /spade /arrowleft 160 /space /minus /periodcentered /multiply /asteriskmath /divide /diamondmath /plusminus /minusplus /circleplus /circleminus 173 /circlemultiply /circledivide /circledot /circlecopyrt /openbullet /bullet /equivasymptotic /equivalence /reflexsubset /reflexsuperset /lessequal /greaterequal /precedesequal /followsequal /similar /approxequal /propersubset /propersuperset /lessmuch /greatermuch /precedes /follows /arrowleft /spade ] /Type /Encoding >> +endobj +783 0 obj +[ 777 277 777 500 777 500 777 777 777 777 777 777 777 1000 500 500 777 777 777 777 777 777 777 777 777 777 777 777 1000 1000 777 777 1000 1000 500 500 1000 1000 1000 777 1000 1000 611 611 1000 1000 1000 777 274 1000 666 666 888 888 0 0 555 555 666 500 722 722 777 777 611 798 656 526 771 527 718 594 844 544 677 761 689 1200 820 796 695 816 847 605 544 625 612 987 713 668 724 666 666 666 666 666 611 611 444 444 444 444 500 500 388 388 277 500 500 611 500 277 833 750 833 416 666 666 777 777 444 444 444 611 777 777 777 777 ] +endobj +784 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +785 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +786 0 obj +<< /Differences [ 0 /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /exclam /quotedblright /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /exclamdown /equal /questiondown /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /quotedblleft /bracketright /circumflex /dotaccent /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /endash /emdash /hungarumlaut /tilde /dieresis /suppress 160 /space /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /sfthyphen /nbspace /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /dieresis ] /Type /Encoding >> +endobj +787 0 obj +[ 675 937 875 787 750 879 812 875 812 875 812 656 625 625 937 937 312 343 562 562 562 562 562 849 500 574 812 875 562 1018 1143 875 312 342 581 937 562 937 875 312 437 437 562 875 312 375 312 562 562 562 562 562 562 562 562 562 562 562 312 312 342 875 531 531 875 849 799 812 862 738 707 884 879 418 581 880 675 1067 879 844 768 844 839 625 782 864 849 1162 849 849 687 312 581 312 562 312 312 546 625 500 625 513 343 562 625 312 343 593 312 937 625 562 625 593 459 443 437 625 593 812 593 593 500 562 1125 562 562 562 ] +endobj +788 0 obj +<< /Differences [ 0 /minus /periodcentered /multiply /asteriskmath /divide /diamondmath /plusminus /minusplus /circleplus /circleminus /circlemultiply /circledivide /circledot /circlecopyrt /openbullet /bullet /equivasymptotic /equivalence /reflexsubset /reflexsuperset /lessequal /greaterequal /precedesequal /followsequal /similar /approxequal /propersubset /propersuperset /lessmuch /greatermuch /precedes /follows /arrowleft /arrowright /arrowup /arrowdown /arrowboth /arrownortheast /arrowsoutheast /similarequal /arrowdblleft /arrowdblright /arrowdblup /arrowdbldown /arrowdblboth /arrownorthwest /arrowsouthwest /proportional /prime /infinity /element /owner /triangle /triangleinv /negationslash /mapsto /universal /existential /logicalnot /emptyset /Rfractur /Ifractur /latticetop /perpendicular /aleph /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /union /intersection /unionmulti /logicaland /logicalor /turnstileleft /turnstileright /floorleft /floorright /ceilingleft /ceilingright /braceleft /braceright /angbracketleft /angbracketright /bar /bardbl /arrowbothv /arrowdblbothv /backslash /wreathproduct /radical /coproduct /nabla /integral /unionsq /intersectionsq /subsetsqequal /supersetsqequal /section /dagger /daggerdbl /paragraph /club /diamond /heart /spade /arrowleft 160 /space /minus /periodcentered /multiply /asteriskmath /divide /diamondmath /plusminus /minusplus /circleplus /circleminus 173 /circlemultiply /circledivide /circledot /circlecopyrt /openbullet /bullet /equivasymptotic /equivalence /reflexsubset /reflexsuperset /lessequal /greaterequal /precedesequal /followsequal /similar /approxequal /propersubset /propersuperset /lessmuch /greatermuch /precedes /follows /arrowleft /spade ] /Type /Encoding >> +endobj +789 0 obj +[ 777 277 777 500 777 500 777 777 777 777 777 777 777 1000 500 500 777 777 777 777 777 777 777 777 777 777 777 777 1000 1000 777 777 1000 1000 500 500 1000 1000 1000 777 1000 1000 611 611 1000 1000 1000 777 274 1000 666 666 888 888 0 0 555 555 666 500 722 722 777 777 611 798 656 526 771 527 718 594 844 544 677 761 689 1200 820 796 695 816 847 605 544 625 612 987 713 668 724 666 666 666 666 666 611 611 444 444 444 444 500 500 388 388 277 500 500 611 500 277 833 750 833 416 666 666 777 777 444 444 444 611 777 777 777 777 ] +endobj +790 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +791 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +792 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +793 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +794 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +795 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +796 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +797 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +798 0 obj +<< /Differences [ 0 /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /exclam /quotedblright /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /exclamdown /equal /questiondown /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /quotedblleft /bracketright /circumflex /dotaccent /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /endash /emdash /hungarumlaut /tilde /dieresis /suppress 160 /space /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /sfthyphen /nbspace /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /dieresis ] /Type /Encoding >> +endobj +799 0 obj +[ 625 833 777 694 666 750 722 777 722 777 722 583 555 555 833 833 277 305 500 500 500 500 500 750 444 500 722 777 500 902 1013 777 277 277 500 833 500 833 777 277 388 388 500 777 277 333 277 500 500 500 500 500 500 500 500 500 500 500 277 277 277 777 472 472 777 750 708 722 763 680 652 784 750 361 513 777 625 916 750 777 680 777 736 555 722 750 750 1027 750 750 611 277 500 277 500 277 277 500 555 444 555 444 305 500 555 277 305 527 277 833 555 500 555 527 391 394 388 555 527 722 527 527 444 500 1000 500 500 500 ] +endobj +800 0 obj +/LYTCQL+CMSY9 +endobj +801 0 obj +<< /Differences [ 0 /minus /periodcentered /multiply /asteriskmath /divide /diamondmath /plusminus /minusplus /circleplus /circleminus /circlemultiply /circledivide /circledot /circlecopyrt /openbullet /bullet /equivasymptotic /equivalence /reflexsubset /reflexsuperset /lessequal /greaterequal /precedesequal /followsequal /similar /approxequal /propersubset /propersuperset /lessmuch /greatermuch /precedes /follows /arrowleft /arrowright /arrowup /arrowdown /arrowboth /arrownortheast /arrowsoutheast /similarequal /arrowdblleft /arrowdblright /arrowdblup /arrowdbldown /arrowdblboth /arrownorthwest /arrowsouthwest /proportional /prime /infinity /element /owner /triangle /triangleinv /negationslash /mapsto /universal /existential /logicalnot /emptyset /Rfractur /Ifractur /latticetop /perpendicular /aleph /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /union /intersection /unionmulti /logicaland /logicalor /turnstileleft /turnstileright /floorleft /floorright /ceilingleft /ceilingright /braceleft /braceright /angbracketleft /angbracketright /bar /bardbl /arrowbothv /arrowdblbothv /backslash /wreathproduct /radical /coproduct /nabla /integral /unionsq /intersectionsq /subsetsqequal /supersetsqequal /section /dagger /daggerdbl /paragraph /club /diamond /heart /spade /arrowleft 160 /space /minus /periodcentered /multiply /asteriskmath /divide /diamondmath /plusminus /minusplus /circleplus /circleminus 173 /circlemultiply /circledivide /circledot /circlecopyrt /openbullet /bullet /equivasymptotic /equivalence /reflexsubset /reflexsuperset /lessequal /greaterequal /precedesequal /followsequal /similar /approxequal /propersubset /propersuperset /lessmuch /greatermuch /precedes /follows /arrowleft /spade ] /Type /Encoding >> +endobj +802 0 obj +<< /Ascent 777 /CapHeight 777 /CharSet () /Descent -958 /Flags 4 /FontBBox [ -29 -958 1146 777 ] /FontFile 854 0 R /FontName /LYTCQL+CMSY9 /ItalicAngle -14 /StemV 43 /Type /FontDescriptor >> +endobj +803 0 obj +[ 799 285 799 513 799 513 799 799 799 799 799 799 799 1027 513 513 799 799 799 799 799 799 799 799 799 799 799 799 1027 1027 799 799 1027 1027 513 513 1027 1027 1027 799 1027 1027 628 628 1027 1027 1027 799 279 1027 685 685 913 913 0 0 570 570 685 513 742 742 799 799 628 821 673 542 793 542 736 610 871 562 696 782 707 1229 842 816 716 839 873 622 563 642 632 1017 732 684 742 685 685 685 685 685 628 628 456 456 456 456 513 513 399 399 285 513 513 628 513 285 856 770 856 428 685 685 799 799 456 456 456 628 799 799 799 799 ] +endobj +804 0 obj +/WQSQWU+CMBX9 +endobj +805 0 obj +<< /Differences [ 0 /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /exclam /quotedblright /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /exclamdown /equal /questiondown /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /quotedblleft /bracketright /circumflex /dotaccent /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /endash /emdash /hungarumlaut /tilde /dieresis /suppress 160 /space /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /sfthyphen /nbspace /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /dieresis ] /Type /Encoding >> +endobj +806 0 obj +<< /Ascent 750 /CapHeight 750 /CharSet () /Descent -250 /Flags 4 /FontBBox [ -58 -250 1195 750 ] /FontFile 855 0 R /FontName /WQSQWU+CMBX9 /ItalicAngle 0 /StemV 117 /Type /FontDescriptor >> +endobj +807 0 obj +[ 710 986 920 827 788 924 854 920 854 920 854 690 657 657 986 986 328 361 591 591 591 591 591 892 525 616 854 920 591 1070 1202 920 328 360 617 986 591 986 920 328 460 460 591 920 328 394 328 591 591 591 591 591 591 591 591 591 591 591 328 328 360 920 558 558 920 892 840 854 906 776 743 929 924 446 610 925 710 1121 924 888 808 888 886 657 823 908 892 1221 892 892 723 328 617 328 591 328 328 575 657 525 657 542 361 591 657 328 361 624 328 986 657 591 657 624 488 466 460 657 624 854 624 624 525 591 1183 591 591 591 ] +endobj +808 0 obj +/SUXZQL+CMBX6 +endobj +809 0 obj +<< /Differences [ 0 /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /exclam /quotedblright /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /exclamdown /equal /questiondown /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /quotedblleft /bracketright /circumflex /dotaccent /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /endash /emdash /hungarumlaut /tilde /dieresis /suppress 160 /space /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /sfthyphen /nbspace /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /dieresis ] /Type /Encoding >> +endobj +810 0 obj +<< /Ascent 753 /CapHeight 753 /CharSet () /Descent -250 /Flags 4 /FontBBox [ -49 -250 1367 753 ] /FontFile 856 0 R /FontName /SUXZQL+CMBX6 /ItalicAngle 0 /StemV 130 /Type /FontDescriptor >> +endobj +811 0 obj +[ 824 1143 1068 953 918 1064 993 1068 993 1068 993 824 787 787 1180 1180 393 431 693 693 693 693 693 1028 618 707 993 1068 693 1236 1386 1068 393 429 710 1143 693 1143 1068 393 543 543 693 1068 393 468 393 693 693 693 693 693 693 693 693 693 693 693 393 393 429 1068 656 656 1068 1028 973 993 1048 899 862 1077 1064 506 711 1066 824 1289 1064 1032 936 1032 1019 768 957 1046 1028 1403 1028 1028 843 395 710 395 693 393 393 674 768 618 768 634 431 693 768 393 431 731 393 1143 768 693 768 731 571 551 543 768 731 993 731 731 618 693 1387 693 693 693 ] +endobj +812 0 obj +/EEICHW+CMR12 +endobj +813 0 obj +<< /Differences [ 0 /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /exclam /quotedblright /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /exclamdown /equal /questiondown /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /quotedblleft /bracketright /circumflex /dotaccent /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /endash /emdash /hungarumlaut /tilde /dieresis /suppress 160 /space /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /sfthyphen /nbspace /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /dieresis ] /Type /Encoding >> +endobj +814 0 obj +<< /Ascent 750 /CapHeight 750 /CharSet () /Descent -251 /Flags 4 /FontBBox [ -34 -251 988 750 ] /FontFile 857 0 R /FontName /EEICHW+CMR12 /ItalicAngle 0 /StemV 65 /Type /FontDescriptor >> +endobj +815 0 obj +[ 611 815 761 679 652 734 707 761 707 761 707 571 543 543 815 815 271 299 489 489 489 489 489 734 435 489 707 761 489 883 992 761 271 271 489 815 489 815 761 271 380 380 489 761 271 326 271 489 489 489 489 489 489 489 489 489 489 489 271 271 271 761 462 462 761 734 693 707 747 666 638 768 734 353 503 761 611 897 734 761 666 761 720 543 707 734 734 1006 734 734 598 271 489 271 489 271 271 489 543 435 543 435 299 489 543 271 299 516 271 815 543 489 543 516 380 386 380 543 516 707 516 516 435 489 979 489 489 489 ] +endobj +816 0 obj +/HNIPYH+CMR8 +endobj +817 0 obj +<< /Differences [ 0 /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /exclam /quotedblright /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /exclamdown /equal /questiondown /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /quotedblleft /bracketright /circumflex /dotaccent /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /endash /emdash /hungarumlaut /tilde /dieresis /suppress 160 /space /Gamma /Delta /Theta /Lambda /Xi /Pi /Sigma /Upsilon /Phi /Psi /sfthyphen /nbspace /Omega /ff /fi /fl /ffi /ffl /dotlessi /dotlessj /grave /acute /caron /breve /macron /ring /cedilla /germandbls /ae /oe /oslash /AE /OE /Oslash /suppress /dieresis ] /Type /Encoding >> +endobj +818 0 obj +<< /Ascent 750 /CapHeight 750 /CharSet () /Descent -250 /Flags 4 /FontBBox [ -36 -250 1070 750 ] /FontFile 858 0 R /FontName /HNIPYH+CMR8 /ItalicAngle 0 /StemV 76 /Type /FontDescriptor >> +endobj +819 0 obj +[ 663 885 826 736 708 795 767 826 767 826 767 619 590 590 885 885 295 324 531 531 531 531 531 795 472 531 767 826 531 958 1076 826 295 295 531 885 531 885 826 295 413 413 531 826 295 354 295 531 531 531 531 531 531 531 531 531 531 531 295 295 295 826 501 501 826 795 752 767 811 722 693 833 795 382 545 825 663 972 795 826 722 826 781 590 767 795 795 1090 795 795 649 295 531 295 531 295 295 531 590 472 590 472 324 531 590 295 324 560 295 885 590 531 590 560 414 419 413 590 560 767 560 560 472 531 1062 531 531 531 ] +endobj +820 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +821 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +822 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +823 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +824 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +825 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +826 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +827 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +828 0 obj +<< /Differences [ 0 /.notdef /dotaccent /fi /fl /fraction /hungarumlaut /Lslash /lslash /ogonek /ring /.notdef /breve /minus /.notdef /Zcaron /zcaron /caron /dotlessi /dotlessj /ff /ffi /ffl /notequal /infinity /lessequal /greaterequal /partialdiff /summation /product /pi /grave /quotesingle /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one /two /three /four /five /six /seven /eight /nine /colon /semicolon /less /equal /greater /question /at /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c /d /e /f /g /h /i /j /k /l /m /n /o /p /q /r /s /t /u /v /w /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /Euro /integral /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl /circumflex /perthousand /Scaron /guilsinglleft /OE /Omega /radical /approxequal /.notdef /.notdef /.notdef /quotedblleft /quotedblright /bullet /endash /emdash /tilde /trademark /scaron /guilsinglright /oe /Delta /lozenge /Ydieresis /.notdef /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis ] /Type /Encoding >> +endobj +829 0 obj +[ 0 332 500 500 166 332 555 221 332 332 0 332 583 0 610 500 332 277 0 0 0 0 0 0 0 0 0 0 0 0 332 190 277 277 354 555 555 888 666 221 332 332 388 583 277 332 277 277 555 555 555 555 555 555 555 555 555 555 277 277 583 583 583 555 1014 666 666 721 721 666 610 777 721 277 500 666 555 832 721 777 666 777 721 666 610 721 666 943 666 666 610 277 277 277 468 555 221 555 555 500 555 555 277 555 555 221 221 500 221 832 555 555 555 555 332 500 277 555 500 721 500 500 500 333 259 333 583 0 0 0 221 555 332 1000 555 555 332 1000 666 332 1000 0 0 0 0 0 0 332 332 350 555 1000 332 1000 500 332 943 0 0 666 0 332 555 555 555 555 259 555 332 737 369 555 583 332 737 332 399 583 332 332 332 555 536 277 332 332 364 555 833 833 833 610 666 666 666 666 666 666 1000 721 666 666 666 666 277 277 277 277 721 721 777 777 777 777 777 583 777 721 721 721 721 666 666 610 555 555 555 555 555 555 888 500 555 555 555 555 277 277 277 277 555 555 555 555 555 555 555 583 610 555 555 555 555 500 555 500 ] +endobj +830 0 obj +<< /Filter /FlateDecode /Length1 895 /Length2 53767 /Length3 0 /Length 54253 >> +stream +xڌ·st&_·5ΫN:xbΫΆm;Ol«cΫΆmul'tlΫι˜χχΎοwΞwΟΉάQcΜQ s­½ηͺU›œXI•^ΨΜΑ(α`οJΟΜΐĐ³²±r°΅²w³S0303pΑqΜ¬L]&@ +{8Ζ₯*ΫŒJ +*Ϊr΄ƒa4OŠ΄½Ήΰ0Νά‹νtv±r°Pύ»45ΐhlζ`oλυ’‚ƒ«•)@υOI7Ο–btv΅²LΌJ–VΆVŽŽ)€’ƒ­-œ’#Ππ―N7{3 3@ θlηp0˜vπ°²·H8UsWcgΰ?εLφ.@8I%9€•$Πθll Pr3±΅2ύ―°5ΐΓΚΥςŸ*(τ4:Ίώk½ΖφfE‰Ρώoίf0ΐ‰ώ³W Ω¨ +J88[ΙΆtuuδadόW5σΉ\ΜμzΤpͺT°01±ύ ΩώμFŽ#ηΏ‘λίΘύ/dfϊ72Σύ,[9 7[ΫOεhχ_¨ύ2ν¬l½ώεj­,,]T"6;fε"aε 4S²r5ύG-c[ΰόΖΘ)loa 0ύΗ₯ώ―ύΣ¨δΰbυo9ιΉΉώWHΝΚΤΖθβ`ϋ ψβ³£Έ½©ƒΩΏζͺκϊΟ8ŒΝώΫρο°’±•½«š—γ·ύΧώm3_[ήΨΥΩΚ ΛΔΐΔΔόOβ?Χέι―vͺΞ6@M+³Fϊͺ("βΰ π‘gfβbΠ³pp8˜9™\ά¬?'ίΤΝΩhοϊοΰŸ½ό—mnυ.@ 'ΠnmΩΑ”7Δ:#;Σ΅;tZL«ƒRœΥc€{ά:Βι̊F+ΨΔ`™’Dξ―%}ΌHήn)…»χ›¬οfg¦Ψ‰lB(1uύΎ)g₯; †5OP©ξΥι¨"‘¨έΓςKθzδιZƒΙˆΎ)¬‰σYΉλ7d?Ÿ₯‘\ίέ]ψέN{9@(!]6Y«¬ϋjx›²I+o¬Τ―Δ6”mHPέΛ₯Z r +–3δ]xIρdtH‰π†LψΊΠr<γʞαF‚ΐ™ƒωΠ}k){UΰΖ€^₯ΐψ£Γ ς% -_έO…°TήΘη '”z"_I +₯£ΰ—m*ηqΠl{?LΑβjνχ $AΜβ‘"ύU'·‹-b‰†yΡ,—?hpOw΅_z#6qwDΚ{3>₯Sΐtp°³§"^φ΄Β1¬λύέOŸ²ΏΏΈ4飜:aŒr‹—2‰:Κ™N6ΫAAAΎΖ έ…Μ7 +Λγ1υ½ŸβΡu[ϊΎEL3ξh«‰N₯ύΐŸ4±_tbώκ—~~δͺŸ +ΎΊ;ΉΤ~ρ†O—κ»ͺQ-?cP hν› λΧ~ωη*nώ²>Τ―uuΛgΞϋp‘_D[ˆΐoŠ“ΤΠΏ9ΘA4fΤ‚l”΅όFi¦ j>ήσυα”ΗΨΨ.w1isc·h’°—!χhΆ;1ΘΤvŠΏβ ΪλLE¬_?΅ ±ΫΉκt‘1•ZΙe²ϋύƒH 4l8―ΨQKdoΧŠ!ώΎP6ޟί;iQCŽ―XΦwFdLυ O΅K°“±[9πΧ&ΌϊίoΨ‹Ξ[EΒdxΘώΫ΅˜LθΛ]Φ(_Y ’ΉYΞΎΜΟ=ސ§’IΪΙ“ΈLίp’˜κφC φΣιΪ‚«νrβ τ+“s(οσΆμA[Ԑ²iKˆƒyΔ—>Έ-ΎZy‡3νιΑ–“€ΙβοάS₯ ˆΣ€ϋκ n3pƒΕλŸxν‰ΑΥr±+|5GםŸhψ +λγDlλ­Y_—φ§5Έ=‹.xέ*!¦i Pe[ΪΉEEߘ2¬sθ>WνΟΠΉ“rh”Ευ$O€Xμuςxφr ΪΩ_'Ώ œϊ>J Œ=†€ ¦ΓΕYf˜ ψ*eb³qΉ–Ν˜•4Nψθ}”:γsχ™–ualύGPό&N—»'Χhœπ_e—Ά_Ί½ΐ;§•Υψδ\졨LΏ9!ϋZJާwŠŽR„7κfίxΣiq¦”«Ή +Ζ(}ϊΚƐz‰B°μg!`»Ρλΰ]#‚ΪΨνϊ¬†°K›ΜΠ°xv¦³α“ΔΰΧΥ¨YOBα“‚ΗL(gcΠM” +”0;1Φ§bXŸ›iχQM=Μzoϊη£™°±υΡ'ΨηpͺQR_A¦‚Έ? SΧε8²ΑΜ’_½ί|:Στy4uV†kJcTnœ3©Ν6*ΊΧ^Ή1Yvq#¦τγΉm@AΦ@¦ψ3δχM=pϊ°ιΑ²Ύ#ΕΊ(Hζ#£2 „Ÿ™οE‚>oΘ"½~™Žο°ΰ}q.†k>Ρ£ΰΩκpπŸ±ξξ%JΣ =i¬?ƒ ΫΉw·_{l,Mρqψ1’„έ°ΔT[?3΅žφ1σqܻѝ\}!1ΕΟ’DΘφ&©;ΉεάΨΕ ™cg&.[4Σ7Ξώ$*Œ+§Ύ΅•Β$gw4ζζuα'? wš”TšwaŸ(έζ:—ƝNj+ *Z(MD·4δ‘iιϋ™Φς-τ@ 7m€[’Κ‰zζΔOρΖ‚{ŸπγkφDλΜM)Ζ·ϋ"υ)¦…Κ’Δ[|ΐˆΛ+Ȉ0z/¨ͺΨβ»β ·Vd2>žž¨fηΕkh>˜ŒTH”ρπ( ΕbΜ‡ρ!¦0;K1aV‹°τΦ‚~hΗΚ—vyfΑWΡbΖτ•εΫw―J +ΝF"]dFφνΕ(΄‰T™΅ΉΩΕDk]ΛXμg|R«Έ‰r’λ Z%ά“Ύ΅·€Ύ!ίƒe€e Σ:Όj44€’0Q§°H-Ϋ°ΜQ²?ΓχpωΡύΩμ†ΐ~Τ+-~ώΝ­ΤBtΟkvΏΘMνpK™ζ©Q7ΙΊΠƒΤ‘“-”ωDί;ΰΩάTZΨΚ'ΆΎuϋήaΦΒ’OχξD'‘m1υΣ7›ρjƎ0Jεΰν†ΐ0υ#fΛΫΆΓ‹Ω†±ξWτ·wΑαή…ζYΡ†%­Ο€er}οZ&»ΧίΘ†, /ŸΘ·δϊ©βωR‰υΛ4],Ρ cΑΌ -Χ/γD.!™8’kδM?±Eρ`‰/j·R0Ѝ7Θ|Ο΅0 cΏΏR7—SΑΙηΛέ‡Β>”*{ίΎΛΊB©U!j’<Θ΅/‰€ξΌΕλkF•ΚWQ‘…PwšΉ{h'DHaEι/Ϋ`σΝΩΣ™•ŸA\ +΅eΒ¬?n6S§±Ϊ$lV^q }„τϋQY΅eω‡‡J (ΔΙJΤΞn0΄ό„`,²μXEδ…!lU7δ2a  *Μ©Ζ›FοGD1U)gνζS9AΣ–Οχο₯œ=2bŸΙκρͺ[~SΗ}$(·Θ.†·žB0ΒΟ΄F^Z3Bζ―J¦ͺŠ`(„OΜ”{μg^8PB˜Ο ΏλΰΒεBθ)ϋςG^½$Ό›3•όένڟ%λφν„“ώnΪΆεΛ5u:Sο!Άu•₯«š‡Veή1ш{i.:h‘Ά`‘υ© £+Ϊ]€΄BŠ+΅šιV`ΏMiz‹Δ Ψοζ_λΓΛ/­‡Yυ5―εNKdΑοΗ½γ ή…ŸŠξ’ρΨμ©`•†ˆ–|―U£]m`#’έ¦lPRθ³ΈΆξ₯Lkne^$R?-™yHιζ_Ήν$Q₯KΑ‡zΌΞΧqΪx¨dΒαάƒ\ΑάV&βϋŸ%?5€α@αAΘΦ€ΡωΜEΣυd—ελ*κΕQ"Γξ„·ž +Šή/΅“8ΞάT?ͺ8“Η³Ψ*k?Fk½·€ŒˆŠ’Oθ’§^γ’Μ??Γμ[‘j9ψD*¨ !w™Cρϋeu}“T<ωΦ’-°χgG]Ϋ~v~aγΚxα‚›Ε,GDI=ΊΣ†ž¬εr*ΰσιςؚƒ`u‚oΛ^*bΠ+ΈbLšcΤbX"LV/ϋEν0ΝΒ₯>U½ ³Φ£ͺ]š*φΝξσζΠ·=»‘ { vθ”geˆζ‚ύ»σ[δ‘Άs2?vQ‡*‘ώ78ωΔ’™―-ď‘+ =nϝ€*xŽέΰ v‹VC½]¬ζo1‚Ξƒ%ԜŸ>>fίR@π-Έ¦‡³ΡKΙ˜nœt,υΒm&γ­ណK£Ήτ"yV°>q‰δ©δ$ζDTω Σ55Ν/ξΈΏΨ0Οι}ƒ? ­ϊ‘ύ b½rνγΝx6λ—?l(‡…1•lΥΘ€BD J„&]Ml΄2«πSαX‡ jΡΞf›ϋ›h9»»_U΄δΜφXK™άiB!ογ£H³LƒξΓ±ΐW$ΩΌ£`Ύ—Ιζ- !L |x#‚υm’Ο"π λψκοτΦΊ©‡ +>lhC-ΒΡ`Λ$’ΝΜz~’Ά,κ#i<Ξ]’4“]§7Λ£\«˜aŒš{!BνWΏ|ο쟐ӜN²Ϋ8ς/=ΝGΡT P¬Feφ ¨eΈ¦rίx N„ψ>oiŽ…» ‘[}‘«Μ‘Ϊρn_Z#Η‘›dIΒ/ϊι&εGι˜κΦΝgΥΝ웉ӗsώm©ΥΤΥ36Ž.0ίέΒυPβΟ‘ΜuΠ›6[ίQ‹?C‚|W™άΚ«q³Ρ‰σRžX”ΐΰŽnK³.a…AΫ£ OiλΔPj‡°‘˜OMOP@…Ί z½-V•s›δtyΈ 7eηήΓPK°—η]sϋ·Μ“Τ¨λθ!.1ΪfE]žκrMΜDAλ6ΡΎšOΊ.λ θΉΞΒ’φn½)LfΡ<γTΟΟΟDΊX1!u13ΐEκ{'ΩRjβbaˆ‘»41›0PL΄ώ… š‹εLιœhΝ½ηξͺΗ+Ψ΅¬·UǏŽιLdd$¬<τhOΖν1αt8ι) σσΛ} „όPO %n²'ΚΊάΜLΣ½φx»4Z-D–‡Μ1J°؎žοτ ­ΧͺΟαV}ž”R¨/φ­_ωΡ)GpEuν_EΙ£)O~βJΐo„ιΖ-ςdύ1?Ky4œ‘}X }"O0τ*؏˜R +9ΆO¬³΄‰Έ½ οΫ6ˆ-‰~v8>πX}·9P+Η_zmwάH9ΑC/;,7L ‡£A=C±A:Ap}aŒα›ΚΪ³¨7ΪqrεΨΕΟZ¨ ؚκƒ]B>6ΞC‚j9Ž,͈JdηΆΝe΄>yΘ°\˜«=EŸQͺ!ΨΟφΪ~½E§Hžυ]‚θfŒ7!Ί5»;!Ψκ”ŽF«¬ ;ΎΫψx'IjγοσήψΑ4)*θͺM΅ˆσnψ!tdIUΞUΙLn݊χ0QΚάςQUώoe&βkΆSv\Υπχζ7O>@ΜnςΈG1…$V‚"ΕΰσΑ”˜^&hy,l‹―χ›χ9LγM˜xk~‡p£|ρ4θlE’(…HΓ_~5Ξ;_“=#πΩ[–«‰)ο£°~ϊ2‡νΙ9€§/6aΩ‚Φ‘ϋ&̈*„(ψ=ΌΩMGώ»šPⳓΪ`ΉΞΛΜ(φΙιΞ}ύΐ§5φ+X6ŒQ§,βΗ2ΣI]vΝH‘Ι΅ΪΑžπm7$ i]±iΪ;šg_*‡Μ*Κ9Ϋ"M’‘c%yhΘ]Š^UήΚ¨W*YeΛβ΅Ώ•ΊΧΦ'‘θΩ!RΧ’ |―ΦΟ™R₯—xO˜ί$βοπΉ$Γ9fϊν΅΅ΑŸk7φϋ™ί† Aσ΅„6c!Θe!FΕ]¦Α―'jt]‹}όv]HfŽψτa+KΏ&’)(;V1ήuϊΚ6ς”κ–ΔέZαaK¦η1KTζζ‚ή=R·n1¦ άFΟN0/ϊ.8(΅Α"ΕCT―‡+D2μ$·°[…Ζ›_KΤ"η±Z‘ŽGΐ)šψ μΨ C€IwΫ±™iΘΩκFθv#MΥγ=cΚ +s{?y\Υ uΆΜΩDυ’πV J€•:£^QsjsξΟΤόΫK£Σ„πβݚױUαzζf]Ήυ_θNγΏqŸcz ²ΎΩφ­Θ”_΄»~‘›γνˆΡpdz£F8ώΰ(>ŒC~%e<»ŠςƒΗl€|#}9[T΅φŒΌR–³u‹ζHΙG-ΪΌ‹΅(ά€©Lf}ΌΗU}Ώrd\Aˆ™gp> …\+EΝ¬„ξp₯ΥC†{ΓλŒ;­šΩ‚|κUhS-5(π½¬~―£Ρ%άσΒΑΦξZ>‡ˆHϊ ·ΑKΔ7ΡeI·ωG£ ΎθCwŒ˜JΙ²:Ω°η5ΖY³Η$xŽY]_‘φΨϋηY’^@šo=(DΩtφπ] ρ;1~΄έϊŸ?‚:Γη^Cf₯#sψ»)§ΗeŽ₯­i»έ-1œΪ2nZ©ŒηŒΦ… ΊŸΫXm˜.uϊ·V?£«η3wζ"€όΕ§·ΘθklŒ.V–ΰΝπžτύ.·K0–s “Ο[Vύ°“$τ‡MB ·&’ώmwuΉ‰;ύΝ&π ₯^IC@cΕ±πŸ'·ιSήχZ†Oώ‹ΤVΌ£ˆzίw«=κΦΈg² WΤ€~]kΧΏ'Θ)u—br +ζ–LμIΘγ=| ρ2}"2ΈΛyWEY«Βε +Ξbœo*8LgU‚΄»VΗv‰μxu“0jΌIP‘“v¬c±C…œ¬XΩ?‹BΌ ΠH ŠΪnjΕΌ<πύ‘*Άͺ¨‘Ίχjzτ€ρΊG{τΧύq«lˆ½Sφό1·”j΄GLυ5θPA9%΅“ͺkΎέlΖω‡$£ŸκΊο|&†IΜzpHHΝ¨ΑI†‰ΎŒ~ί¬z-μ±Mtiζ{™RΑ΅1κ―}ƒεΆQσ%Λp˜fdΨΩΖIRP²2’oωοΫ:œš^‚-ψ… +ŠΓ8—°‹Fvφ?ΐŽW>i€l«F@Nνί©—^t˜Ÿα!06KΨO.KγԜλΥΧ/MTΙS]ςJΟΨΆζvtγς„wΠΩζΎakΏLΈƒςŠvΆΒ€fΈyοAσϋ{ΤQλ +•ͺ˜cσ3<ύΥIk^Φ‘m·ε4ωζfLεHqyl©?W6β{;[~šΖ^΅:*H=—}ω#'γ:Υέτš.#)Vτ#Τo ΟI.η/δ™ύ6 k‹κ‘΅Ž€&ϊ?ΛΏPζ–…ϋŠW&€1Ϊm  Κ©W?ti:HC¬ΖY†˜αyγΈuΌI±6 +7‘O;J隬tHE@υ#b՟G:ηŸΨ +«xg«Ό\‘ +ŽG§ρ7τΛ=Ζ­*6SΒN«[πΩd“Κ%V£vTBΎuΕ]bO₯„q^€9πd,xκΠθIΉΦ1“MNϋ+€δUyώ2Ήέ_λ»FΪ ,Zο°ν fvgα',’μ${!g~άΕίω†ι”3I™pٝΏ €@ά{QV¨‘J0_³vV³\Όh'βQφU_)UjΆ7ˆ]¬nŠΩβσ¨)`θCbϊ;„χ"@¨ΠΤ*‹vECέ’DοΕ1ŠΩ£ζJύ6g«ΫAύ­wο{‡°Γ°a…ΡpΪΒ MΧ~ž°J4χΏ ;ΞTψ^£ω/6ΠΪ™ Ÿ΄No‡8X€AΓΎβ«ηšβΠP¬°ˆ\3ψΥώβtΜNYž’vί(Σςn¬όΖgβσΪόέ.!σνΘ‘V©Ρ‘ΉGΘΖ[VτŠ΄\€/ψυΧŒΰ™‰8ψ°@Ε§"ΏqAΡΎΕξΘ>Μ ΝάΉζk΄5Y ύΐΙ—ωZNZƏώΕtά-Π£οsύ»­ŽVΩ@MBΘο\iθΙ…uοωΥΠ†xQΫΈΥhUhηŠ-0°£Bδ;Ύ„ +šU©hα&sς—©αψϊ› QτHΥ―Q΅χk­•γΫ₯Μ»!k5r™Λ_ε½”P:G„hΪ‹¬Σ«U «Ζ»φΩ8Ž3Ρ™0}Φ+’M: ΣΎρ""tΣ£P;Τ…:˜1+…θΎxΠτtrMyT.Fj)[yψά‹χ_Dywe!z­"νγ¬ϋ–σͺQμεΐKΛ‘ψΔΎΩ£°Κ‘/'›<―΄[ Ι„Θ&b;Η_Ψͺΰulƒ[9όυn€Π$ΨS£Ώχb†ΩBNiΧΠοε's—5πωw”Ϊ BJδ3>έγ*=J8vΨΩΞΡΠ…Ο‘λ^wμT€>^‘oω|œ/δ‹ /“O&72­FΔ~ΡΦΛŠžαHμϊζ_Z£W Ή³ξχi¬… ‘“ƒz‹―|λqε8p4½v.Κ%xΔoˆΛΙ·—䃣3ΔΌšf6²+R«νY.­¬ ±ί/£4/=|.%ΉάjΊΙ™ύ©¦/BΖκΑXλ«ζχe’Γ'ϋXχpې?ψάλšQ₯ χ”Ο#w Έ¨Η NŠόΦOΝΪ$»½ŒP֘βˆβŸn9ΤέR·eΔC ͺπαP£,*1˜υ±6mΏν‰v֞*«ΐN­αS{ ν +bκ‘ϋsS‘Š«›;ZiFyT *,ώƒQG©Ν + +ƒ3‹‹%›φγV΅#κ$žY‡Ž7«2ŸΘ­²y’_ ƒ X…: ]6η0νrΕrρx»½Ÿ}Ι9ομ'kqΊ!Ι…v…κ…Φω’©ΣΡ= +G5ώ1 ³Ρτϋύ†Β¬„₯yFχ:ƒ;œΜΎ˜6/€£εQnޜ:‡ΦΡ­Kΰχ©)w+qˆΈΗάέKjΜΉΥ^nU,υ1eGeP- ψΖ98CJΓΧ †τˆ…*₯μ ~—πUπΤg°eΙ£[dϋωήΡ57”cc«ζΒυş›Ζ@’»ΧXέgJΥ/hήΓΏF»Ψmώ|kK EƒQΓοmήϋΤ¬>k’ΧWΑ#αL ’2ΛZ’Ϋ7p»yρξσυΎΊ…ςQ“+\fϋKb4j™E{• +cHζιΧ¨χχπψ(«½”zΗ2‚΅υQ€?:ϊΏLd^Tξ­Κμ“fSγΫΰWΆŒϋN'–mηΦ’—l*¨£nNT}SΑiδ£°ΝiZ a‘ΓΖjDΗIbt#S'ρHΈ.ŜφPlσl_Vά%L“#F…’6Ω Σ½΄§Έός°I53•ŠIΔAση›Vl²‰ΰŸ-*C²l°`•‡»φ"d…•ΨMŸv/Έ±¨Œn=I:FͺΓθ…έ‚}‰=Nk‚ŒΟΫίE=K{•›ΛkΠΫ}Ÿqωp*σŒƒΤ γ²|»΅HΈΗ_ά=n ©ŸΚ4GWV‰Ώ”½΅ΫώΒyU8ΥΫΓςV™²<ΟP°Ωΐ~XKW%ΚK³{ΔOtε‘_yŠεζgι)Ο ΔΥd„3◐·FbΈ`Νb_-T=!Ό‘–ΪͺπCΕ‡z˜IˆVuIe?Ψ_•»ψ΅ž Ι«jf2ΪΕD紎€ubID:Θ΅Ϋ»δHJoΝώ- ‹TЊμW"@fάΥΆv[ΒΘΌaBΫ™Œ€Ά–—πξƒ,+)^ύδλ€Ττ©".±ΊΜF±#Aš7Ζ`†Βζ=Κ!¨¨­*†ΝŽΪζێ(ξ^΄E¨•‰“UΞZ_M‰•ώc>φξωŒΤ <’|εφΖ7τ•ό΅rνdJ1υΥ.YgΉεcε+GΒy-zΝRE²έ_―α +Iα‹•vΫ|„ΔVe£”ψδΜHαΚuΣξtmΆ/ώZW~EŸ,B’˜GŠ‘ž§γν;ΞσCBEΊΔΥκˆνΟήf^’ s „ͺjiμ]φ)ΜΟ»Φxαœ“<3,ζGsˆΠFͺ? °pΚArvnωυώXL’’°²£+ς§ΟΦH-Κ‹‘ο›=Sπdΰ@2ڞΠs«ζkPΪΩΓf]Ÿ†ύω“…­κœ%>Jδσhfd {B Ί†pB»œψ>§ξ+’τͺςORΑΆS]ΑSΝ-κ•ΐ'† Η3r!7»λ©Vΰρ“έjδξ’AΡΪOšp‚…/(T:ο¨ι“«@•ΆŽδFf½\}, +Ρќΐ>3C§$@wρϊšΙ`ξEt$Eζyδ š87-K»mZύ&—ƒιϋ±φmσ,Ks.GΠI—ED>° A_ΏSσΚπΔέ0ς4žςΦ8M/π‘TkύκN=Qop45-αιι{{ΤΑ Ϋ"— Nΰd‡ςΐ|Xωέ X?e*¨Λ}.eΆΑP>ˆ„nΕFqŸŠψΉ¦FƒΩaΒγΉ?6Ήν¬!W.±Qǟ›ή~΅'βe0₯]?έΏ/ZΜ;X{€r‚H’с‰Υ„ήΟύΥαΆqٝ6₯8K. Ή›e±8SNκ䨡žΡfά‘’]“ΎJLΟ>3“Dφ}EΡ|3¨ktˆ +fΧρΓΪ!€ΝυMClϋΗ‘g₯γ5X‘1ψgJΪά „+ΪΰORγˆK€2Υο_Ž…­=Λ3Θ+>VΦKA“Β)tΗ•ωεSuξG‡­ίŸ|«αD”fœϋΑ_c)³›°―Π.ζ°*Xœš,RαzΊŸ,ohšΉ"+—˜Ιθ·I ŒiZͺxΌ₯B"‡F»Μτ|Ϊδ΄XΖj8²2½β­3 8ς{‰šz‡¬ ςϋZxBUΔΥ–Χ²₯‘² ΠG)Η…ΏcZy₯ΊΕ㦨Wλ¦ˆNΗz^f{`πί³wγiJ詚**XX±LCc]Μ ΏRΝΑ\ ΖŒnΟΏδϋ%Ή}έφm+5·–4gYίp¬R3`£yJEW㊀…λšjBvx˜ 5ΣYlα-ͺhΊΫΩκ_„ΣγΈ–g\@΄|θ!pv +r9•ΰXΆ’Φeμ]Τ.h6ΓΟJίu™zt_EΤΈΠ'Ίj²`ž—eKΕ½/i½¦λRSφœΡ³™Š ~FIΘ‹6l΅7_ͺϋYT(žΛΧΎΚmωχ/οΔ₯ΐ‡zn +MΟ¦Vž(όTǟ *ΟΛΏ„z~I;μhυ_„ΥΈ)?qΪL1@[ΈρWωlϊ?’woΑόU2ων*lb@€™ϊλΙ“φ £Μ@΄3Ž©ΝtHݜ˜Β—ΐkΚQ»qpξ}TΝ$P)α;Mέχ3h¦OΎ†‡ŠŽΟ€«NτθFu”œχouΊ]4Γ±C[»¦ ‘Cω +V%oΜdμ"ˆΕS0Ακj‹£ ؞F.ۈ:σΝυlΌdΈ’λ}‰gJ “θζBpm€α Κ~Πvj½ϊηjc¨¨π‹qΚύFν.Ή°»OQŠ-οL*ΓΥgqσηπ{zΡΐl#ΑΫ{¬δαΤƒάj€d΅•Ϋ)ƒΠ&­Ξ3δ'νΛ‘α[½ p‘ZŸήηRqŒΰΞβίleIΖτ·/yΑ„H)έJ b“#ί…ΚZ=ψέ€FQIrl•LŸμ!― ρWΣ5BR9oܞ–κνΞ²FΌΡA‹š ?“V«Υbλ°NμHž‡¨αΊΊρ5œ#»Lmrή|ήδίγΤ)μ¦Έ π½eΰ“£]IZΟΝλ| 8υ:%ξ·^ƒ―(ί–€²ϊd°©/‡XΏ Σi-°ƒEƒδL™ΛγI|t©r,?Σa‡^ψΦΨ:„9rGEΛ†κέ–€φέ6k«\$.αͺE\lχ Šοό±“ΈwΆ&‘έxAGS¬ΤŸζRΦ–Ut`KΙ}q rΏ<Ρϊ!W8/_°'©Ξ’ΆΠ°w‡5Sl ‚(#Κξν%³"™8r€α^Q’gΛΰIύž…„ΕεpκώIpŽŒ  >xŠγ6C!…SVΙ;ƒθ*μd¦ΏλΩ5ΖΨ-O—Y“=™O”RΌY;D’(>y›γ+θ’}‰¨.7?&VKςΤ‘aΕiX-ΰΌœ¦+δ :₯²ιηŁMαS€Ο8Ρ'z¦lg`Ρ7ϊ2ΟzcOθ‰ζ§Ξζ‘\υˆYF‹°`7ΨΉ˜―HŸˆŽŠzυύRΖjξ6J‘"yŠ•τ;²εs‘H‘'J‰Qf~lΕ‰Ν‹~€*7δ~dŒWLŒΌV•₯θΗυe?’L_ρ‹‹¨]ΖeEdfH|t…MlίςmX<‘φ.ώ¦EόƒΆ„i‡Ά-φŸΪΰθ‹΅²L\ΌIΎ½;KZ(ΕJq†όMέ™[ν`†4ΥYΐ@.Ζ8Ό‘6 ‹#gFS΄π•β\ς|—XρΎ>Βρ³©Α‹†™1\»07_σΗΪΓ–vyύ$ Λ—€΄&ΔφέU¨ς„½=+ηŸ„²f\³*$οbόμ)Κ0nYΧΕο`₯φΖΆ 'Ϋdšλγ)”ε9Ιož<“ή€ΚΣm"jδ‹όPΠY3V™­Η'Δa£ΙσΘT}!ό€™ψΒϊR’βCžΕ­μr±λίΊ›%Gvώƒ޳+\χ)PnšiΨpο¬Η\‘•.ΚibD™Π6†n‘ ϊq²›Ÿs‹/€ύBΥ‰1ετΩLRσƒ—£΄H} +FΎ™¬PΡθΑ'ωΓv1νs\Η–Α1Μ¦4eώRσΰΩNUΖ±€Rσ`Ν|Β~΄?R έoݝ&x―#ΊW=^‡_ƒ ώX^8$( +Pΰ[δ‡'"{ΠeΎαϊΘx΄ΎZ,±,‘πQk?K·0ΫλŠΧ©!2+Œ2Ά8@ω Ή!Α)οψΈe> ΑXZιρgˆΙθ|Ž˜ Τ žaΕυ:΅‰WώWn]ŽΊ\“ΌΉΣVX‰x€ ©€Ξ½Ϋ‘Σ࡟ƒυ~I`ί*σ…9κρΘ¬τt@šΨUnp= 3žqπm³β•I―όψΖνΕύ>.ŒΥίΖ¨,νΊ 28WŒ(ЍΰΈWベ5έ5ΰρι•]εOZύΌΚπ]œ³‘KίβsϊρVd57•ψŽ’((eιZά\Ψ έ0Bšƒrη ?Σν}ζ7\.JNc»Ng¬ΒΈ Άγΐ…žRΤϊ§xκΛξNOŽ:az2£Ž4³-ΰ‹ΞrJY(8Έπo Ω»3PžΒέ >R•kz~3Ρ升ώ1{že²οBα¬hWέϋDBkώšςSα;ΡΠ)(Υ–οLκοŽα%¦κΣρ!ώ@ΐu’ΈϋΩdΖoΨΦιcR‚ŸƒOυθΡί/Dίe¨›’S+Ijμ‚»u + a?½|z{ς £,aέΜ*ΔIoΦZ]7lΰ6θpψ’ƒΌwD ιS±klλτW9χŽπf?»Έΰω +jρ\'iΦ@¬z'} +ϊ­˜p'6‰JC"αLƒ]’q/zΙΦσg‡W{4 Ο C™»€trιg΅©ε΅$ΫL΅ΉΈ‘]€;ˆ££ώ£š―ί8‘b†–RŠ45κ βΘΓήAύ₯x86Ψ9Œ7Χψ·žK;–J*ηΝθaΦlΡ₯"‡0ΟΟ(nšσΰφΝi\T Θ\–ŸiΧ Edzw}Ηz„¦ƒΌ9€νΒj“ύυϊ +NΗ{°πϊUE/Ϋp\pRώ‘ξ\ΥD]”›έ¬θ$Μ;bj{=ΑCβRβύMpZνί’ΤχΘbΖΪR"3Ξ§Ψ3 <ΐΕΔ+g<ν•ώώi «oαίΔ +K5pτyžS$HαB.§Ω&ΏΟ°2’)¬έk θΛΒ°σ&Ή¨ΖN•x\±ƒtδxΨŽ Tš‘L]επ@PW«₯N£&φ0ƒΡY,Χu[σ¦J‰aΒΫί$Ÿ»δ4·ννΥ΄Ίζ’Ψ5ω¨}c·{TΉΆ΅β|ΪωT":Žl7H)§a‹Ο8ό1ΓX37Φ€—UΔxž_οQύιŸUδ>£ͺΡάΙMΩ‘eαƒΒζQΒΆ0¦ σχ(’Y’h χΪ’ϊΊπΘGΣυ%ήHε\=Kt―dΔ +₯LΘCPw5ΐ›΄Ή’ήΌpLΈŒR-΄QΖρ¦oω kίmΔ*eǟ΅,jΦκϋ·„PΣX¨šΎώ^—ώα)Ϋτ8fωp%’#°ς5•Υ#Γo?)Qt‚ζ…œ O†+δ…ύ>’CvφΞν΄Frφ֊¨"Άoϋ³4έ΄88nqρζΨsΥAΌΤ·UU/‰l§έ1`t|]4έΚΨΉΪzZ84Nƒ`φέ„TόΕ*![”ρkKΰωόŠ™“νƍN5€Ρ²ΗΟnΩNlσΫe˜Ω½i|gtλˆελέΝ\&ΰ·όF«τολ½ώ°ϋώ¬MP|Z±7ςΘUΙ0¦k]&€ά¨ŒΥΝbΓ±ΔΑγψJξ7>₯ί†R­ψ³{•,χ­šτw1ΧΫ«KŒ'h x«\υτ! κG?Ω>?·a0<ηΗ8ιŽn^h΄8›{W·†εΆ€-ΡrμΙk8τώη© Μν +Y¬υ^WΏδ»eρύ$eΈίΘ2]ε­,ξΖΙΏ9½wψdξ!ˆξVYuᨀ w(4ΥΎ΄ΊRΩyγ’ΥuŽ0Ξ.MQsψ*:½ξ+»NΈ22MR:wŠ=LΚΙΉΟθΞBiΈΕΥAΘv"υiΔλy«bϋ6‘eaδABUΨΝ\ƒ~ΩΝΗͺ‡ΉΔ +|ͺυŒφYΈΎgνY9-»k9η!ΆLXjƒ”Ά]wΪj…EUcΡL{3{ώ2Ι> ΜUs #ΪΒΞ•δEΌ>›Ž–l)΄>η„+1jn;ξά»wφήaΉ>τ5ͺ›[E₯yyh0Σ Ϋ~ΨFΚΓΊA§ˆbਜΖmΒ¨{oξ° +Φυ₯ŸΆ'_i˜j‡wŒ™Ε“».?Žγ†'ξΚ£κσλ–W₯Σf­ŽΊ;ο{—[.;ΨΘ€] qŒaC=0£xΝGiϋ0ώέ“ΠBp{ίΗ)›MS}πΛ Όή_WΩ›΅’ƒA΅ΒsyŽ”ζeA™τ™₯•£œMτ%τΆ•‘°Β)%+‰Ο7₯ σ }ρΏ™¬΄;gšΤ₯£%οάκ ZX΅lpπŠVα6ζ2ξ LjΤΤ²>އξ©Τ ) :ψΑW±4ΪΚχΚz @Ύέ¦π¬°Ξ—ΊΨώˆπυ»Œ’)˜† Lΰ!‘…δH`P(wڎ”κΑζ {U•αθ°Šή’Άα©«’’ŠβΒΩ)t6€kU E$‘»; ’qΜ‘2ȍΥΫΐ\ώϋΛ2–ΈΏ iέΊdwJqXήγ°qή΅¦M^ΏΤ9Ο ǘC£xΊtψΰπάN±L χ™υO}uΨι₯Ÿ sΫƒ$Fο /σ₯α΅ΗIε.ŒˆϊD·νwCλfΧ1ΡνO«}’/iε[Y‚``ΫΈΗζšοžϋ¬£`}›K9zTŒΚ_¨Ϋž@ΖΗJ(S4³bS τ5°2ΛΈdiΗBψ’[%pt ͺΈZ iT˜Α₯?ls΅UZΣي1€lrΆ4ωΚ!ς’:Γ8‚cfρ[Ν›>Œ°,ΏvVΗIΊς€\²άΓ²ξξ ƒΠHΕ`ƒ—αψ4PNF}€Τ•ΟW<|φ­γΡX&©:—uΓBΙRЎx„οΊ%Ε΅xΝm?­!ψ†ΰɚAŽU–υΟΏζΌ°ψL;Κΰβr―όƒ§ΊŸΈ~εcYΩ_E” €0½)ik73޳ˆΖ-Ίκƒ―ΒΘ:!+V‹άHZΓδˆvgδΟ8Aψf‘6zsk§ο- ORVœIΙIΌδ±_ » υυN|m™‘gO;»Σ\|žn€ΤZ£ΟΩΑ6T³.ŠΤΰ$¦ΞTKUZ΅:βYω¨ΰϋ%<Ο1ƒ*‚RΌœΜŸ +.‹ΝΝφ“hΧ΅:p/Π%1ž9Υ*δ«4!]Θάμ?DΡ,βσcΒV— «~λ)B,.‰εLμ=:=HO>“¦:Ρ†VXœορΑΞ$q?§jn’œCΙυΪVΦ;Љ>ξkόˆ$V²Χ^Ÿ4Wπ3Ύ<ΐAΧ vcε…6‘ΙΕίQ“h˜M/ΛσΐX™‘ζ¦­ζΩκηjΰO²u‰ + \ΌΦΗΒj2Hd€ύεuφ?Ο %lužR’+ή{ΣswΆgκ&Ξ‘ΊΘ+4ˆυύίRΆά%]§B]?h?Q*f“ΡΡΊΔ²-΄v 9€Y‰ίEŠΫ-υ+S·qΝvεJ0΄h`ˆδ’ΟΆ1ά7lEž5φPσχ¦€BvγΔ‡Λτ:·TΎσ½«…’ΊΉΥ†&“ψzš:Z1ΐB Έp§—ΐ˜ŸŽ +ud=ε;»d€ΙΕς‚©ε*ξcW›YTp‰POΪͺ£Ύ†σ!Α<”Ι±w7D¨ν¦“Π“μe W9Ύ~jp σ BΘo‰,~Ώ$³Ι‘ I {“Θ~{u$Yρ‰‘°€ƒ“Θ§*ΑωΕOMu–ϊP$˜υvϋb%qΏ0Γ«^37ΩYBε"daΤt‘‡*Όo3ή +L›ΓΑ½Š›΅­)“ΐ–Τ°³Ρt'Gόό½ΰΉΚB‚h9υί~W?ΘάΕ―nfSΚjl³HυI‰ΐL•ww/‹4žNΉƒƒ_₯"ŽήΘ^-x›€ta%ίv**ύŽ³₯*(`Ž?\Ζ«0Ώ)j ‘˜χZxfmoχUΈΠs‘*°!MŽήΊ²‘‘υδΑ&ΘB―Y²ό–cΠΣ»ι—Β’γΨ#F>a―3ΕΛ:ϋydΝƒ©·\@Ϊ6§ΩƒΆί±σόsΫ νs™zWΓfX>ωeσtΤΛͺDΣδ‘½ϋv­ΊΒ*UπZN,7’YB rΗ"ΔϋbsΫ‘-ΰO›ΊA8ηΓE8όΙ³’‰νeσ9BτˆΎ¨OlΝTf}D $„ρ —oώbΥτ¨£αΔ4ΐώΕ$)]Z ³½ΞΎμ£ΏΒ½†½4ο9\,aΗΆγφ#‹%²sτΓ3JΣ«,€E„+₯ΖE%Υ]Φ}›?Ϋ?‡ +³τ*`ΖΩ:ΩhCRμnΩ€=§SLͺJΫ –Φ„Όv’ρΫdΏ‰<ωωͺΆΈ|ΛΉ” ΪƒGd15ζY’i +Cθό7­σέIΣ€σT8Ώe£0£ή Cy‚v– NΔΛwš±Ž΅ώο~›£μ¬1ύCc§Ύ‹₯έΙύM‹>yκb*{[q₯Δ(GζdΥΆͺ«τvΘυ―<•ZΔκψE|T"χ³Γέn c‹ωjG=Α­Ϊ(δxHšžΔa²©λ˜P@O9>lJa^νΦΚ\~+v¨\QοEnύ/ΧΆq{±…ςRdRΐυ-ϋΝ:/Ύ‚WKN6£ +nΠjήI>-]8oͺΡiΤ½€œZœ‚B3Cw— %‘%±±AIšεε‰ +ϊA‹ωNAΉ±r₯$Δ~xFKWΔ“+Β§Θf΅SΘ}^ξ†Lfν6Υ—gα‘4² ›OzPTšqI˜ΈΔϋŽHΦΣJ„‘ΙšβNsϋρ‹"A>ΤψΛBΆhηΎ4%γ}Γ©qJΠα;υŒςμYΧrrΏΐφΟ«˜žFŸ»qΐ³owkƒμσοΑ0©hμVŠvΩ;}Zš\ˆΒΐy‹N:Ί‡‹wj„QšΫΆmΫ6žΆmΫΆ=mLχ΄mΫΆmΫΆχ݈½Ψσύƒͺ‹ΜΚ¬Κ(ϋ%Œs{ΎθTJsˆ4π΅–‹†Μwˆϋl>Vq© +ίο`’­wΕ°ΧΦBZγŽ7¦eσWH3Θgέ‚΄ΜκΕΫͺΎβL`b²Tγol‡qά$ΒΆ^#’ΌΦ__Ϋ=4LfRΰXœTίSΊ‚ΈΣ²λUFΞem™’ίvsή.:5sCH‘7DAŽB4λ‹kŽ\#dήhΥ㜑­C²㜩iσOfͺψyœ|g>Όvθͺν"·©r‚žqN{Nο$Ώ΅½N=+u0g³ͺ°Τ\b8y$ +hν­jΧ  +ι₯ev޳w&-ΞψΙ84εΧ+bϋsfgήΧ7γ―—­΄4«szMVmλfšΠp€ήdZ§Ιq|±w4š½ΣΆχ­R”χ4ρ·Ρ™ n,ϋo³φΆ@:»"B.™)’n ΡFΡΒίυ†γŽ~—ΰ·™LG”±ΜΕάμbO©,G[«hŒ3cŠlJ©πγςτ,LŒΡσώΪ_©=)γΟ€Ύ²ΧΧ`<”Όu„ΞpœσWI™^έ}μΰ*δ]]ͺŸ\"vΪE«¦λhι²gͺ­οϊ¨?#Ύ$©„ΠΣ θ{ΨX˟sΙF΅-›S¦x³†ξ$ΡΫ/ާ Δ`ΗZΌo\tΌ ,9uΊ—ώJ†H†»ψ₯²k·ƒ Gς%WH/ΚΝκ +ΊΎΪΟχq©.Δ(YNtξ!»zΊ+ΚΒ ‚t7ΗHΰ–Ϋ gR˜Ξ‰Qλkr/Μ¨­KjΛΏBP¬{ŠΊ™ +'ΥτΜ©JήΕp˜{L‰₯V€­Tώδ΄³D’UΈH0ϊ₯†ηOΖ\σζˆBδϋβr°-­ΓΣA4{½w½άI˜Ώ.„ 9ΛZE@v +ΆŸ39 W{—NόAΘeaΑG*ψYUΣ6³RάuΝ¨¨r{΄Έ4Η€‰/=dN¬=W)ΤauρІBg―Π.Β¦¬β?Ž‚¦Δ“)Δ΅ξ‘d…”©΅Εζ;‡8Vx’O!vŠ€ΘJD=νGϋΣ²δ ρΪφ·-cz·Gςƒψ˜Όs’©ξ-r₯ͺŸ‘™¬E„tλ”ΘF…$Ν~‚ΰ»2vά9Œ"υάΞy%ψ'JPή¦:3ŽlΜΝFΓf·/Ό6΄²ηYBΣ‘y^/NΥηΉjx¦Ρμ±—ν–½™'ΙH"£ψn¦F’υΙ›,ώ=4²^& ολ-3…ΜgεΙ> +“Ε*£ΣΆΉMP§fH§CtΰŠΔΧ GŒb­ˆέ>χK ¬,τcb–ξd―€œ–;§z–Z›„ω©Ύ6a1k€ΆκωΌ):1vW Ωoa !χ`PH©Ωrœ½šEPΝρ-I@$·ƒShΛΆ οώ‚`{"Τ¦!{ρ¨‹tq ΜΟ/wωȐ`SP΅°(ΘΪ΄ϊ»„½fυcR’ +οwa'ΠM05€Έ8-ο‹­•o₯³ξ™—Ι#[Ύ4g_±Δk‚eΕvσ¦§1Hγ(Œυ9$Ϋ:χζ'ηπ㭁J΄ .νχoψμK~ώt'ΊΩs‚Ή©XqΛ`—‘Ϋ†τ¦DM€ΚtελVζθ?osNξŠ]εwϊ$€—Ι£ρπžΗe7LgMλš¨ΕyηΈ#‡c~"•kΛ(Ψ * cφuΝY©=ΏBˆήήυΟƒnu΄b·Š{`Δ1'€–j:ξQ€YT½ϋD+ΩY}²"iߍΨϋ―PόxˆQ0ͺΞRTƒ»&|©ͺcδš +R|jxŽΨξ½zρΕρ}τ–č:˜JI–ζ†*β^eε1ωY:˜ΘCΛ‰k-«μt.£Φ>{™f΄o <²ž—ΧCΉ„θ${@QEς–y„)%‘Hˆσ‡Y ύΟKBΞ?'ΌVL79—MΈ΄5bο`q„N; ηs)σ»H8»5BڝLcPD»ϋ˜[34υbͺν?–phΊOΎ·ΠνHτ 5mZJ~Q Χοi±΅OΙLOΛ’“§κΏsj‹ΜePΐΧΦΓ›tωŸΐTΞφ©„©1―‰αu3¬°ΗŒ S–8­Ψˆ)Ν¬ρ™Ϊ‘XSMƒ!E“¬΄ΐξ7ŏUbŠΈšœ „{Γjω’Ιi4Ύ,~ƒιΔKΚμˆΒ‹iH¨κ(ŸO‡=U>r+Σ’UhΡΊ¦·ZbFœe4Mm’ΰŠˆ~ZT’{‰ˆΜΟ–₯„ωBKP΄₯Φ²Ή·^}έ/>Πl@ yΞΚ"8\{ώf (zœnιw]`žK]]ΏθH^˜eLB†X"₯ΓO7ωžK,ΦD{ΝFpΏ&8\z;ωΫμΑC°μΡκ$Žμ΄Xƒγ‡k•%ώN]=Π’:θJβ΄hɚώvg5κέ΅•,“Iύ ›NΊ€„φGΩQ°~^s¨(Ργk’ ‹1O5―šη°χ0 έΒSWΓ"Ό¦υa£ [xβi˜?™ΌHͺ7’SΑFŠŒ΄:žΘ¬Yv :E$PˆβK Š1₯x“Έ*ΪNξ2ΒφEg™έ—,*ΪΉUΥόζS( +ΟDŽu©ΚUgόψβN—αζ8ΑD@£¬©“μ#xςdυ"Dv’XsΙzw•γ$ΫA#[Aœ?½rN3σ/ψyUΰF}Α·h„Eka2Τ\”:[Jύ(Sξ4NŒ67ΘϊLR0·#8X,ΐΔ~ +Τ‹΄»‚ΚΑ©ΔD½‰!Άž‹.Ύ" a‘Ÿ:°|εώ-NψπΪΙAς¬Meι1„-cŒϊBΞ΄φΊyWΟFGœ‰ΟoΒΗhώ nΪ6dl]Η@―ˆβ}?!‘•jœΞQ·_DQRkνP65Ψ₯ΏžVΧ­Ξ1cj'θΉ=«ί kϊƒiL SΌ"5QΖ@?b«(ΨC‚ρ…†©Maέξ1sκΩYΝS;@obœ~—ŒσΞ&Ήεœ_uFΓ΅ˆι•„§ς„ΖίψΞt<&mΜέΆ–’JqΉqŠ, °|9βIšd μ'ŽΐVaφΑ3³ΪΚ¬φτ Պ„—;εEC Žwyz*Ώ0Ÿ°|")Ί@’ίhκό${Σ‚„RB‡­υ%N―Uι_υ‚!ι…?‘ς”'΅z¦»!C}sΤdRL»ΐށ*‚―«Ϊϋ\3ω©μ_υΡgRΞ¨sZZfΞ +~zάψ’/NΈ₯Oic–˜‘=paϋ@_[σλŠUλׁωΨǍ%;…¬Tpg`ΏRwŠ‘νϊ`TxsXρ² ˆΰ‘ž‘ΐ ¨&„ωσCΊ ΝΏ!ρ€ Jvwχ1/kVL¨-η'aYŒrΊΥ`Λ §Dλγτl&0ƒ{σί―8ΨχʞΡρYα–\2\Δ>έHRπα4βŒJά+lqΡίι(DόW>Rέͺˆ9iŒˆΒΆ±–s·―άΠχρ +-οWŠΝ‡%έPκϊιΘ$$Ξ!ρCR…κ%»L\ΩKvCcnΙ…B’κ﯁ +1…Ž|αYwεo‡ό +]Ο‘Θ©ψε}ψόΨDΦΧΧΦ`δόΊkAGo +7%ΈžΕθΌ ͺ·ιK\ι“œωuY‘…ηΎφWM…‚“γ&nw—ΑD»šκΟζΪXα|x·Λ οΎΊώ_DΉ.F›ν³τΒ6xc#]{8ό―r ί\%N΅΅k7dŸVn{ϊψ7σ―±ζά›¨„5½!NΘanοw2–¬Οέ|†g5ΌT,jt–AE½ΦΜ\ς\l*6@­τ΄σgtT§n’ίΌ‡'ΤF’+“Ή$νot+=hKςuΗΧd­²Ιj\ΐ™#εΓΨζl»„%/²ίΉT)bΟW+tΚvς-\b˜X‘{Vκ– Ώθb +4Žά\fqΦ&‹Σ>‘W]3<NιΒφάωK=L§ψΜgγΘΒƒm(γφΨ=σ:όΙUEΪ•wiΈšρ©1uμ8οWl[ +ΤΏA`‘ΏŸO5ΐ>a‰Ίε Γ:>Ύ΄ρ|©΄Ξh1¬JΗ§†[«5K€έDζfΘi-κJΐŒ2l\ŠΗcΖ쬎WΓΙϋΝ¦"Ά`υ‚όΠΆ*jsΘ?B ε(»Uωζ 0Εd ρ‘3 [ΖθΓϊΦΓV€T›ΉrΚL™~`-[ΆΤτ,£ξΉR™‰oΑeλ=k†#'9ΏτE΅ 4:Qύ^΅1Χ~Ρ΄zBώzΞƒ[g/¨c…C ΨΟψ8j½Ά³ώHΈ¬/μΘ*ρν©°ZΟ±[΄°«Β2’—“]*}ˆ}ΞB3PΘ$Š‚nΔΓMu7d'(ŠΓ.ή–XΔήr—,΅«Wμ<Ή₯ΪΗ°£ς Ψž>α‘δ­*>΅ΐδξ¦rlŠ'oθ Ϋσ©7ϊΝUΡψšόΗώ8™Μ=Α€ΆΎ,ISVdSv[ˆΎŽ17δ©-ζzχyY•‚Ό2²έ Φ5P–διϋΪ£@ίΦ„5Γ$ Ο…ΏΎ·›ζg«ΠK}eΎώ›ΆχgX–ŒvΝpφκ?nXWΨ(.C'™ˆ ±Ÿ U@Άΐ8χR“η,Θ6 fΚ(‚]‚ΐ’š·a!bΝέΌN“φF=Όή3;| ΐΎ3CΫp-έΛπ»}Χ°쀡Ępαέκ΄JρUsΨ~ΒΈ6ΫωΆŽΘΒ8;βπEk* ΐΙ­ΩkPς.›UUόo }Τ5Ψπ%h ςTdm’mgk +ac’|“»Νλ0ά‘αDβ§2ΜΟΛY-_κΦ§€Ώ²˜rDΞΧg₯³ͺzoό ±X_¬­Y_JnΝ‘ϊ”ΐBY闝ήή₯Ϊ–=‰†­CέςØΉ ΎϋΕΟΒvΘ‘Νή­ΘŠτκπaC΄e!kόtό όl3ΨB,J}Ό—ΰ\ΰe©„j‹^š#O&xχ’Kθθ†Ί£>‰ΐ*c^²‘ζ%VΕl υJ€xm{Σ#—ήk?Š Άux~u{α oδ868©5χΰ|œB‡Η/ Ρ?Ηΐ†Εd²­ŒmρεήΡ™Lΰβΐψ±CΎ +oΑ—θ.½˜ Φ 5vΎΏΊ΄ΖΆv`7TžΧΦ-\ρΌŠ‚ή-ΑΚςωIvωό¦λGΓΛ'€d|§»ΕυΟΆtΗeŠΫΘδμΏίL~\:œ¦ϊό£Žδ.Άۈ%ŸPBo‹y³MΛ> Ιϋ;DΑ ΪmHw +«ƒM~η*ξ9bFň»5‰AYK>αͺ%_Θœͺ›bΝpΥγ¬%X*fHΙΫ,Q•/rž†_όΑΉό!™ξΑ)λ…ϊœυ>Ο‹]ΘmSf&»ΣκΜΥΛN%©±ΟdΣ~†λ Ν΅ ΰ°-)Οϋ­€¬«iˆοDœSByΜ]O Ν–!κΘG©`C·x«<%ΈP’· lp8Ώ°³Θ/Q_±τ—9i΅π‹7Φ2LwΩ¦κ„S΄„@YTΐd‚ΤΓN‹*’ Σ@ΰΩ—σΰ1’IΗ<[(šυ yσαόγ‘t™ΦΑΈŠ;Ι‘ͺHi™z>΅JXxͺm±:―Si–―΄ςhLIΉΈgΥω·2Ξ΄)nMκ—›ω +"–ξ ‹Ξ¨γDͺ"GΓw‘ Θ„D—χIΙΩδ"qΗ^͈§PƒςέΩίγ[1g΄₯ι0Φb Χmσ‹Γ¨γζ)‘z™wD(ϊŒΉ9ϊ’Ϊ—ΘΑ!― ήΑ9νXe‡)ψOϋ3ήUK·ηψμ»ξΞ ~3-2‚r«&]ήϋD,G1$δ;έU°DDΗΫ¦r9b΄-6@X€# ˆm%x:?ΨσmlΡ:ΐ6ΤΉΚ-Ρι‘ΕεΊrΩ8ΡI£"q)d¬ F ©U~ݚ-PΡ?Œώd·a1Hˆ8UΥz₯ύ½°ζ¨N―ώ ε|―Κ}^±ΙnϋΕΑv«ν`dέ<š\RQ΅–·ZΜΝ^J›x₯(―'Μύ«|Ηγ‘ž"gδ”S HMv:jΰ’€o>βq– "ΝJȘ/έβNf–†Y8ϊύAbkOΛakθQ…Σ\G:Ο]3αΨF;kž‚`‚°V%gUŽ΅ΚpΣ–±m΅ν;.q7…φƒΥŒ‘Ά₯³ΝfάφnB\”“ρ’±±}|Ψί&œϋ?@ρtς2wOͺΜζ»[γΔ-wbο‘;Υ|αxC7'σς½m¨Άν–Ϋ–τU7yG#[-€θψj]Ϋ’ω»ηHeΌ±Q₯1ΫtγW5½?!ΖC<ƒNΠγuΓήX9ν·eβάΏ%}™*CβGΩΛ B[XΟFΊύͺ_Εc8ά₯b5Yτ₯€‚a'‘“.nυοQM)—A°φξΠβ•<φοQ!€jNXDΒU'!taMxH J£8˜~Nr˜DcΏΏ’7²LζYFφ<ςΜmg•ƒ»0•›ΡeΓ³./5­@«1,›A‡²ΚV:mS,¦ ­–7Bk„6.вΕzX ρχOvίwλžΔy±YIΫδBx ζžH˜Έ]2χΚΕz ΊFΫ9~7Ω΅œ%½ ƒΎ˜η,“žTε€λΖ&K‹™‰ύZ$Έώ‘5³H`aaw²{VC3H.‡gΐ¦Οζ κ2ΒrκB­Œ(<φδ³ΦvΑ­Ϋν/θ ZψL( U(ؐyΌCό’θ±’΄Uοa/δ‰9LΐΌ"KŽΑͺH˜Ε(1Ι0moω}Jγή8ω¬βŒΚM4ή½m›²*νU βα'―Ϋ?ΝΙSΖϋb―η,D £oœβΩ4LTΔπΒ₯«.³ώ‰Θό5Ζdˆ©tr2ΰτΐ}‚>,y»υη·ΐshaΕΰ5ζ³ςΖlησQχ*«0‹!‚ρ?τs…zl—λρHΉ<Αˆw.Ԝ4ɏϊΊ•―΄™)Κ£fuδS MοrM¬Β Ε‰Ν5g|’t Y²`WΆΙjYό₯ώu7~Aιχ‡χWyšτ)GpŒ°+ζY ΗΕ²΅©Kέ(uΎoU’ …±πž…ξo=Fήq™πήYNθK€œφ„}\Ϋύ­Ξϋyx'.—,{ΫfΗ +žϋVqŸΫ&•]η† 1‘ˆ„Ίμ„ΐRΡάh‰η%τΞΣ +U©~́²3Ł'‰"%„ˆζ•C"oaκ§—ϋYsΔ2Dι’ΌΒG@Ν±cjD±χγφ΅ZλάSŠ―˜β-Β1W|wXΏ4\!ΒΔ’i(ΈδŠvΓ/.$YCΨΐ£―_ŠfΆ˜’ŒF;œΥ­“—‚ζO΅Πφά+Ι>‘ ΦύX~gͺΛΫ8I{}ΜΪ€ J²˜/œGΐ¨ΕμD‘cΙΨλͺ<ύ;‚’ZζΒίΘGΈ/Ž@[z‚—cϊœ§=NWτs#}ΚVτM½FΧψΡΎcŸd—ΚΖx«Rπ ²ΣoOΦζ†}G\―˜LχΞzΜ•G8 ˆzΠq©BχŽCμtλ”φW.Dœ£YνYb™΄ϋ΄“ EβIώ#ΐŒΟς―?KšY₯NgHcΆ=†Ρœ¬;Χλ` ‡v'zΨ–Ϊ›‡κΘCρƒ«ΠD£v«ΐ«Θ†k­uκŠΤσΡλz‰”™ 9ΖΙO|‘uϊag°Υ°»e"iδ₯_Mg•O=š3ΊrXΖžιΡ…~ /›½δξοΖeY―5Œ―Σ>ivf(‘: +/’βrR‹ΫυWΕs+ΎΪC‹‘lHrt—•M6jv#(‚Ζζyύt€C’tΖQή]9՜‰™βΡa|ή|dέΰ%ίΘ@7ˆHŽYη” 8t4QΒoκc‘@χ“aΦ>]Έp‚Ν₯Žΐ(R ί;C‘zLψΏ»›1 δΕ`KΌφΗ™\―‘cJP[”T!²€Κ½ M‚ƒ¬gβtν\τ΅{ΖΗRΥi™0'…•') §X7:jQσύ '™ιΗr'"]ΡϋD#Ί!”~6όγεlι"Βό}’Γq7U“˜W‘αήΜ€yΠ·ΦBR“~‰ŽΪ'ιC|’«:, —0Vζz”Ϊ€K±£Φχ"rΡ₯ν‚D‚›-ν ΰδ¦akKlΈqUC·Δ½1QθΗœE ΜΊ*”ΏΎε¬)Ν€ωPZιјœ‡υαCuΥ¦miχŠ[ί‰ŸΪΠgGŒ;‡μ[gΩ‚-cšΎƒγηω_ά_ΔPECζΑ8'hW)ž—.­%m¦§ψL$]σ§rΝ&²­–·D$+p}Ίdž˜:/ΝίR.…pθr₯ή‹±Ψ#’vβ©Ψ6λcBbL. 1a§jƒ@Y[ˆ₯4ύX· ΫΐΖ°„vbΫ +"”qn±“‡FΚaW¨AI8ΠǜΙϝeβ|%D›rΡJτg&²Ο!ΪϊAδnΩ?¬A˜Φ8ΪAΏ +κΧ$εdžnΞ„Z °pAmuζ2 λˆύ“xΛmνή +O/b1„,RtΔ½@ΜΗ’Λ£«ρšOœΏkƒ―R=pTZ8+Άβ”άNΐꈍ‰ρ δuν«Ϋ>·;ς¨)XqΪΕIΗdšŒ_Z³ωί»rν Lv―L\˜EroΤ›?²Ά:Νβ·ΫT@‡MR’ΏΌ{Ώ:\ZΏ― Έ><ώλDGρμw-L7ζy£Ϋ0„Hή:Ε·i•©hGƒ†Φc Ηξ|$σϋ‘dιΜL[‹3/=€dh?]κλΚWλ­Οy;ΕΝ.IŠLηή²ƒŽ-A+ήΎ˜φ…eαdύΤC~n^5΄_dΔU¬t€‘ΝφύΉW-ΊhR{‰P³g]’ύΦ[θ”‘HD’»vŒ’=Vo?0‚σ’JΔΑ¦§ρ2LVJΓ 9~%NΣ|1$φΠv}* tωΈi?Ğ?•?¦lΥμ8,2ήωβΥω&Ÿή‰ΡΖ]θH0D‹’J΅³n‚nΖV¬‰[Dκ>Θ:iΰ–ί»€)—}lία0ͺpЦΠΝF1”)™0Z°G„p6π?ƒvφέΈΒ­tΊv`θ΄°Ι¬dMΗΆσδ©tϊ(*|˜ΰβv³βμ₯"σdE$Κ‘ŒκΐƒΔ¬#λx?`$|*ΉηΑFkΣηcˆ“}³ΐv–΄Β¦ωθ ‹εn: " Θ Α/ΜwΉA5XΜΓ›7ΆΫͺάΈ”rυŸ*VŒZϋ3|=ώ^2ςχWQ•K{φϋ#WzΉό|ρqΐKuήΠ)nŠ6¦’ Υ­$¬ιόΤΞΜlη?ξtΛΙx +α'±Kšy5 Έω5 ’ρζ(uYΖΙfDαΓ―ΑhΓ€°sτ +Ώ©P—εƎ€•Te€φ6ώΡ†§ ^8Ε,†("?uˆώ₯—’Qt”ι¦tvVη\2jta)žt΅”’΄ Π6Ι•[}Ya5c{ΚQhμ`Ϋf0΅EΙ·-.3θ½#Sΐ1\ζζΎσ‚NQβWUgˆΆΪ1ΊšCIσ=Tή&‚M)άοΆ} "φ Ί1°“7zδ½μWΗ-[¦8[AηžΪΡ-Ό.8Ύi°UθdΔq υc™šŒ³-PΘO˜6€.s„JϋAηΩEΖ»}C\ΟgγgM(¦IP Β +šόΛVΡ=ΦυdΝHaώHšω'φ0½ΓPl›oŠY}ˆύ‚_Ύͺί3:ΗxJUυΖΤ4Έ·CW(xήςIHϊγ• …‹-] γ§‘δ› ΄kη{ήaώ‚JPσz<%²ΎπVόRMr•4sΎ)κ”5‹CΡv‡μπEGtΎτγγVœWτK˜ρxWμύΆaφ’u­$\ˆC{;Š#0ήΙ`²Z*" £«wϋ Œ“ώ˜σo•P4όνν(΅Έδ\*_Ύ:u—{%rε½zΪ_‰ν›8$ΤΊ†˜vν ξ*8oπMΦώ-”¬b›…sΞ8φ£Šϋ| +ώ{zΘΒ»¨n<Ξ\&ϋ,tΖ=„«Θ! NΏΫΟl3rΣ<δ憦$<€WJ U’‚±!ΪuΡUp‘Θuv›₯l?φœcΘΔ¬H” £fΠb°«ΒFΔ­Y4.2SpEδJ#]Ύ₯‹BOe‡ϊ“*¦Ÿ#‘-‰yiϋX—,ηλԁ<;^}0ν΄ϋΕΑ΅R»ήŸ―p Yƒz#sγΒ¬Τ¦Ρώ=σ±vbͺΨcƒœΡš—ξΣU%Φΰ”^ +šΖΣYqΦ₯;οχ=špϊ±yPN±}ν9Λ€±ή`?ζl­C-λ7Ρ3ΝΛψχO‚ά¦^£ΒΠt0?Ep:£Ί +X>4B‡·‘jQ/B€Β†Κ@iž_X`Mυ₯·Z6ϊ¦  "‚ϋ,ΓΗϊ20“/”όΑK[”_/)ώr-{ώ˜ζΜήC7"αΜt•Tzϊ…΅>J^ΒΑ—X”μ)ΐ–όb‚©3›‡Ψ‡nu· B3yΫ³€Mέ2žžμ{%œΝŠο䫊·9Ψαt"˜pΥHΤΨ­ŸΓS%;:{“‡ξb4Ήz"GwΑ1«‘6γ?ckW>Ζ Σn5^•+X(α+Ό¦§.Δlˆsμo3{)Α΅ϊ°ν„‘QΑΞ²sˆ΅@₯΄€d}ad₯ΤΜπ…ΌJ½kÊ«™š‘ͺΠίι‘ŠΰΉθήn«VιPψ”‰—₯tΪξ¬M7! +Y4(‰Ψυl΅Ώ.§tC<%κΑ€H‡a΄φ·₯Η3™S‰Υ +°bŽω«‘ι\ΛYΤδοΫΓθxχ°ςέV± λb^x{΄G©{v·Ψ+M¬>R§šŽ'Wu»Ju‹ύ@ΦZρVπB‡Λt ρQεFώŠ Ύi"©YΛP6…΅Ή9JO―Uel$ΨXe ΅ΒΑ?™Žδ-)O"]ΌΠ݈Ψ©“Ÿ§ϋγ)fmΓώΗ¨NH»Ϊ:ήΰτkm]]Η¬@ε?ΙvΤΎβ3ž9X‡Λˆyb;2ρTGΠκF”KJ¬ώ’UόMgzί„eYό 㜏΄ϋo°DάDόF7\‰όφ€ςΧg"£]9.8uΈΊ}&›+/O·»/ ¨i%=¦ΗΕ p } U)Cσ–wΤΐBϋQ–3 ›λg£>wQ~ ζ&fN‘§_Ξό9Η<ΔTξcθ·άΆP0 ΚJ.ώSE­‡FN"ϊeفͺε°χάόC’OϋͺXϋΤ/C€z`<Œ’Gο!ͺ‚Ύ₯»)ΐΎΉ§Γ^xcΤΐΡ|YΪξ?œ™".<³γÎjw{–έ²ˆpϊlL†&C,:YJο·σΩ”ƒšΏd6Η N-dή+#W­„^Τb{gΓ»ε€?½mΠr-Β\HCηdνώ wEKƒ£ηͺŠ…ΠͺςVΒϋ™αωU Ě©΄§έQB_Έίψ”jά\*₯Ϋ֟:ΙMγ/Ν”ͺδΒ£³œ±TN(„O"XγΛΣθ0M#οfΣΈŒΧ*“…ΰώN«0­£‘εŽ1Hα₯”ώ-a#0y;μJΆˆx“μέݏzΞμ>쒁€υ\Γμθu«g‚„φfo›(= Ϊ"~ΞN­RΑ+UŠ/5Y.ΧΫΕΩΛlu¦–3ν‚μLͺO§rς¨\Κβu'QτLω%/9ŒšαEOwN@‘δξzό rΫ `"a Ώ"β™+―€¦E=a”Γ,Ϊ χ_ρ>.ƍ?$’\šψΰι"=εKݟN;^δΎΡ#ϋcWl‘ι«f J4r–`M άHσέmp +Γ"Ϊ.ώφΝ~ΰΠQ©]&iK#ΈqΏΫB€žA·υš…ΐΆw‡±X#γοΖϊTξ#€`H—PeNω T‰;Oί¬δH€Pfwƒ²ζw‘˜ ©9)ζŸ5ΈΖΦ°ψ{Oώ!zΧΟfÐIφg" 1I9–ΉutŽ‘~©&τŸυI’_‹KŠj£ε›ŸDϊͺ"4Ÿ6Ώ Ζ ΅Ί‘Q + ζ½qΗC$bΡ55ΆCΜ|Ό$ϊK +N§–άoΩ…₯’„Hۊ ΎGW΄¦ueldo8f˜8!ώλ΄ ΛΏ[cu(X ίoqˆϊ˜v“Mκ.•‹·I5λ,7aYΰΟzn’oyτK%Οzξι‡ΧπΪl.‚δ]sέͺγ†mŒ“9άΨΌσŽΤdΝ‘#š·σρπϊέcσ½·Ψλ mΫ4Œ)§‡Πφώd-j£u kΘ―šμ27zx[cJΝ&‘Ν—pKΈ‘„²k ­ΡE³»ύθ²ζM*&O›:9θ‘yρΥ;Ω‚ ύ3½Œ…₯3ς}_ΗxaJQk +LYH”Ιz΅Av6ξΕ™˜Y™ΊΫpΞ‹™ΧNrρR–}m₯I°h&WΈDΞ΅γ&ρΔθμΏ:§έΣ;R0 ށ”ςhβ€ΛJ·πΚ$!`{Ϋ$jTοjΟϊά7Š1^:݈ϸ^LeΜ―ϊ@&tΘΘΟ1Áκκ3^―NΎ°%ˆˆaP›ΎΞ¬μΉ{τ§Δ·P« 1 Ό'!Φgαψ³¬υ}m§_‘‘iίΜ’ώ¨‘ή΅‘g !/‰£ΘR[4ΏΘ«“έŽκ‡£}ξΓ!!ύΤpSa9ΪYόδ‘QSήτnοeώ\€#‘]x "\Tψν`Λ κVŠͺ]’^Ή`C—:«Ή%`μ΄! +€’Κ•ϋ:ϊF½GbxόΗ9υΛ»›ϊ¦Ϊν³ECn™ ΠŠfΤΈϊμάQ¬ŒšΉkέ%ssμ₯ε[ΈΓOﳆ'sZζΤQKΒW, `8|ufΆI"Ό–Β›/*³>£¨β €ΊLm’‹7πͺΦθ»ώƒ%Zψ Βκ[0ν΄«—…/HόΡ֌6υž τ¨α,¬Q/<λ5ξιπHSΨ₯^ίς ¬ΠϊƒgΚ¬ΞT-t© #Έ.±‘i9ΐaΙaBiƒ#m^«‘Ο›θ¬C\ιωΤUͺ*^—Κ1vΘ “}Ή―D~=ITΎ°u•!©Ί‰¦g'>”+NΕΣ1θ£c'ξΥV›ζθ0ω¨`qv0'Ζ§ ½]ά@ϋΣyέ):§ΏM1|?e1ΑςŝOΑΰ,L $_ΔΓ8ΑP,D‘P΅0ΘUθΞΞΥΧς^Y‚C-^Ζ°ήΏ%֝ΊY"S—ͺώj—ΐ…βŽ {6€AiόΫU£ω·σ9nμ€Ύ½Ν,9Ν*_ΤeΣ9‘œ%iRZζ€Ω½ΘΒ •ι2vΑiI¬Έu_Υ εΘΧ’ΰ‘χ6c»pΏ$η@ώΏρt—ήTΎ’ΓFσ΅f "4”΅­2o―]‡S\Ϋqοd,XCb`cΕΫI±ήΑE +A–ή’φŠd΅u¬1B?wŸ°ˆf)žχTοb{•›y,œάζ2OdgΛυƒυ ²TJ‰“έž•"Π©Z΄”³φ-8}ΰͺDu±ίϋُ;X‡qC­ Χ•mΉΜ½V!QάΟ· kvAzεςšΓίe~Υ9†§yc!6#²p*ž§±€fα&ΚDύjθ& T³‘+m~ΉάπΜ―”IiΉXρξwΤif–ΆΧ`1…ομ{όƒ>4—_sγ蚺ό,yv φ-ύ­Πx5U±7£αΧΣu>i©šny«Œ Ψ 4²v”kΥF“\Ά"πΙ`L)K*‘Ω²α9NRχΨ<6Y¨ΦΏΧ}vΛf¦ίUO4쑫ݝψY·QΫΏϋ[Z\Mκc-΅r%Kην +ηžΡ\%c˜iτ!±KΒZς99<3ŽΆ σ[ $q?Ώΰ8Β_¦ς57ˆž+\Τ΄k]zδ –P₯œf>*όO€²oG² ?ψ»Ψ630zοj''_ͺTΪg"ͺIώXΒͺΔβΕ RΦ2/§V…4"KžmB%ΧR$boF•.Ο‹’$pΒπ¬z?5Ν›30P7ΐwΆ=‡,QδŽ]wCeΩ*±5lT‰`Θz2ω°C,b+h.€ΌdnQΑΕ δβΝ~ς{Tε˜p €2ͺΥg`Νm‰<‘;±ηu¦ο3eήΗ5Χ-Ύ«:KδΎ…Χ™ΞοaRvž?Ϊ»ΰ7’–όm2ΠI†ήvF†~ΛλΞIΩ±Ω^%VGρ έφ`α[ϊ7ΨΊJ«+€QL²ώΘ aάΰYfΰ犾ΔMΕI7e\L°ΘήIΜΜϋ=Qq*ߐPΘζK£$²βϋ‰WΞ€Vλι`‚ϊF»ιΤ@―xK¨ š‰Ϊ1šΕ}ή"€c Θ-"Ι·&ΠΌiΗ,ςΎ9Φ·γŠo>~‹u©œΉwΊΨύ1S^ωΚ$λ#₯Ύσv+:m³ΰi;ώnνœ]LPNC 5y[ηΨ† 0’†9>R‹χ ι{’žΏ‘)7[mVΨ_pg«1Ł2E˜uΣ Υ*ΝY;ξ΅ +Ρ8Χ±…vτŸω­8ε“D’οœbmrn“²1¨FW27Œ\ί©Χί£ΑώeS9(j»Α­"ίqφΰ‚JD@ŠχuFA«½Ι D¦VG‰ί‘a/Aˆζ(ωo’& } ”>ƒ’Τo μQ²q 4ΐΛΖ+&=-‹NΙ3ΒVgΌX?[ΰΏ‘ry›yaZEΣYΨΔΗU_!pϊVτόΜHm.»sO(3mBb{xmm‹q <³ΊKv[­ˆή( ~…ΊΰvI±ΝΤ>w|©ίSGVΌςfπQ₯•ύΐ;`Jj>ό.έΆ3―Ι²a$ΪΉTε―‰gΪϊχpd1υŒHsMή&bG§5Š€„ί‚ΔΊΗˆXψ‰nž‚ΦJ’·V•oΦΕΎVδ@ϊΩ7χ{ΡνaɈ-ΙΠ:82ͺG%\žJφQ)o™"Λ+πQι <Φ@O˜/ΰxώIHV¦ψa)bfΤ]ΦΌoQc°η¨ίηŠSΙ‘*ž_ŠδU­―Χ‰Ζχ·Vή#‘ +†±ΒδΫΏΙΑΝ. nэςσΟ4(―Φ΄.^&±„nρ q+2}ΙLj‡ω؟dŒ5Θ_P²™λ1.κ₯W}όιΑH[$τΝ‰ΩΔ–Ώ2ζΓVΝ@‰―ν Β³+ίu„‡χɚ 5ΝS`/`ζ±υ•*ΑΊ„΅dΙό¬„ί΄θφƒΟ¬XόΕR5ΒΓρξ&ςξΡΏ6Υ–(½ς›iχ)­¬’y_!βؚšΟ‹ψΘdc: Se‡\Οlμ&Γlλ]‡ θχ¦ΪΣƒηŽMβό:yό` ½ΓŽ>˜Σθκ ΆSž5^LX‹χƒ3#¦ΏŽ/73ΛeυΥ6Pλߐ.Ήhρϋφ―(ψO?ίΩU£‘ΎAJάE₯’ ¨ΨkΤΖ ―ιΚηP­&ჽSτ“₯υη^$¨β[ήθ”egίj£ HμEξάξj³κΪ("Ωσ‘–F„bΠ—1».‘”°ΑSŽί”£Ž6ρY&τ’„ 8ϊh82›ωήc4καOW`ΊI3άψN D*‰œ­Ότˆƒ©’άΩυy£q’Τ^‰β€A¬-“‡ΉMA{r[ˆ¦s_ηήΡ!ŽFN~²ΝLΣλΨJΔ:"7›3:ˆτεωpTtλβ\.ώεΎ ›qΞ lOςσE6[“ψάώ“<]Pυ}cf?ά7 4§γΥγƒϊ΅…Zb›ϊα!2ϋM#Θ1@-τ]]β±x5†tS$υFγuNJwѐ‘έYP Χ’ΠibCζ­2&«yΠΕχΔΑ‹$βΚycFτΪkά}FŽΣ¨pF£kbΙ>pΌ;6ƒnmψδΒwΰY] Νe‹¦Ήϊ61P|³SŠΒ€:ΐ +fζξάW,\°…ΒσΏŒB<ƒΥwέ6ΈΡζBŸS˜ ΰΛ‡J\Iτx€αψŒtDcφχΏΔΨΡΜΐ§αRζm˜Ax#HNOc“-JƒγkV+ιφΤ½B4{δ’γ\[Qψ^Ž€ΨξΙo`Ησf$uNžίΉ)HXμΉuR(†€uοΑ!Ό§x\l +`υQCBx!—;{ ξ ΝακΉΎχ~Ÿ’φQΘΖ»΄8_—…œθiDŠzQΠs΄ςΩφš\Ήu‚…”  +ΆβΪdY"yΩΫπŒΊΌD‹cRh%Ȁ?ψmeξ{ΠΫ₯)ΦeΩ$7Ώθ +¨„{ΛV«Θr*žT0άϋfΆΟΕγ)Γ]£ςαΡot»JExυzx>·b˜5Όή%qS²ˆ )#Š~))x€¦λgs­βXφt,?tWb~$_οs3σמη©ην"νθxμ‡Ÿλ@΄aϊοΩΛkς'Ν}θ₯7(e@1~ΗWHP²'NΰΉ†—Žpηx«'7@’ΕΆγcΗ½žγ"¦-"lί'ІΓGΛΡ'K`Ζe€Qχ‰ψž(ZC4>©$ϊE§lZΆ+mΟDeˆ +_`,ˆ*ZΎxπΡΆeξ±ΨԜΏ«ΎΪ²S›q­ ΄ΗΒXaσY,υN•,‚.Y’ςLŸ €ςiΐLΓ@R˜ZJζσπ8 .?VW˜sόΎτxώE/Όόΐͺή,e[θζ¬Χ^e‘«g/λαT―Ž„Ω•7‡RΊ‰ σK£Nž^z δ© œyύό²έ’ ;–―ΔσΉΫ +‘¬ρζ…ώ£ P iBθd¦ƒεJyη½€:j£Πμ€₯τζ[Ό s¬α†ʚoΒΫaΔ³*Nq jΜ%±χ"Ώ[$6ͺsJ›©E(’§Οφ‰F_ρ»σƒgIΒαβz»™³M°;VΑ€ +’N\yκΪM/Χ―<Ττ+μ>ΰω{Τ=u€» ϊ(9zΦ)―l‘ώΆ―iWiΥ/NέD„Me£Γk,o΅ϋͺρζ ‹Λ9t.VΥϊΙσA²škœβn9kšςνš²dΫαθφ;_>φU|—Su―±%¦γl1Τ’J€£ΦB²π³ΩW‘:·πΌH푬·^šz΅΄ΒΕΏέιr0ΰŸΫ7¨š|(‹…ΐ9ύ―…·$]shαvƒ;ΏWο $ + d[–mΫΆmW²mΫΆmΫΆmΫΆmsnOΜΌx·?"Ήφz1H›΄’Εψη\:)‘Ί»}Ή€·Μ ^Φ.εΖ ₯0’†΅ξD°$“³MΦb&6•l ˜h»ΔΨ§ν³}ǁΝ&MGεBΡβ†―™VΎδ 5jG3’A?L2F9ώXβ]WyiE„γ™ΙΖY• [υ¦«θ]YŸ—μρ$Kόαδ<’$ƒΐ׎¬μωY„§}¦ε; Ρ―ε‹vήrΨjπ·=Ύy:zŸ•φMPf¬δΕ%@κž‘%,•†Ί₯-—Δ»7½&Ύ­Tμχ³Άžv—Rs' η@τŸεΊδxτ"Bς d5xGΟΈιιCž#εΜ”%·Τ}]0ΐ O*Ζefχ Κ\ίgŒϋL<ίxζ₯¨ΫΧl]}™Ε0_§ΊS}Μ`ΡΌεDAwb;πg–J7!Pυc4Λ£€ƒšB₯Dβ%±sP +/qœωη#>/„΅wq΄W–]Ph<&Sd>J68œν榏JΜP&η5}e'LαPœ™H/hΨΤ’f½λ5e\³Hΐ& +3Ž{±u—Ρ]•’(v@ ±/Vέ―τ²ί‚Η’:Ε­}Eˆv—u3c¦Β²…cA©l(±΄·Ξƒ|«lΤ%S:Π‚¬γΤε’ί*Gƒh¦Š1Λ0( +@$υ’‚€ž8aίΗ7:dλ_uaJΠ~WΑϋ‚y]v{FΚ7θ\ ‘¦‹v’‰Λ½‘©,Ϋ‹”ZxDΪ’ε•+ΪσXχŽMΥɈ²›}‡‹FΠNšη h’]א: Ie:(|4ɟ:±S!*κNi>cX.,”—¨Μ„M2<ΰξ1π(šθ³”&“•(·²}Š£cΙάvj;qW™·Bz3ΟbΚ.ϊŸΘςΉ\±HΦH]Ωweϋub΅Ίa©†RΓppŸΕθg6¨Έ~όkΧ kβFΌ&`g²“b΄L>WΘυ5]Tν”ΰ°{νΒΒ ρΈ0&sPixKŽp―—sK‰/ιΒq!οΗ.₯0δμIx¬3•'χ’Ρ{›‘4TfηΔζ™m«L4ŒΛ™Yτ0Π;~u o{puJ!#[ d…±Ύ¨έΠbjΩOBΰωšB»η p.›#± ΄ΩϊφEv!υ(R?CΎΒ e)RɌv99l²ŽkJ`…¦Ά>»Λ•Έ½O]ς &ΏΌ €)Eλc lz6| Qϋ$>p½XeC?[ίΆΉsƒ/™ζεθW¨Πz­ΣΨά7ͺ‘~\©a>2ΆΞϋrŒjτwΒ… +[ν‘Αlƒρ6“³š†υFΊΒά°ΜšίΐD=ΉΚ*ό0ψέ 7W±@φ½|Ϊ)N(’Ηθ=~?·ς\yΉω‡¨ Jέ•©D~ΝεΚYμ)‚Y.΅™…‘KΎΒyβ­€κ4XppλΤρ€ZWo’š|^‹έfΩ“ 9±r°πΫηπ¦‹4ωI%έΩΛχΚ y‹£•’=‚νŸΠΗ HPΩοΰ*Up―ϋ‹―λMBUΥb1γΏτOΪ…βR‘υMuQšTq*… p₯eeζ{uoαΞύΚ‚Χϊ`'R*[bσKΈ΄ Πit[d½½˜uπWxpŒίυ½ƒ|η ΐωί+9υŠ•—ΗNŸE­!”0Ϋ«N Ζ8wω3Ν΄ˆρ…Q> :Ÿ+Μ M“:ΰDkF°ΦMQF*ΒπZEςkr^°ψ–F«ξ§΅T:ΠU’ΘaΜK°­Μ ­ °#K4Q2~Zž£χΜϋR0—ΩT²Ω-ϊ?.Z™•d‡•`/ρ„u―sLE£’Nˆ5brΛ 2²­=38Υ¨E’2μν!ά₯=€sΈŸ­:sN7_oŠ*@1ΌΙΓ|–‰}@Δ’1ή•ΗΉ N`Z»Mv'’4z₯$θYνξΝ½4ͺ?[υ¨ΌJΌόκΌSvΚo‡Θυb€RΰŸ,Ii RFjήδAΒZϋyΐϊn”žίžrΕ£ύΠ͐ Ν=ώΉΉ:<„PΣhL &ΰ ~_QΛ“ξ:—e½c3 |I·˜RŠ―π₯»°οΔ»'o™5ΩΘδŒi‘EjΧε­ΦΊΛΎα· —-’ΌξΎ GBλ(z’΅ˆΈJG·j¬Ωd0/p[‘,cνT5ΡΔ@zΙ·9cΤ™–kΉšηXΥ"H`w )·υα{DPώmJβ8NA(’U +Ρb‹(μΡCw'(²ΐψχλΗ\%U]΅άΚoG6 bύϊ•/lΰιR”β½4γΑ –ξΧώŒCΩΚ +^‹y@XSεΏ“!‚ΚΤdŸεDοΰͺH3γ‘μX/zδχ\RΉ±½0Οq<³£Uί‡v6 ¨Σξά»‚ξSY6_¬Β΅Ι“Η]¬€ψ»G±°(λΟd1uδ6φWHLcŒ²V2·Ν~™du¦qΈΏΡ흦lPΥ'J4.Α_οQΑνb.θ)eΞ0΅0Χ.ˆ\ι i Ÿ₯υ—|νy»₯TλΥ-ξkUVΪ£¨Οβ¦θ˜@IΒθψφlΎU—[ζΆρ₯Β¬cEς+ͺξz73C˜VN£3Kϊg΄-Y«~Ž@FοΖy=ž(’ŠIηΠΐΫĊΖ.€Qϋσ‹|Fe? ΄z€cΜ"˜+Œf#uΖίΨθJq°6•Ά©T7φ]ΫDΈZ%¦2ΦUϋ΄{° +{3ΰŽΠjΙΓ2Φ•Οlј& |ΞΊŸY m›G'?Ι‹Ρ‹dQ1±Υ•.( ΔρK,)°9@€Λ²vΦά |‘§Κ>±D―Ttύρ¦CΪ£™¬ρηi^“=¦gyŸ$Dι£NPϋg1ΡάΒΖ8HyI”­ω³–V$iZk“ˆlΗθφg$σVsτ<)γΈζJݓჲ3>Rί²Kž‰ΞŒι rΠHŒ ― ·––£Ν½ΐ ―Ηχ™h« Φσ Λ1~_ΝKλ¦$θ'Μ Κ°9V[17©dάcm)yw%xwWΥ +x…Κ“-©Θiθ|ύBŒM/Σί§h§”?†½ ©ά‘ΙΙ *˜A’β©ŽόόΘ·Κ‚ ežμ?·)Jν{i¬œOiυOΰ2ΊΕN"9‚¦•©ΐΦ ω'±c―K©q ˜I₯W-EΤ‚ŸlƁλό8Ιnmο•ώΕH$ps ¬·^@‘n`X§€9U_ΰ* +ΆΒW&’ΩŸ•mΊI0=SrbΏj0Κ&Khΰ~ΌΛjQ φ +w~>gG€4ΑGaΤωͺη>ή…6Σ‘ΥL(iw•7ˆ.e‘‡Dm'‘5AΗ·˜όΟ €9ίΪ‘ATυ²°κΐΈΑ vΚLψ*χaKP?ξbŸΫW©ΧrυΝ„#€©J4†vsWσͺ·Ι͞¦"‘ˆ 0»ϋ1›’ΓErΩΉcΒRπ˜Ι€6’M'qhk(φ,p]vΖΡ~ύJΟ¬ΜΦ©ŸŠ)«ž³ +\χi°Υl“NJ9„φγTRϋ7Ψκ`]hA”Ϋ`…τ!#ωL±yκ{νVS%šΥBά0Œ€zΘJ +ΆQ~‰‘^±΄υFŒC_폂u-ώ=­Zέωˆ*ΌoO3‘"Šy‘RνOQ¬°7yΪͺΘπΥ]ϊ£Ϊζ”?{N‘4jdXωΜξ|Σ₯˜£N&½ΣHcŒσ£σ²Όαrϋ‡“ΐZω@ œΏ… +²Y9&)Ά}F”‚;ZUΡÎφΊ υΩW=«\ΤλE;—φΉ‘Λš&*EFŽb­£Lήκ,`k,2…/““₯"2ΞόeK †X©!HέΟ]GfχΎι"λv‹ζ­I΄|Ω€ύ9ωχϊ:ΙgγΧ욡ˆKs +¨ώ†ΫUμ¨Ϊ ° ~Ε’¬ε­'ωΕ>η> pζ9‚€ρCzŸU΄ΓεA{²,έυo΄Ώ>7ζΗα/*„s“QφβAWg¦D6π]huςm. ι^ΕέuΉCrxξΓkškΒ ϋtΰ¨…bΓqδHѝP)!f”‡4έ)j"Rω;8eκA+ έ’Νπ]ηΈ#αοΈƒ e"₯η) ½0­σh,FΟίϋ°Υ‡–ζά­OΥ~21'ά]Τ6Ζφtέ$hovi―ΜO·V3’/*Χ +šΔέ‚jξ«Υ’Ό€)ŸGK\‘αε­—3ζPZ}σ+\,¦c§σύ2β'Α;H©Ύ£1ι&Γ‰ΒšΧ#wZΦe{Ώ΅woΎͺήϋ›C,‡|Ά ΚxΛ—T‡Ίο& +Z~uEC—=…‰GΑ–ι ŒΒ Ι¨AF}υI”#„ސ§Œ£dγ~`<β"Ο%ζ1Ο|ΩΘvϊ‹T4•ρνPžUˆBεν¦g092θ ‹=’¬?Aςkr"‡DQ,,ΆΝ- ‡!\ια‹έ &n£¦ FΚ¬QU˜ZΔk:‹HHC™`L±6Ό’ +<_~ΐ‹C5$&H<²•šΦ)ΐ!=ΪΡ8pΰΑό©~Cͺ“;’³ΟελΨΏO$`σϋ}ι†ΨόΦμΔ=―Or ζ9Ο2p%pΏxΘyy€€ό·'Oγο©\‰›ΐwT?Ϊ‘·M―ϋ9ΏΟ +π†#[’Ρ§-Ο„ ύ Z)³„I].H;…έ&Ÿ`άήφqπ’ έ \ΰ'y$™‘—ΡΉνf’¨ž•ΧΤ(Ss*:~)ΔV°Θ‡οαϋW"r»@ί₯E΅^_”ΔπΊχCΓV’”R€!ίi΄γ‰λϊ°ύωΌ•;#_ρα %γ³8“¨/ζζ+iΊ,i }Ψ‡ Μ*ξiάFs5IKΝΎκOͺχ-ˆ¦ͺΨ†ΗOP3λ@#@”»5n-ršiΗOžΉμ~–|G μ‹–fJΐ€H>MkΆ΄ΑΕΘεX§ uΡ―Š„ $wžPZ·ΚΈκ7° $[Ψ6-\Ϊƒ; ³˜μΉ@N=#α λu|±Ϊ’.B”γ?Άη.w!i³όc:PI#CΕ&Β +jθM΄παΧ>!^„€5XA ϋ*„›VΤd—σ3…”°κ[²ΜΐΚ·_ΤpΕ&½”‚%τ ƌ”ίΗΩͺ:θίΐSΌSg•yδώvpϊta²p„I‰’Orκ²`M¬χ{„|” Δ1κ:Υ–όρyX–zψΝΘπΆΥ•°ύυWΆ·©#ΘίƒΕύFŽ!-u^Ιξ„ζAPL­7cϊˆ +½;ΐÎίZRΫϊΚΟΑFx 7;†Ά…?Σϊ–LŒ-u~ͺf…Ρy–Θύβb‘l +νε1Ώ ΩκύΓτιI kΐG°q—p\­Ζ€…>κ0dΒθ~μX‰»Mΰ?€ ›ΥάUlzm«:έΔ!Fe(L`NυZžžΤoƒ‘ή/¨Οk<ζm»C +O™όNΚ?–KΡ[i=e‡‹†~‡xρŸp’φ¨οπ[henο>(‹τƒο»!AΞσ½Œ±μzν€ΏoχP…8―ΎYo-vu—Œ σ—ΰ gη&Ό.Žπ¨5#| ˆ!‘Γ9\aŠΡ$:#ΪwΛέ7C­ΝO71DΪlcMΧFg+) ½Nδ-‘γ¨_Λ₯–ꝙΖ]5ΕΓv0Ε™ τ“B½–Λ:3ŽE†ξΤVWΑœJn€Θ!@-ƒΓ—Η=Cρ[AΣG(#oΆΗ₯·U\‰#{χXƟiΪ”³Χ§1ΞdJfίαvΧMΘf“Δ’wΎ‚-Y?>£<aΟFώ<#C€ΝZ$^YSZ_G[έ*~φ²ηΕ EΡGwKY˜}βŠ+ΡF0`kΉΠ/ݘ”·©₯—1Ϊ*³—g)i1 o( &d›EX™¦¨‘œbNa|)έ_#[ψFZ'ωΟuΉπcogu‹§ύŸΧ”ν ΜύZ‡.ψJΦπ Ρ³χ«ŸΠKΆWΜF°ςaN­ψ΄γ|‘δΧΠ])y?œΜ ―ΖφD !,Μ=0«ύΆ¬]Νt½\†Ρσ"­Έ Tk8›ε]Bqg,jj Κ6νδ}M–­ωΝqœΒ<(ŸΗm¨Ϊυ]˜f–xƒ£γoy2Ο*kτfj…ηR,˜Zψ1ΫλAwιatΚiϊΟ]V„¦‘ b’βτί4nͺD­ŒΌΫ½€±©OρŠ ΩA[±<|B;ΎT$G“ΉηκOnέ¨»ΪπμP ; r΄Η‰Hδ₯λΟΜ@άzΚΒ!sͺP&₯A,ΰ1{ƒ7ρ6’šλ +η†<œ'UOΛi|Y TEy₯’ !JQg7±₯‘cCƒ΄@?G?κ©9Ή.tX β’.ˆ}*ƒS^¨ΉH9Ϊp6ΑvΏXυΧUAεX¨g΅$㿝§ ωYP:7aŒU)½…δ„©ς˜§lΑ@; {dpu6³Š…Ψš +Rlδ M›ͺςΝdΗ{α[g\­4φ>:ΠΘe +–|(Χ±‡ˆ‘tΖ]Σ¬1κΔμδΌ+(QΛϊ©”( 8㸐7ζ[Ί6ʐζΕEχUιΣD~zd HΓ4qΪί½‹?>άΧ}iΰŠδβ'±{—^΅―ω5P ,?HΥΘM2 zΜ’Žκrx€3Δ·υ ­ΐ“Κωhιu*d›HχΡ»;©ϊt 7yv/Η5’ΒΛ yόœ‰—βά-–nTε§~αEBV yΗ”Ώ^’|€ίΐα£}²Ν₯ϊʐ|h¨ΔΊύ-Pθ¨MΣΠθΈΫγ>σ―e o•,jS·+ξνψΖD +ρT !xHΎqTyΊVΰYƒq}š ϊQ”[i™ΖTNHm…ž&S6ΨΝAΟ.‰3³ι€/Œ:iΆMΌω8A|άΪΊε—Ν«ΏΫψμψδ½&½€Ων$ϊΜςe덛έήaFϊaZΊu›J•ωυͺˆΕ¨ψΖi­8Ά$ ρςο²wαƒΗlQzψMΙΊbΏ Oρη›²•εœη½’?“'ώ³Eώ}\kGyΙ^t +°θŽf.Y Z— +‘_< Pχ[T»2aϊ± ’Q‹:=ΐ%Υ’–‘€J2 r½Ώ¨{υ@fd{ŸjBξIΠωœ.·g€8ΣUέΛδΚ‘Α9­QV€°œ—°ŸEΥ=†wO¦€K9Εο”²Šmω1 fkΪΈJ)‘MŸν‚vUŒ`?MΔ4Η~‹d†ΓΓΫ™h-~ΰ ξ,#{ρl0‘2¦7ς•ΤΪPwj*ΐˆ¨‰ˆη,`ΐ–ΓΧAι­}FG²ό)Ήϊέ«ͺlŸfδnμ³²-uΦij)™ΝŽ-†Ÿ·Ά/€W!f)8lt·8ΝάOΏ±ί―3‹εΧ΄ΟγΝΪΖ 6 7υ5ΘoœbξgΉΥη+r.KζΑ™?φSC†Oš“S’Ίd4Vž‹Δ°ŸδΨΈlΜ$_ fέις‰θΤ'ΚχsΉσΥΗt5b;₯2hΗ ” vR‘τž s?OnΨ9"b +ΛήτtiΌ2Υ3_s:œgDjNC¬ίΞr0α•δ~Y[4βό°“WΚπΥ ›0β,«$_ϋa₯ρ«χ/GdΣ+\/H©βVΣδ"€š££‡π³ξ’ βά+χώ>ϋ4Ω…uΣ]D ™²*Ζ‡ό ϊ`ε±CΣΡ£Zj[0ΨΤržΤ£υΆ=™Y +ϟΈN φs”΅ΌΠyƒχξjuΑ+Ώ±~νΝ‹·ς2I³]Z!¬{Άϊž‚—”N=@4ΨHή³˜όψ½YΔΞTV0.ˏՆj―ֈ΅s°!c$‘/f’2ώ'`EKiΌγ4†Su‹ΟΠw5;²…•ι–VW³ΫR8΄θDτ‘_@τ₯ϋκ’ο-BΙιΠ +Ήύ0X±”M³Ξ?Ζ1 ›ή‹§»’X9@²½Ά;Ϊέ^μ±:hmτχ"&©'¨ΘaΕC ,£ά‘1’)Ό2ήLeϊήF’ZΪU#”mΖoβ+;΄ΞΊ.όL@!GuίθN(ΥΊ·1­{ ₯^Ρ!9ATIΧ㊑ΐΌs‘'|΅Eϋυ+Ώσ˜rκO‘;{ΪyŸΪω )’˜%ΝΟi­)σR•^£ΠΨCωΨΛΕΠ’x;#Ÿ―DlvΙS“Nuνυ +‘Ζ¨άK₯ρT°κ:`dΞS5½qߊ'Ÿ΅ΨΆƒB‘—AM°eο^]|*§Y΅9™θΡrωA›%βUϊRνƒ@ Ÿα šr,’s™Σ ·ι+ώΏ8o­tĈNjϋC;ΞΣ3ΑGΜΠL΅9Ή,Σ +N§†PΪΞB©ΈžΒ½",“¨Nnμ#ΐž2ˆMyŠŠ‚ψ1˜Θ[ΰƒ†?€4A›ϋ%YOΩ βφ aj£a¦Έ‘ΞΪεώ”MΡ’˜DέDΰο©jΖύ—Υ܎wψey'VXS‘—ϊΗ…’¨‰β^ΕQΓΛΕ F]>ύ—ό» ηϋΓΤZ¦[ύQ„D6/΅ΡuxœB^.Žηΰ/LrτO^…±Š +¬•wv9Αc6rͺψ½δIτ>J[ iθ$qΈ₯ƒ"΅‚_όψ +Bψ$ξ7iΪ%΄v°3K9MŒNA˜ΰxδΈΥΣΣ€ΏΠFdcΗߞ^·ϐX‚½{O·kΟχŠ₯™ξ^ίόό…Y„X²nEΦυyS©βπ”t"μγΕ6πl]žΎν,ΝζΩψφ5.©ΒJ‰X ΛUβ΄«°ό«3ΌΣΓ±ψqE(—BΘlVΓ΄θƒn%¨S‚žυi9φ__ͺ5S,³³ΞOεD:1§νΆš`χγ‹UYqΊΘV<θšgΦκΩεΟqk‡Ώ#Αs ™k%mχeωΏKωΈχ(ΫΆω9™jN‡xΧ6Κ“]°)πšΖ»Φ Ψ΄πlšxΐοζ5UƘ>―Τ›xΜ!*|~πΛfCͺι+ω–ο E&u'/2*"4*Ώ‹Ύ-Ϋq>γή[ΧΡ$vψυΛaξvc΅2Fπ!ΔμΎ`Ήώώƒ‰³qψΜ²Yω©ž.oaςU*‰4Ί+eLΊ₯οΟεΔΕ?Γ1Ζ\9»ΗZSπ˜U¦M ς6œOˆΗ–Ζcm¦β™3Φύ8s:y-IσΎώ3r`Δ΅;”:Ά›΄.$RύyΡΧAα1ξΑΖ<Π²\¨B H_Χ+ΓΎO'ιφY³&ζ‘#TΊCΙ|*ϋ‘›ΑYη_`™ZqέΟο~*2ΧΌίz±Jω-jαό_όθ’±fα i|AKλΘ”€WΙ .ο!³˜€s@CCFF(­Ϊn–:τΗτ»zμ-ΐk$Σ<8”:Ι«HmΕ1e:mΦΏC*¦ζ—Σoe)φ>EYΰbš„?Ήχύl£'Ž"Ÿ’έpk–ψιj“pΉ…ΜSξ*Δt {}1V>™1tEB24Jcξ d#dΑΜi Ε½i_ bD Μu?©‚eΨgVT―»>ΈEIu/¦”ΞWΑC±άzΆB‰½―јχ±nd“πKίkq η{Ԏ”Ε6μ8…Ιbϋ–·S>Τ‘ƒ¦QiΈ2SŸͺVii—{pεΥD¬κš˜ U°!ήI°.B”©ΔDlcτ ;+έ]φ8ΚΉ“§VωKaq Φ78™čΌ3²θ +FΥ¬SžΟ(š‘Χλω/ιΉνζβ%fSvEΏ…> + +Μ‰ΗνM7ΘΌΜ„˜pρqΫ’]^H~έυu‚LΆWδΡhβη δOFEϊuh΅pΥ[ X.('Χ2jδ?τ%twU‹3l\γLvόΘΰΏ– ?NK•˜·$ή’ΌΘ‘SZ«"¨|HέkyLjΰ>ωόvžVΡλΑΦρ½“]?{ΩώžύιŠς—`Γοϋζ_ΊQΕdΩΣ}`― oΠπW ­Μφ»<²­₯?ž”vΗKh‘KFvδJMn1αΓ8ˆ6.ήή7큷ϼ>³™=<iw%ΰšTƒE‹’ρΔΧƒΒJΣ7HeUo~η―IpαηŽS*Ε6ΖRY8I¦E]Έz°ΰϊ²ί‰1χ§ΌφBŒ5ˆ<•ΣŒϋβχΞΏύ ±ίnώ2]ΊΚ Φ N3ΊΨωΞΤbηε&FΕ76ϋc€Ρ +‘לπt–μ8Κ•³C’xHΩΞ@K©0ςCΜ6fh‘„$ˆ₯?h Ηڝ%±­2ΏšΠ―ΤΊ;ω8Αq1ρo±F›ΐ~ Νη'\ΗJbθP4@ΟG Ύ +iΛ‘:?bαι6-Ξ“ϋ.Ÿι ·(ξ·„¬Ν©c)^Σ‚₯±LΉϋp%νΆώΫΖ7ϋοΦ$1•NλΊΧ1WΑhΕCcMHΕJ”uIt<ލڛBΩΝ †sφ2‘Ε<@€\•τۚQ1Ÿa‘mμ ΪLΧzΫωz໎ΒC§θ?-ύ_αA«&A₯?>+%Ώ‹I™ήώϊ>[άτ\[c€Š!MΎγε_EyΙw½‡ρ–WKUmμE?…ξ# ΔQλγΐ)ή\b*@¬­Mh±Εnh Ζήr@Js19α?‹·¬œά²£ηβ βΘCF€zΐyέ ΔΥ΄³lίL={Β-kˆšo6°d4½ŠL0œ’ρ§Οήu.—Ή νŽ‘e€ +ƒiΛu€·ͺ€2cF—z“'σfμνσΨBN WeT…1‹;khΌ­οjγ§”΅δ֘ΜχXΞτ…ζΦ_KI}‹mΏΔΐ}ΤΦ‚—cΐ©+ν cͺΖTύΤO 4Ρ +.qέ΅ύEζρ/V—Pϊ€ ‘t|πΟA‡Δl`q«κΡ2Θ₯ΰj’w½0\@ 3#uγΰΙǍή+°ΉΌeS(Px#΅?Α“E&άμώϊcM“ގΛn–« ςΟ°uz»ψώ/Β‰ ‚§‚Ύήυ+ΤD”;€ Δ*3ρuŒΉ0$Έpλ«΄―ΔΟ‘TK ϊNG΄(!ΞΫ„-šˆΛΒ’€‘η―?‡qτ$I€ Γάε{a%ŸgJΨ¦b~39^Ž„Fe.)žΰQ+ UΞξ^ΛYόΒažY.hηTγΔ¦`²Λ@y‰ι cΙ™ψEΥB!8UŒ¦±νζ"]Ά·#=ι†fq(<ι,h’qΓGyΥy±ΚΎf= +Υc.°³)Cή‰ς<|=ψSΘBβυtpوDtς„Ά~f½jι,λi·ŒdΪ §‡ίωΑΕA¦Sο₯ ·ψFšš&dΜ–©p σ`ΑχE™ηιwΎρνΖΓΡϋmθSͺΡρsJ’ΈR¬"1ΉRF_`(— ΰ>υνf%ΗΛ(DTbͺ}HςΘzΥ¬ωΘ[ρGAyͺΒ.ΘΒόέWpψM6Zβj—IœY¦™Qτ.ϋ =οίu°HZ¨ŠTGΐ–Ÿζ6ζ+|3›!δ<š8J*•₯—yMό·n (~Lό. %qΘΙq怄 +sΚΎνΎ`ς―\m:zxj€DΈ5hC˜ίΞQ8Έ4€Cœ@]όB¦KΞ’#Ή“‚υκάbŽκΣΨ—υ”έU„:(¬6u=Π+6 ŽœQ}F 67ψ·£ͺκϊkΔhS#ˆ‡ϋ>BŸvγΖƒL^_ψρΏ],y}8θ΅qξχœovˆ―ՁΰΟzΡΤ‚nζ·€X0χžΚ|Uσ‚τΘem§CΥz3shήAr~TΈ’MΪ`’Hϋp –:Z=Vňu œ LV„:Σ«[™Q‘όeB=‘Ωh,2u2j―Ήu”ŽP©ͺEΰζI‰΅ωόρΧ'›ϋςΔB@αl%wϊ/ΕΆ½Rfξ.κ±eN *νΔΰ΅L/9*·σ(ΌΑ‘šVΰΰΥoό’Τ4ύ—‹˜r #Υ±ιn Ι—˜jw&7Ɛr)Ž£Ακ¨’3HΊrνΞθΫ}N‚2Τ;aX ²]VVwTΠ― !zΦΩΨψΘ™Οα½ »Θυ$]48Σ­XΚ„R:»ΠΔ{Or)$qζŠv¨Έ!Aζ„Ÿ +ς?Γ‘xͺ~žκŽͺwG;μПΫw£\Rkά6ΥԞύΙjψΤNΦυϊF$ηDΖ¦Kζ’ΰ&E8ο-άΑΗ’ψ±Ύ(^άεΒβpΟΝνΨΌAΫv@dBێé½@7ΜΪڟςSN0ι^&"Pρ‘^Πμjρώξϋ’qLzσu³»“Β«žD+•‰ρψho6.ΚΒ`κg[|«eb\d|WίΤzEGλΆν‘Σime”υdXtψΓί«ŸΔ-„. + ηd‡³‹ΚΦQΊΦA—{$ Σ(wίττEρα›Σ‘·iŸ‹Β–ρΐ₯ο…ƒJO!x7~o£˜c]Ρϊe"€žΖ">‡Ϊž€&’Ÿυ˜ΝaΞ"bWϋ΅…‘d›&˜HLAρgή‡ψπ΄k6SoτΈG†Ξ}0£‘€³WΜήIxnbgΩΈu%GΛ%Bξ8œ%2{¬’ϋΫ‘*“87>·{B>B€,όΣμυmέ:|ξ +πτEο•΅mΖ_£VεώmΡ –˜¨ΦgiΧBjΠGΧέΟ~ξ ƒTληΗ+ …‘\Cœ΄˜Ξ@fι ¬»3CX!ξμΣEƒ3­ —ώVΞ…Ω{ei’R΄4FΨZθοˆ4Cp ‘πβ~P1 .Δξ'DάόH°FlΨ¨vF$ΰινοΚ|} +]„9}]Όƒq#Δ›ešΑηΨ[ξΨΉΜŸ#™Μž2 ιΪΓ@kχ”f6o“ΟzoΑωΕY$ίΣpqbΕ$C-―βτΪΣξ Ά@g$Β‰$ΊΚάώ{\Θ!L_―•ξu:J>§Ϊ5Vpψljϊ0™k­T«“«n𠎠6 ˜PϊZΫ+·¬K˜<£‡°/ΗzΖ΄#ŒγήβF₯‘ΐ)€$Φ‹^€XΪj0°Ϋ†šε΄;UΰΧφ«t­< IγΦI.‡xΓYq™jβύ& +š'γΨ-κbέΘθWTvψΐΥ*dAΥ(λ嬓WΧd\p]›Pφ’&o%…™έz;o±Ε g+5S}-Ηο΄ΐp2I5θgTRΪSv¦§ny*—BΩbΔα,= ήXστeŒνϊS—ΚΧπύμyw«?Ÿ₯ΩΖQ‚θ5₯ημσu-ώuΞ¨°½„φ’ΨΓμD’) ―Idεέ]ΪΆ@ΩρρΌϋΎ%?ZκR’…ΖQΡ–²>Ψ·Šΰς^Π˘ύGJΛ*4Κϋ'#Ό—΄AY¬ρtNA₯';όίdŸΨ|I;GΆ  ͺ{]Ο΅cαԞ0ŽmΪQίv[σΝ™;d•α»w?ωΜγy‘Ηu™S9έβPe’ŸκΪΨf ͺήΝBΉU ο.½© ֞E¦vΚJaΘ59[h“]νš‹nZ–:’ωŠJ«,ήδξΌZφŽšxw΅ +ά[έ7E4hΟ2Ά‚Χr(χFj2ώ]ηhš4·©δ;2“άmQ>R’΍y·‚etdKΖςΔ£$ƒhΏ±hBΦwzEΧ§τΛΤΊ…§ έγ«»]>žkΊmXϊ¬ 5ΖϊHΩ=Ό1!/Χ½ξWaDκFKΚ!*‘=Χγh›+ f[Ίπ‰―=F/’ΡΜρ |*Θ(賌§ζtδ£ d¬ŒΩ‘œΑ?(8π’’ΨJ’Qƒt”ͺ%€•—ρθ²iΝ ±mqi{Ξ¬/ŸΆ FŸβBσg w㚍u§υ€Ν?؊R·κnφ“yνΓΉΆM]nώuΥ)©’zΌΦή5CшΕ-oΈz|Ϋ[α +Ε‰ '&€μUβ šποχ₯ˆ y+ΗF¦ q‡mŽ‘S»Y>|¦xšEΐ9T·HlqΤg 8₯yW§NFK~$Žu.Μ*8΄0£ΨL°ŽΗ4ΔΈ΅Z"žhMcσIΙO…_ξγk}ͺΰxΦΌά"eλiŸύ½[φ2ΑδΟ`ΆSφ‘l’„ΣhψΐŸπneμృ–]ΌKσBύˆ·7!θΰψ»Ίπ / VBςΆOφ{V©jΏύ†€ΪΨEσέ@±θω±•σΉχ3ϊ0cϊ aξ6Δε:ΣaΒμžVsΐv· ΆΦ·Ÿ ZepmLδ›i΄L4‚i=™0’Ά\‚jK΅ nΘΉV1—λι +Q|ΊDζαyΡΧΊόkώ¨ £J'”ΨlΦG1\9œ‹έ<‹έyΛ«5Χ>{€u@:œβθ>i@’ωe”‡ έΰKα£F3!¨4V3NmCώ±k–—^δ4ωtH_ώUτθή»A›wžχ£ΓΐψJT:δpόgφV-Ωΰpξω\]ΔΤ3₯PΈͺ”‡πhXψ&NtOXϋcΜΠΠ‘ X"žΔiΐ`ΤtΤγr―ΝξΑN€α…P°ΊΖͺόHώiVυhͺ[F΅ησ©ά‚μ1•2Qš·ξ&r·γϊ䱐‘Ξ© wtžtΟΡxV;υeiϋ΅ΦΔέ\NU―yEΜΤΣ$Ω•ξΘΩn"ζ‘“rP1ΜTαu]©ŒmΞ1oD™Ιεk—†– +dIΟΪ”Μ„φE7†UE¬ΕRΜΐ‡"ژ \Χ` IpxŽŒ—³Ž€H«·)Q±ωΟ"˜<ΩΓ±αήB›tBπ° 10±Κ―ZΚ΅Ο»ƒ½9…ω%iυ^΅»Qι .©ΕΰRΜ=Œ­ωWλXŠy J«Φ†ΰΟίόNbς4–ύ.‚BVα†8Œƒ™«!‰Οp’Ξ¦;Š“αλ9ωQJmςGϋ™+9\όŸ+betƎΜμr{Ž"\¦ŠM!g‘R―‹θ—μΚΠx7ΥζΒή0ΌpΥV]Ζ(ŠSΦ ₯)eAT©%ιί sCβdίϋάΙά:Θτd¨Έe :βCΠ4ΚbEUύΥοΘ]o±ΎtɊί9Ι°U’`V¦a6WύvoSTKQΪΰ2_Ζ†”Πb@2F;Ϋ.aΡ»[›°wpεξΏεz,C οτƏ,ξΈπΙ>ΥΞm(ΔΔTΦΤα%Pξόr‘xš?‚“`fŽ!Β^s’ˆ¦+1_νY§Ιš”„Η5ξ{Χ¦AΚaΥ# ν65Φτ̈@εΈyKT.N`"2Θ—ζυ=†ΰΩB!:XRΩ’~ΎΜgw ρ ύβ`αH€WΓΠζυιuˆ‘©¬ΔΕF£PKŒ₯”0…pύ‰zΥΉ%ek₯†ώΣގ\¦3ΰ@ΊΆnΈ,%,ΑŒιΏΧ‡ι¨ΆΘ/–K)ιQάώ%Βλ ±©cϋR|Ϊ±²jψ/Ιξ$Ψ:L_ΑxK‘λ‘<}ς‹Υ‡«cξdlH…‡H ­ςw1lΘΓΐ+˜Ορ†Αa@ρη Υ™x4¦ΐμΛ^ζl"·2ΫB)§/][Mο°λίόΰ΄!Κd&Ÿ>Άώ–₯] `ΤE]…θrv‚”2[@ύόσ« 'ϋmΣΩq|ωΜΙ5E‹ςλ?Œμν¨%Ε=¬—4 B»ΜπHaŽ?›ν4ς žϊ&+ω6++γ#ATιύκŠ<ήϋ ‰…α-‘w“3)δώ8άPs#,FΫΧ{ν€€«=Σ=ƒ‚ίπόμ)wΘX“ +°~K–κσεΙϊ²ΘέθUljύ+g&,x7βXd¬έw*φ5ύ) OΝ‡5=yΰzo&‚!)P…DβΛaΘEθ?›ˆ™^—ί‘6uμ •fωˆ°^ΨTΝR'IVϊΉ²θ‘šα΄ύ^nŸ-5ΗOd0oώΚ€­™τ}‘82ζΔ ‚λ‹3Άt©`Χηί2ΪόU\R†ΤΔ€v=Θ~^vXλ ŸRxξbκμΘ4ώΡ.7O γηšΖ΄lk‡ CˆΡ:€Q‹τ\m°ΔΟωψ›œ<š ͺ4²γ“A·ΛqŌfŒ‰,%°υΏ‰8bhGσΔ7,$ΨΜ΄τΔξKS^)Λn^$t/(»x‹ΐtκεBνθ((ς]Μ’”Mγ0½ΎvIž#QοςuW‚{ «bίDΐ‹Ύ\š–σ\_3XB>+’»+1¬$π,<α7δΟέρ²'―7ُύeDΓ”5”'h±)θ­…gLΟτΞO?ƒsE+λ2Fg3€.Ώ¬§ΧΊψν’¨Έ}^ή€νŠE—QWœ$q΅xε€€O&f›oB¦KAνϊ}1—Ύ?–½ιΘ‚9ŸΙ(«½¨F + ]lX%ίxκό6{αό:ΝόΔ‰/σΏΐ‰u©˜‰n₯y§ω¨S°(RΙΘΟ°,gτ)š¦ψ;t?•όHΝnΈ@Υώ%ύυλ +tmΪ₯Θ·Ο¬nώ‘$𐈞΅|”ρ„ŽvIΧ“«Ÿb$Ύ‹¦&Μυ²ΩŎέkδl%žα€ΖΎdO¨˜)\rΈΌπUx›#„pEΗ0ΉΡβ.ώ!ψνΈΫ‘ΏBΥQΓ*aT$a9/αω†“ΤHF|Sx1‚@(¨ͺ«0ΙJC³£Ι9lΜdIo·&ω?•ΒΧή€ά‚u˜χ-ψirηm”0Ϊمލyta‘Α;n7tΡb₯.ŒψνuΪ’δ€n] 5·Ω§]_gΡawυP޽k‘Δ‹ΉcHέΤ±ω.<δwFJδτϋξ%hνA‹ŸΝ©¦ΗΓΈHkHH_]τγΛΦ‘]Ρ»›%(ω^Α‹n§»Γ²1‘5‹JN³ŠPΙ%υƒΰ+ΉJQυΕΫΉz¦δŸΎ ¬ϋΜ¬.yTe&εψ·„ΧΏ•˜λo› o 4>co`dzΖΘ§n 8©πwγ‡_ˆ―/…5\$ ΠwkB·₯E<—„Ω#·Mn*“@jΞΰνύvβHυ`μ‘ώB{φ +ωΓ―ά‘ύΕΨCΩιπWςΑyφKχڜi†1Ότϋ*e1 ›”5χΟŸ»²·†ŒΈΛΒLNχη8―ƒl rΉKβ*άo σ Ε¦8˜ hϋ¨J!Vœ “ά:WΙζΔΞ†G-6 sY}GAcί|Β–υΉ“"…’ΉάΗ³X/΅&VU#ΦΨ:FAτ6ra9ΈΗweߟ“O-ΖυlΨΜ!LjΌ_ΏY‘}Z|Σ"ΏŸCcpΕ’3ώΆ[Ϊ=pΞι cΣa§dψyυDX +’=“e „ο"ϊθ€»άέ„Š±IFXA{Vζ"²ζs˜`wwΰψ$Ζr\b iΗ)Φχwφšώ[ΗϋΈ‰’ΨŽΕΉ6›TgΠΑΞΩM\YσΕƒξ;PгφΖΖ³DΥώv„EUFμνzΖ*‰Υδm(KF‹^ƒ–CΙgiŒ9οWΦ·v&ŠνΉΖΫΐ³²Ίω1='Q~#[σh#ΞRξ’mΔτΫv.‡γζ2P±π  +«η +Ε­γ‘ΡHΖ$6Ίͺ΅η€ +€έά'^TΔ’Gό°Bgf:ύΞ»ΙΠΊΚ›Šε’ ΰΰ™Œα„ΘΜτfŠρ«ίν<όΠMœψ^ΐ|(CπΝYΫ}Λ½ΝIΗA~ώ=ˆ|δ˜T"/ LLuŒ5ϋ=cάǍAW‘―ε@ h—P"» Kͺ FE,εWOΨ$xm:ˆut°%ρ7™Xχ<Κd‰P’Ό2™ε©ΊιRτ‰)Ψεwΰ*)ΠVŽ:°-ȐϋvΗ/L}pόΥIRMAیΝαΐn›–«'8ΦKΆσdjD~vιh'Ž\XΆ½)ΚίHQΙ=›F«¦o¬;©jbΠNψοΦͺςΑΐ(‰ ’Υ±”„” τ6γŠ,>Qαœ<¨UŠο7H`ε$tv#)ο9―υph‘Ν4Ε΄ΞύA‚ρβύ n¦ΝΫ,ώ†:ΑΖ€›wl_{Εε?΅΄"Vή―e7Οڞ–θͺH)Lπ½ͺ‘/q‚ό*°‡ω+p’gƒ Š‹~M‹}- }61/6τpΩΨ΄δ 4Ÿ΄άC·w3Ύ‚%IsrΜ§‰«…šΞπL)&gώqU]Υρ(n0HΝεN–`€f΄— Ρς_Š•}ϋν΄Εμz¦Πσ +ƒp9…Y»U+£Ήs*“ƒf»εc#ΉLΆ'X‡ς!*ˆχάα³ΒKΛ‚ρNΧ@¨ΉB9σ™œαqΨ#'e(' +ͺՊ ΠόT¨ΪΩ·؝£ΑπάyŒ! Οd°:OΤxg‘¬#jRtnE΅ύ=Ϋ]αo`3ΑάΜ-±τŠIΖγ±Υχ,hάΉDΉz†ΦJ^5²RtΠ•Ιl΅:MΖ’΄UΥ$‘!Ÿ…Ÿ΄έ|±―„ΨeωΞε°'3Š'pUόN~μΜθWγS7ΫΥ(R$f‡»xς.]`L°ς’ό0’'±•ωάsXοa.Υf~PmιΫΜ„Ξ#H\ςC6;J“&κΜΞߎ°3ΫP„G&Cœ„Cθφ+Ψ†„q‚ 8ˆ_Úί•² ³»κ$ ¦δ[YžίΩB«Η}π ‘:K,Ή]37XLtŒGJ3φ&=˜;Ž„Γjfλό G £¬Τ@yf›’ 7_λ €>;’mΊ:ί/š^ͺ;tƒχΣ `Υί£k² φ­Ÿ¨‡τλlHΓ0oVΒα6{„κΝsΕΘɚQηοΰ₯“Ÿ^AB\ηΚQ°’Λ<9ω: ©£Ζ\½X–Y„V³εΕcΥu“_υͺ0Θzxπ]ω‘τ]NβδΉrι=XBU“ΤζyΝ(Λ©z/ΙiI½¦!TNρT6³¦R6¬ΡMυΉ?Š +=·Υτ?fP ΛΈSqμ 9·ΓD—tžp™ονΪBΛ θ«­L!ͺ‘νΊο™₯²8z’ΰtBW‚ΰΡΩY‰©ΙPQ1NŸdjΓ₯…Ί£¦ΈΨβ ΩκrΞγήΏv38&ιχλ:aΣ…†ϊˆU‚»xZδjPš€β…ξ/˜]uvx#μLν{γpM}ώέ +Ϊάƒ§ Μξ‹―ˆAΌ7‘£sΚ`‚ιWUgu„JŠ#Αν‘0pξΘ7I₯η„ψ”­FVΗ«6ι=¦ψ‘΄ D.LxŠɜ9ŸδK@w¨«,ΤK»ΐ6fZ=Wkp„eα Γ“_ŒρJγXQzQE€οΨ†ΰ°C;Ώ+Ή` oϊj-Λ’—‚΅ν<Ίa—ϊ[²2₯YK˜–―AΦ0ν|Wj…τΈϋPš΅mˆoτλ §77ΆΖΜγ’sA“„yUmέΒΠΩ&ŸήBvν–=E«εάs©τjšCh°0…2@Cp»QΑO[¦Γέ %‡½@ Ξi$*γ +#3Pםyξώ™E.τ36‹―[apVE2eΞ4ˆ|Λ‡fp/έYΉΊS₯K­;JΕΌ›νΪ">,ξ­gλv Τ|Aϋ‘Α + —H'Iό΄lνvΖ•>υ|\ ΄–ΟΡl™_`kΣψ‰z ΥΧ2₯ʊόS ¦ΛgϋύχΝEΒ·‘,ΦΟ>ϋϋs=`Γu±°a—9νοXΤΛκέΛ 5:I_ΙPΙεV³¬ρδ–•ν#OΚ'ŒφFΙΛN*Ώ¬v±0ά[>—6Ι-g²ά -ΈSZΙA*j΄τ;bφΆψ³QdΔ‘οf˜{u9ζμq‘ζιŒ„™\TΚpW`ς‡Ή.ZJ”ψχβΧ­€υƒ½dβ&ƒ;gXd6θΫt ·Ψ[ϊsq4ƒŒ‹,ΘaYŸΈ‰‰’Ιl˜μŁεžΰ+).…ώw¨‘Λz<<όΠe Or>'V봝Φ{‰ ³UP}­R€:6L^ηΟΕώGS‡Ι°±Δ…`ΤJqr!0 +”dΌ―ς}2™oΙAu;4&ndJ€]εt}·ϋg˜ν"Ώ¨iͺrϋ ψι!ΘQ—Iη™‘(±‚Ν½νΔΛ;ΖϊΙ¬ωYzβqΔΊΉ‡Βϊ^-ο³IeN¨(‘Όυevρη»NšuUΡϋΉΪ‘°„άӌ'fL"œͺΘ“19^­qEΦ!']ˎVΘ{jnρΈΰy—₯NΉγt#$Ψ™θ꽨8t6hF#Κg9\RP +η²χ\ˆriX4Š Ί@ΔGjZσιΩ„AωΏ­P+!ž^³ή³τ.τͺ‚ΒύT d4mFλiΣ1ί Γ ήA{"βδβ:`G =2¨0 }©κ˜œ>ο>3™¨&œύ_––ŒΖS“]ΣA0­=b!ͺ}³‰ΗWwmšΣKΎόBœY²λŒζΙαO’ΖΒΖyγ<ΣpoJ3T‰ΡΎΘ¨₯@ˆ>±‡ΆμϋΙ‘cΈYQΊ¬k‘’Ϊumυ΅~BݐŽOžο(Ώη<|Yμ©σX­U +μ7ΠcΒη[Rψ ?eΧ ͺ—μΫOoFXρΈΖ¬6@]RP„/•& Q°BΡηMΉΔcμŸφn;+$ΦΈWٝ˜ΨμτMX~sdέε†ϊg‹'ΞΨΝ”:z@Έ:Ί½ΐŠίcIϋ²‘μΨ5A6 '?q-^ŒΉγU‘²‘ˆ=ŸΓ!OΙΈ-4δ†ΕΧqnϋx•‡o>K‰Ρ–Δ$0#Γ\|w₯&ς g‹BΤ’cΛ?ks$@σKήΫϊαΡ°SΙ[JaΆ›ž!v€Fϊdξ9<ޱ[β•"ΰ_“&ΐ¨:MžΜΫ<&aϋ‹*…-Ζ ώhByΞ8χFΥήΚ~‡ΥKlMψ±Σd»z~δ³Ά#5EΑ;aEd‘܍*™,Γζr©o;Z«μιφ†V|©Θα½+22IΧvNH'ϊ± T-²‘v›°€hAo’αD’’ab2_±§#‰Σς†©lΟ{υu Cj^MόB$Ί—]†ώJφπ܌6±¦‘PnΧr8Z’ ΄Αrεt™ySKΝάΎšxϊ2ΖδJ!Ύ$B©?―ώΣ,7Êœdp}/zœk§^”έf|hΚι–ΛΔ°"ιΩ3άD`…Ζ]¨B°| w»α;¨‘rMΚΎώ{dFG‡•θ± Qΰ‘"¬F Z6ž‡Ργα¦ΰi©₯Y>χA6ΌϋΉΧΦψΠ‘^€I "²…φ΅ςDޚP[W +½›(s‘+N€Χΐϊvκ>γ Ωr{SωΑ―^}c‘Μ;Ο†T%5’¦iΗΒφηIU)mΌmxl‡yΝ3οΦΙ±v1ςή²ε fCdΗ½ίνv+ΌT#ώδ‡ΎΩq„ώK6X‰p~ρ)©e—&‘τeύΚ•Ι­‡‰Nq]b@1k1zηέU(Μ (yΪBϊϟ.]s‹G+9Ι ξ&χΔΕʏ΅¨mψŽε΅Ζ_€mΤI&Θ)F/"Lk+O~J@’±Ί|†‹g&‰·ΟEbr+ΞρΪ›cΔ`»#„ϊ^“”|}˜BL²Τ\ΜΕ@2υšmΖΗ…3M‘ΣάΐλΝP{Ϋιk[RN§`“Xλ7\%+dΑΚGό»ΡC'€ZθYδόm2Α± υΪρ<|ΚŸŽ’[’Α„pΊE˜–³M˜<§†«`*ο[₯½dbΧ΄Cά.gΖRπ'>½„χ§B-ψ­ŸάΊ7PΔΠs­’’Euˆ_=&r€ϋωͺβF"χφM€kΟiΏή\g¬λΨ+CBυοΕNΦέξΏ”Η‡Ϊ±…kΞUηA"ΰf—Ξ"?‚[—yΑ―θ ΜU…¬μœΆogΪμΨxβυ%ΘWΊŸrCρXρέ-ΟΌQ{ΡLw}8y_Ώ€&ΰ-YΟά1.α&4.²†πA"wηa~sjb‰U„CI–_Zi… Ά~ ˜²+lo P>m e}˜”Ήg„°‘Ζκ~~·u­ω¬Qϊι₯eŽb‚‘GΉΘΏς$G +Σ%]‹›*ά]υ{θΉ½xα«“ιh[&ΰU%«ζ6ΊΦkœή«; ΠΰtQEθΛn’ψC+ +Ι;ή,‹Gs«1υ ΏNΛeάο₯)ˆeRάƒSι―vžχω—Κͺ‹ΞΗΆθK1oFύς(δ9μǐ]Žΐ·Ρknκ΅i‰’―Šλ—>Eir)ίS΄°Kηm²τ&δ„'BžU‡œσSΜ +Δ ›2ε<&\½‰eUCΜ(˜ΩΜ °βΔjS[—’ΨΝ ιr ‰κώTe- \Ϊ†Ο2‰φe€9-ž^Έ„ς^Jš@η!ŽΞkƒˆξL__UΨΒ›¨Β‹°ΏD3³žRΖL=ΠίγΜ‰2†‰·?ΌνtœNI!ykμίΘι)^Χ—[Ι‡’oζ¬n‚Nv΅Πa¦I(=«λ‘ΟΓmsέڝφ#ΗΉξl|θ¬ή•K„Uπfo..£†χλ K ΌΚο`pΈΙvœ!₯lͺ΅—s]S#’;‰ξyudρ?iϋΰ$ r…ι©τWœŠσ9ω›PQQ0ε›·Ή/άE§xσ[ Ž_»W½ψ"^¬„ΠΓp«Ϊ’ j ΥJ†Η’ΟG#Ž„@_£ιΘ­ +χQΑn>˜l€}?’‘ΒOt…œaϋ΄yμυΠaeσŠΞ JK=Γβ'<ΡfΡ θeυiΰρŠ} oκ(Lξu…WTΥ$<φkΏŸςS[ΊΘθΙ—’ζ”± ΌΑ†:κ5ς?Έ'¬ …«Θ"*ŒΆVab§θ_‡aώ‘}jn7Ή7ŒΗ³κDLV·ωθ{Ι~|ΎΑΈLτƒK_ Τ.|~.˜Όφή΄y„vq‹½ΫO D]\O¨ߟYPcΩΣ»*&θ/'žͺύW%°]§ρol%ιλ% 1ž&ϊv$oΥ+)kcd ŽΖΠFŽŠιBjΛΔΎφ¨iθ$©jιώ²ož σΘͺ½νŠρLρ„#:’oό+½Q«k)$χ³¦GUp U[ΡrbΘ\ΣΕ’ϊ:meγ.΄’¦tsΤθ½O`/žœ:Ψ―5BM΄OθΤͺ"hD€ΠtΞ›˜{¦;Υ‹OFˆUwξš—,ӘΡKρ`fώ"ε²LάΕ-2'΄ΚȟO7<¦kJ%žZ„·jσχ0πσήpqA5t d Dω ӜŸΆ ω §ΦlΠ­Οg˜VΕ IΣ(ΐΙLX½pR B6H6ΤΩ»ΰ%@›IνϊRƒbΜz΄άν)5ςGξs½Ω,yφΙ7[Q8JΉxΘ/υΓι0IΨwΛYμ/ΞάdE),Œg½"½ΣΌ-συ³ρΏi³~ή’αkWz}Ψ`!f'h'³.ΨΜξΑΗBςr'T2²š―±ΜAνΎ]Ώg<‘M|o@e6ˆinsβΌ*.,z½£J>άI ΐ~~š¨•»}€{νfΦήμλFVΨ™YSλ.nΤ1…cΘΟ%’³*ΈS°ηp™Β­b3kσ—j‘ͺM‰œgƒΨ!’©b{έωdNQRcΤ™Θσku© z­VΡγw}£<Ž»Ύ6!υͺ 9™ω|τμ¦b―kΈu/[ϋjϋΈΙtž ?jɏlοH{ϋe+ΆgFiΰ:|!:ͺt*ΚΈώπς@Ψ«MωΧΒP k‡ς;&™…γχP‰ΌΝγ„hΆ‘ς9=ΦΤσRe`±§¬Liy~z ίʞP&κŸ‘V*0Ί! qδΗ㐉v Jρ +Όθ[LηU0Ξ-R”l*‘β\‚ελi;nρ^ΣwΩ~¬ΞρΑƒτw`νQo“…j¨v‰tοώ;―Ύk˜ρ pρ?v†'¨Ϋ;€n9Ϊώ#λ +οά|ΞK%˜ +HΔΗΣ/r£Ÿί¬oΫpΕΔIŸN8=οΜ[m©Ψ"_˜‘Šψ-g³qΐΔθy–’‹“4>ϋ OD'G±uΤ |ŠxΒD+Žd±eριΎN•—NτΡhIrΈFQŸa\‚k§ΓC‚ύ„)If2ΩΨr)soΣm§ώ§s;Θ‰/Ό-½δϋΊ€w,‰ιW"vd ΊφQmx‹jΰW’ξ›w$ξΗﴝ •9οbLKσ^UoΚo§ZχL,§Bή…Ρb:χ¨ε‘λΣύ +΄NβΘό@vkφ¦(η ΌD†Y’©ίKq,ofΊ–ς§β[έyΪ~ ;Όq± +\RύΘyυΎxΠΔΔ¨πJό³Ζž΅~½©[x»F=}:#ύΠ +EΐzΑ»Ω—_¦νΥ_½1a*•ρΎ)MFo]P7ρG¬Ύξ€O‚fYeSΣβ$ώΊ"K2nΕ~·KT–\ΰrM€ΕJΓhvˆ ©z»Φ#Π–>δσΊ ΊJxΦά•h +endstream +endobj +831 0 obj +<< /D (subsection.4.1) /S /GoTo >> +endobj +832 0 obj + +endobj +833 0 obj +<< /D (subsection.4.2) /S /GoTo >> +endobj +834 0 obj + +endobj +835 0 obj +<< /D (section.5) /S /GoTo >> +endobj +836 0 obj + +endobj +837 0 obj +<< /D (subsection.6.1) /S /GoTo >> +endobj +838 0 obj +<< /A 859 0 R /Next 841 0 R /Parent 616 0 R /Prev 731 0 R /Title 860 0 R >> +endobj +839 0 obj + +endobj +840 0 obj +<< /D (subsection.6.4) /S /GoTo >> +endobj +841 0 obj +<< /A 861 0 R /Next 732 0 R /Parent 616 0 R /Prev 838 0 R /Title 862 0 R >> +endobj +842 0 obj + +endobj +843 0 obj +<< /Filter /FlateDecode /Length1 6916 /Length 4896 >> +stream +xΝ9kTΧ™χΞhFBo βε4ˆG$ ’’a›ΧΨ‚xYΨ(Ζ`ΐ&qLΖΖΨi“Άqμ΄vΖΝc9Ng{ +l³qq›:=΅³›&έ΄ΙΙξΩ?=§΄ΫGΪ=± ϋΝH»ένώΪ;Ίσέοqούξχ}χ»wΰπΨΔ R‘D’’ώhxIEϋGφώΙΓl §"΍ξ‹Ζpω]„θλϋFŽΕp]5BͺK‘Α0ΘIψΘB Ε₯³#ΡΓGbΈφΐ‘ƒύqΎŽ<#>Ÿύpφ‘pt ”4ΟŒσq!™Rbύ­¦u"j„υH4&–a|,=° §σ·g©{u՟’t…4Ϊ;ΏΌ(Νσ³ssέw~uοCεω%`$ΐ±γΚ/έϋ!εΝ;ΏΊ£P±,š…Η6-Fn¨Δ.T\‡εΨu΅‚yw[-ΰ7  +@<φτ¬θXp@@7@ΐB€νˆG38Οΰ&£1ΖC;a$Œώ δA%\ƒŠ ‚Ϊπ…:υ¨²υkΈζ ­Α@ΙR*V)zͺ „ˆ…:ƒWI΅c›Γ`:x3PB=υΛΈψ*₯C‹ΈΠσulϊΛλΜΏ/¬3ΏψΪ!ζί>(d>|˜ωω{™ή·3?{ΏœyχVsϋV„Ήyλuζ§·fέ-ena[rγσ΍³ΜoΈ˜·Wv0?Z 1?\ΩΛ\_b~°e­Μ0hEΏΒQv₯h…›„H!Ά²+ψ­εζŸ—«˜7—Ϋ˜ο/G™Zg––Ÿ`—2-ΛxqύΪΛΣ'όŒ<ƒ|W ΦωEθY_v”ψΏ»dώq‘‡yc‘ŸΉΊ°Ÿ¦˜ο,œ`^_θf^Ύ|ˆωφεiζ₯Λg™.f2—.V2ίΈψUζΒyž9Άˆ™Γ§ω'IσE2ΐœθžαΏ0?Γο>Ζ?>ŒwÎcξc½pμφ±υcτΡξ)ώΘόΟL}yκ…)r +OσuOσΞOσ£ΣxΆϋ$jώ$ϜόςΙN’'Ι1Ύe²w’˜„Ζ#CQ^ˆβ½ΡƒΡγQ2 +”Γέcόψόολ›“ωCέωΡωƒόA3ϋA©αΐ>>2Ώ + πƒσ| zyO/~8ΠΕwΟwyšψέ Έ+ΠΑσσ|{ •o›oε[i?³3°ƒίAZ™ν ί<δ› |γ|ί@ؘ@ΐΟϋ±ΙΆ(Ξbf‰δ›βΎBDδΣ¨Γ\”―·EK·€gk»ψφ΄v τ¬€ψξΠΖ_꼚† olνŠαO>υΚ¬ +™ν‘«δ‹/fΦu…±νρHνu±@€sΟΈν―Λύ4±=―qxğoφΑΠBPE(±m³mπρĞρΓP%bL1$㝱 ν—θ0Ιa˜LœO*"4Ц η)τ4zΝ£wPΙΓθQ4‡Ύ‰ήDtΖ{Eηίη +ΐQtΞΎΣθτ+š€΅|†ϊ(:@ΩΧ„-λΏ£”λΏ–_.^=R’‘CΟ`ˆοhokmΩΉc{s°©±!ΰχΥ{λΆyά΅5Υ[«*+Κ]eŽΒ{^Ž5›³0f“A―Σ¨” +9MΙH#»σχ²BN― Λα +Dœ !|‘W`δPF`Ε~a`= ιΙ‘ΏτΔ$=›’XΟV£κ;λγXαf=Η.βΦ΄Ÿͺη:YaUjo—Ϊ² Ρ’•=XŸ9RΟ +Έ—υ ώΙȜ―·ΎΐŽTJ/ηTΨΡ‚RM΄„ΑƁbΑΆΝ °@Yυ;χ)εΉΥ_ƒΦχQΒq +mΥŠD¦ΈΔM3 8ΌΡF hλΛΚu9³θA}€3­‘Ξ’Ύτ«Θγ°u +D―ΘΉΆΑIβEΞΜg³{/–υqΎήψo2bfϊΨ;xVϊY™ψ¬@ζτφυGDœγκa…`KΤ<υΠπ„γΖτ-9@>ά ‹ΝΠά¨`βκbΦ bυ ·‡€.1ͺO0yΤΫο%8|ΠBΔ7':FTP‹k -!ηϊ' ₯lϊNTŠ:E=„d/8%Η7˜ήτˆΟ!6”ž%x:Α|\h°Sτ§ς?ι €₯^°ΆΏή†e r«‚ ιd§θ- °~xquΥΐΠ t =ZWΝ†p:ΪƒYβbλq!­ήθ Ίz³ Έ₯ς7TJ-Τ›:Ι@ κsbσόŸͺΕ€E…ςYί`ύ} +>0( ’‚ρΡώw= Ρqc€ + +ѝ β +μ΄Y`+Φ)‘D/šY΅°!nλδ †<-!Ρ9’­%Ϋ₯„δνx”t„ξΗbόŠM^Ό%ΐέ#$ψm’SEŸJx@Β7Ρ†Ώ`7n°Ω9lŸο.\|@ΔΒφΟΠ9α3ΖRΨ©~Θ’œ?Μ±zΦ?^\Ÿι›[πxζF}½‘*Ψs\γΐΧͺGJ›ώXϊ΄8΅q°£ΐ‰§nΓ³­ <ΫήZ#ΔΞv„psκ­λ\Θ^h‰EΘ#Q ‘*EVDΔ‘ΪQHςιK„f$L"Hx"F-&4Œϊ‰MΏ!GM£y$Z'Ψ^ζΨ’°}σXgd·SάY(ό?,` W»€ Z-(ΉΑ:AΕΥ‰t·HwΗθ΄H—suNƐ†}nΓVœ/Ό +WΛ€Db#ƒ«Ι΄¨κΤ¨-Pί„:υTΤT‘?U”ιFVϋbSΓύd ΰYH ίw*ΐΤHΧW|/Κ€&–g± !n δ]وμ3Š \T-u$ +"oΒ ˆ„^•h;jςdΘ‰ͺ«υΪ+)υπ˜}]!Γ£ΐ$WF“­©†F· #ΗGτρM7Tƒ±²;>ϊθγU±εpθWαωθVQ16d€jrΉ‰ζ,…DYn©Λι,©%ΚJs8‹–h₯ςZY²… A2F©%$όζέ0Ω~χOΔ²·…J©δ$₯VIΙ’LΕΫς ‘Ύ\w+'εI)δyεή,ߐΧς ­M1Ν:šΦ™†-άξ>ϋ=₯½S!;qηq2Ήz‡Γ 2Jv#%)½Π“ές°!Ι@&θΤκD…<Ρ¨Ι©λΊχΨΖqŸάhjύ·δT&$ά4μρν«›ͺ#Ξ9q’Σκ,s’2'~Ά_°cWήΉ<"AΟΘ1E>IŠ)Ϋά·Υκ²ΫHέXu£¨ˆΚΉ‘Ψ_ƒFξUχͺ±±ŠυΏ'6τ«%EΕ={z6 Ž™DVVZ+“ΠIq‰Ζ“Η¬&/-«jΪV«%Θ7Š"/7OvVηLE;'_Ά6ΉmZZF`J©PYΛw” >³§Μπ΅=μŒ~%dI«έΧόΏ3am8hρgαoμxώhsnべoχξΊrqΆΏ2AkT©Τ:₯Κ¨–kΪŽ/-ιΩT}Υ੐;”―KγLgߚpVv„€B=`«Χ)ε’r@?ς̝φ_πΏζ'O֟«Ήžτ»wΉ‰W«ρ…j<[›Άvm%^.ΔΟβ ΆΧlDΧΕε8)ÚAΘ2pjr~2‘LΖ.hvkˆ +NUδ+•—8™Θ!ŠΐŠ’Ϋ¦ΌΫ[tά<ʊΫ,쐺ΫΚNWφNM Up#΅ίwΏΙ!jυ―ΎΧf‡¨…—ΈEΕχΩ~Σ R#ξ ˆάZ’Ό£’Cδ[HRrΈg ‘βr%–«%“LΙΞωϊΦƒ/ν}>ΊΥή:ΡΈ­ίk)άσ•ώžΣ]φμϊ~Οφ©–|!ΛΣ³υθqΖφŽ?V”³ύπφ†?χά³O?‹λێο.Θo™hφ ‡‚ΫΠΦSζΫνt΄μ―r…w³¬AΎ—8T,ˈξ΅5yέYOέ;οl x­œ·‘₯ψΐΤ”»-ΰ·ΐ‰Θ†&=όpήΡeΐ΄+4xŸzJM,*p‚ΛαΉ“&Σu1»²Χ•φτ%]»u‰Ϊ%Εrά€Ζπ_™O΄D­LŒZΡb΅2ς­ΒWŽOΌ4XPΈ_„C3:ΆΨ*οΤgΆ4n―λσ¦)‰Σ_ωγB?ꟿυΜ§ΌΝΗ{j2{ž{Χ>8γ}¨92= š½ axŽJ†όζπ€‘ό•-Œ›Vι!'%ίδΪςυKt(]βΎχ6vτάμωaσήΝ’b«ΈΓ²Δ\%z8Λ@ΡtNŽδtklΏ%€άtŽVͺι{£΄JIS j9fΦ~£7©e fr‘J­^e0kι?Ι5 T‰6QMQκD­Ϊ€‘Λr^Q’Κ½&’ΩΫ„ŒΔ2ZIίΩ/ט@οΠϋ xͺuxŠs‹1UœTL(N§^H%Ž€β#‰8ω:$«UώHe¨¨dΩΚτeg2]Ψ―\Ž/ByΥP)F4v¬ήŒ₯dpH˜VX\9…0Ž%xΪ₯7CZž%&δ3T‚6αž/A―¦ Z !Π?Q›ΘTwVΉ‚%jZ)' ™<₯Όc¨ΌϋT—=ΙχDτΧD‹V‘N)hM²Ncλ°9rω‹#EE»·εr6ŽΦ˜4j½Fm²p©‡zk}F³\+Dκγ°ώzΣqrχΉέΔΔn<Ή Λwα©–S-Dy3Ά6cY³©™8_ŠO›/˜‰‹ϋΏψϊŽφœžœ;r<€6ύ:vn5l5$»–ΫνΛ,K5Ά'/ΗΒTΜΈ°έ ’mb™7Z8Ώώ*Δw=]η$vtΙ6©MΚζiVϋΉ“ 1<^>ς­ύžΡέUZ8©4ͺ„ςέγώ†‘&kQΧ‰ΦZ£’ΣJβP‹-³rwUΝΐφb₯dWZa¬hlν|bΧCͺ½‡Ϊ8Ϊqv¨"y «VλRτ[’-y–ίފκ°?lͺΣ$jθœ@MAK΅%Ϋ–-S'T­Ζ˜m1uŒ{k΄W( Ί΄- +ώΊAώΎ‚κqΜfΰጣDCΜΐ…Yϊ•\ ΛΔΏ₯*PΑυdGnzα’Μ–₯Χ+³Ϊ”Έε” –Δέ•VΚ2Έ€ΐ"OΥ?v%R=Q¦₯d)‡°5Eƒι.§½λτΓn>;ΝΜ€ΉβήJ4Έ`ΩΨkΛρΕ}/>R™˜šͺIο«MZy“Ζ5O΄ϋ‡ύMj6Ύ›ΑΐΖ€°kCQή7‹ΦΧ7n2 vm5±π*؊D<Ν°–5s\ε8 ΄•² ˜ ^…­N­™)΅1-1%UIRψξ½ac"₯Τ$ΏαfφsuRR’ϊΞ j‚€Υz•lšMΡ€N4lm˜7~+€yΕC¨|tφzͺCQOΣΙ +aN*σπξ5ŽPqX1gyήB$XR-ωr6ν|‘Ÿ†3‹Α_Z΄υΊΥλYbYΉkι‘vσ’> +stream +xε›y|ΣUΆΐΟύύ²§i’ξ{Rt‘KΊQZZhθFK)΄΄Ρ”΅₯,Eΐv +e_ͺŽΒT—AGΐeάͺςk@ ‚‚Š:*¨γΰ2ΰΰ#PqΫΌs'-Λ,oζ½χyοr~ί{Ξ=χώξ=ηήϋKb]Ρ9τΠ"dΆ,jnω•€ΘlYΊΔJzD€jΓάφy‹H;  ž·pΕ\“έϋ[η4Ο&~Fζ΅’t–‹Lh]΄d9ιI sΆ΅ψλ“±ˆYΤΌάψuλ΅Ν‹ζˆdr{Η=Γϋ…§ΊreXg€I ”}0šp€?²bΩΒλU99·ήj<0ΣXτDjdσΎ―Vζ…wοκnΉπa]”¦hA«Λ ήΦ!€nΗ…Οί€‹β–Λ^Vόjl¬XŠ5…βXΌή,fΒ½((DΜFY‚rE!¦‹Γ!,bšŸ©βpOΎ%αͺ‘μF}ΡhKͺΨ+b¬c[Δ"Θ Α%ŽB σ‘#‘yΘΘ\d††ŒGZΑ©’G΄€_ΕΡT‡Z!ΪΔ,h@δR_;‡T@ˆ˜e('PDuϊe ς—(w E9‡’Α‘ΓsρŽ ΫZΡۊν­ΨΏηnΕVP ?yβb-^αGO\*βO\β{Βw„sTχ-iίΎ&œ%œ!ό•<ϋ§Ιψα/„S„“„/ &|A8α‰Σβ ώDΪη„γžΨ 4σΔF">σΔ:Ÿ>!|Lψˆ\>$폄οή#ΌK8JψαΒο oή"ΌIƒ8B8Lxƒπ:έφ5ςόαUΒ+„— ‡/^$Ό@8H8@}>OxŽŒϋ ϋΟφΌ„g{Ovv<„^OL6FP"μτΔδ φαIΒ„Β㞘,tyŒπ(΅{„π0α·„‡ ζχvΆΆξ#ό†ΊΎ—p5ίJψ5αnΒ]„;©έ„-„Ϋ ·n%l&άB]o’ζ 7Ί Ώ"l λ 7n$ό’pαzOt.Ζε:Baa-a a5aa%aa9aa)‘“°„°˜ΠAψ‘Π扁ƒΈ–°ˆ°°€p a>‘•00—0‡0›ΠB˜Eh&4ff¦¦¦¦=‘#qdnΒΥ„«.B‘ž0™PG¨%L"L$Τ&ͺ γ U„JΒ8B‘œPF(%”Ζœ„bΒΒhB‘0ŠPΰ‰(ΐωεFς#Ή„B6!‹)Cdžˆ μΕAΖ B:!JNH!$’‰»'Ό;K Ψ<α|£σ„BΔ“ΡJ°β±„B4!ŠIˆ „Β‘t‡ΊC0ƒf‚‰`$ „‚ž #h©O AMFAIPD‚@`Α|„B?αgΒΒyΒO„ ?Θ·eίΛ3bί‘ρα[Β7„― g g%τNΎ"ό…pŠp’π%έοϞ0›ΕΛΎ œπ„αΞa"|ξ ΛGν8α˜'¬΅Οφ„•£ρ#OXβCΒ PΧοή£Ξή₯Ύώ@x‡:ϋ=΅{›παMΒΒaΒΤξuκϊ5Βοhπ―^‘ϋ½μ +Α‘’/э^€QΏ@$ ο€ζwΆn§!έFž·’ηfΒ-žΠ:l·‰<7n&t{BάXχ+OH#bƒ'db½'d:β&OΘx螐©ˆ_Rέ δy=Ή\ηά‰gε–3•–c-/’Ό€rε€ώ*‹₯EBΩ‰ςΚ“(O τ <ŽςΚ£( <Œς[”‡PDyε~”(ΫQΆιZ-χ lEω5Κέ(w‘ά‰rΚ”ΫQnCΉUΫjٌr Κ&”(c΅ΒΟΒyΈ +,Βd+XΨ:O0™l­'ˆoΐ%„Ε3_΅„_Ϊ m„k ‹ ΧŠ…οl‘€OIΘ#Œ δrΩ °—e2 A3ΑD0 &ΕΛz‚Ž %hj§Z圊ό+JΚi”―Pώ‚r +ΣωΚ§(Ÿ |ŒςΚ‡(Δ΄|€ς>Κσ(Ο‘μGΩ‡ς,Κ}˜Šί xYEz₯ΗΜ7Η + +ΞrΒ2ΒRB'‘”PBqKpŠ c£iΚ‘„B0Η^QΣςΠσ’€_ξ8„"Š@cYE¨§¬O¦‘Υj “ 5„ „jΒxB‘’0ŽPA('”†βiπV‚…Gˆ%Δ’ Q„HBM3œζΌ§Ϋς3Κ”σ(?αψε”οQΎC9‡ς-fυ”―QΎDω3Κ('Pώ„ς9ΚqΜξ”Γ(o ΌŽςΚοP^Eyεe”C(/‘xQžΑŒοAye7Κ.”{yφ…~ŠρΒjΒ|? +±VΒ< +Λ\ΒΒlB a‘™ΠD˜I˜A˜N˜F˜J˜Bh$Έ W"Έ !ƒBNH#€†RΙ„$B"ΑNΉI ΨJ‚‚ £ Ξ0I>””“ΨχPήE9Šς”wP~ς6Κ[(ob χ’ά(Ϊ-Ώ3,7° Λυ•]λzΊ\λ*ΧΈΦφ¬qιΧ©^#κΧD#V­ιYσΡΥκΚ•U=+]Š•!+݊Κeε=Λ\ϊe,`ie§«‘σDηΉN1€³‘svη’Ξ;:’AύPηξΞC’ΧwΠΤ™_XΡΥyk§‚υt2#7Ηwκ+–TvΈχtΈΉBαΉv¬ƒ ™¬Ά£©C@―] Ιά{DGXT…©#³ΓΩ!ώ’²ΝΥήΣζšΤΦΦΆm{ہ6εΊΆΝmΒN, Ξ6­‘βΪΚEΟ1Ψ/ψΐ„rPπyD]Ϋ>aϋ8# 8}lΰ ΔόŒy֞yΉ³]szf»Z2fΉš3š\33¦»fτLwM˘βšΪ3Ε՘αv]ώWe4Έ\= ϊŒ:Χδž:Χ€Œ‰‰h―Ι¨vMθ©vΟ¨tUυTΊj+ΩΈŒ +WΉ˜gΑ'Δα»=+ξlœBίΫ+΄Η‹=+ΆΗœΦE3cΤΊ¨ΝQ’/]"-‘›#·GξŒTε‚ΠΤ$΄›»ΜB¦Ωi~Ϋ|Μ¬σ³`άlάnάi'gΟ}FΕN#Ϋx π­@qRΰΜΐΆ@ΡΘuΡδ ΜΘͺ0,η8‡A,rŠ “ βfs2²+œ†„€Šβ€I3Δν̐˜RqFηΣ NVœΡϊ΄‚OΛ@dVΖ€™’s³›…Z*ΔηΠψΛc·BCj΅Wν›\-ij§Jlƒd―ηWgέI΅AΧ”©ξ^ΖniμeBiƒR]7…τ7m‚Ψ’j)Άήνwμˆ-i¬–ΊxΩι”Λ>^tiL±Έsρβ%©‹Sρ‚2c1Z–tβ[Γ+–;ρΒK€.©ΰΕ=°½e§Ε3;±tF3ο½ \α. ‹]3ΫΩ‹ύŸέωύ2_Υ΄δύαΰ‹Χιβˆ™3䟻ΥΫΆ\ςΛχupόzΰix^€Χαπ-Σαoξ7ΒψόΎ ΈoΥ,”Ε°”KΪύ7‹7(A<*πχxΜw +‡ΐK,[P W$^΄ψ‚|}WΪΆ xήTιΑ$·5 o`ogYŸοΌPŒ-MΎ< λyYΎΣYυΆΫ/›@;t@',‡°VΑX λΰΈ ΦΓψΖb–o†° nΝp+ά·ΓΈξ„»ΰnψ5l…{ΰ^Œγ}° ΆϋλΈΎ έ%Χςšΰax ž@>ΑoαxυΗ1ϊOΐSh# ιO’e܏ևя{=OΒNό'A/x`μƜ‘>¨yα μgΐ {1›ϋ`?<Οcbf_”mά2¨cOς ΑΛπ +Ό +ΏƒΧpeΌ‡αΌ oΑ₯ζε‘^xoΓοα\kGα]xή‡?ΒGπ)|Ηΰs\u§¦ώτψ}>ρ{G―/ΰzφaOΤω|Œ}‡“rG±οcp‚iΰ;&ΐπa‰gο.9C[ε<ςμέƒy{HŽ3ΟΗNΤy†(κ<7Ob̟ΔόςΜπς=ώl<…Ύ½ΧΑHσ(mlήτηŠβ½}x,x<)šoc„)gΌŸη‡"ώ†'œΡ‡rq1 <†<~οΓ`t>Ύ$†_ΐŸεΘπθ~ ΗξγK’Η£|#Θ³ΐϋΈ<ΆŸc[ΚoΛcΞc:Ψ†Χ}ˆϊ)<Nc€9Ώ’3ρ|9Tώ_ί…3π|= _γyς-œCύ{΄œEν ^/·^iω~€α'8όϊ/Ρ.-σš~ΐγ &0.–.Zy S0%SᙦaZ¦cΜΐ™?¨―¨Ρ՘¦ζb«‹uZΉŸ ΜBπΌ g,ŠEγΉΛβ˜…Ε³aμb]δPkl,ΩύνΒδ–‘Cm-ψ1*άί χMa™l^SYs`9‹ε²l$+@K:κΩ¨ΒΊL™%P ³`!œWžγΈBπTι½μμϋ7εγ +;|?ϊJθί/ξa μ0F1|˜Ρk™v(gΐe»ο{6Μχ΅rœο΄βΌο4ΛςΈCœ‹gΦqΕXν¬˜9cϊ΄©Sέ†ϊΙu΅“&ΦL¨_U9’Ό¬΄d¬³xΜθ’ΒQω#σF82Σ’ν Άa–ˆ³ΙhΠλ΄΅J©i墊&«”Ψ$)m••ι\·5£‘ωC“dESΕε>’•·kΖͺΛ<θ9χ +O'y:‡<™ΙZEιiΦr›U:Rf³zΩ”:7–7•Ω­RŸ\‘ΛŠDY1 -¬ε­eV‰5YΛ₯Š₯­έεMeιi¬W―+΅•ΞΡ₯§A―NE=–€d[{/KΓ䂐\>ͺWίVνεΝ³₯Ϊ:wyYt||£lƒRΉ/IU*©εΎ¬σ%3άlνM;Ψ½Ρk‚YM©³m³›§Ή%±u‹εέέλ%sͺ”b+“RVžˆΐΞ‘leεRͺ V=yθLRΪM6kχw€ƒ·υΖQ_biφ[TvΣwΐ+ω‡Β$±ζΑ2ΰΨp„8Ώψx>–›½N˜…ŠΤUη&έ +³’=ΰt€6JB―98Xκβ5]ƒ5CΝ›lΩr[y“½΄5BκšeMOΓΜΚo»€°c½U›f΅΄r6ΟιΆ•α 1–Πΰ–œeXp6ϋƒYή›ι@ζ&œΔ|†:·δ°΅K!ΆŠ6°{ωόz·ά„¬εRH©M-ώV’£Ϋβ)οζ‰αδ}Ωκά{!Ηw¬7Χ½+r‘‘C ++Ε€$–w»gΟ•,MΡ³q}Ξ΅Ί£γ%g#†―ΡζžΣΘ³d3I)ΗπvψΒΚ­pnWx:γ΄%΅]cu Ρb#Ο¬x±•a…IR‘Κ3ZRdu³htΓ»ψ=xι²~Pν₯•Ψ‰MK+£γqqΛ―2€hšC IƒP^έ獼ω€R¬εsΚ.ΰe’"ΠίΫί§ΐcαAΓΣYΙ琞&`يΥIΐyΚ&žΕ«΅V·mŽ­Ρ†kΘYλζΙα±–σ[]oγ_SεlϋWIΓeΥηSρΥ ξAΏδΊ₯ŠT9―<­²>NΦ‡ΤΚ+ͺ««­έ[u}7ΏΉΝί!XqarT‰UΝ7ηεβf­ΐƒVΡl³š¬έΝ^_Χ¬ξ^§³»½Ό©unƒn[Υμn[½»s)οϋ5Ρ+ω­ƒ šU7”€§αΩSkcκzlCύχ^ό,mέΠΰφψ½©€±7λά{­NΩ*p+7r+WxO“QΡΘώΡ{]r­B6Θz‹—l#'΄1hρ +d3 ϊ hSΝ)Ϋρ…;,’S€ηpΉu6OΟκΖΦξ¦FΎΉ S‰o&1ېΫόaA ιlsJ$½­„Ϋ‹Ή½˜μ*nWΫJ$Ζ08^<“Ί›lxNα’sC4kΔΥaβ«_°[½>_ƒ;ώHt_cpαVη;³ͺ₯χΒ`‡θR%i±­Ώτ¨Ϋπ刍Z07˜@Ή}*RW£Τ˜ΚoκžΟGd΅š$¨΄Β΄SŸΚD~#Gcw-›/lt•tφυZԻɍ*ή \>#uŽΌΕ†U-MVΜ€Zκq©ΣYͺγyCΛ<‰sdΡEϋ+OK΄λ :I›β›—υΨ!ΎΥ>yY[οwΐ{›$=Ž(ρ’Pϊ`t°ͺŠίλqπάυήM&Ϋ–γΡΘ-ίJΥ’Α^Ռ‡?΅Χ£Ε–?ΨϋΨΉ‰χqˆ¬j>σŒ»hoπϊ±­ΰ'ΐΰ+=ΝΖ|aBτ^\ΨΠΨ}₯Ašššž¦ΉjΝέέΓίo@ρ†ˆ½€"€½ ~ ›%°IΉjjΟ~€ŠjX«¨JαX+Ό‚ί‰«a…ψ:„αΘπC.^ρOδπϋν0d<–ψχJόλ·@ :τρ―»T²~#eyΜ#T‹aβ5βYΕ,Ε1εRεΧͺόoξ‹α·h[@ L„†ύ``χαWτQμέeeštυσ¨ +`eo`Œέη V†θθbΫΥF±Ξ\U¬ή(4@q§ŸΌ‚—#AŽ#ΜρIί{}¦ώWΜŽΎ£}™YΜo–%$PP«U*Ϋ° aDRb^NNφaDn’mX  ΫrσFŽs²γ=Ι2Fΰ:?ϊy’Xޟ ¬ˆ/¬ΟR²T{Έ%X£-q{ŽΥX]cΛKŽR*4*Q©Q'ε•Ψ\ΛΖ{S‘›‘CΖΖ ϋ_TžFxαjEΩ…ύΒΙχ˜Υ +ƒ^Pj5χ%Η…&dŌ6 Κΐθπ¨΅Ζ¨^ΩάΏ5ΚΣ…Ϋ£bμΌ/{?;ΌM˜Ή»4xŠjιrχF%y…۝Fm°5؊ɈŠ0Ω„¨gY +$ϊNξ1°šΔDU€Χwr·ν‘^–αΤκ’"Έ–„šΗ©j€ˆβ¨ΎΤβΎTŒcj_*s8‚ + +S_vfVτž3³ν<ΆρΓG˜sσrβ1Έ‘²Α|E'§3jϋ—Ζ§§Η 7iuJ₯.P;ΝΦkΌlΤ¬`οπς< ±>)Mњ–€‹LŠΓ@λιΓ1τ‰αΊ-ϊˆ$ΐ΄Ιw^!bΌb Φϊ#– Ϊ'l3Δ +/8΅`ΆΛ±°{Yκ.•*ΐ6*v;Cλ\ ΝΡ>\ώ­vΖ'l31Κά nζ‹R!–]\ΧBC\vRbN\@V2ΛΚ¨_²¬!m /³’&₯}i±+/FΌqΡ£‹‹Zt&J…ΕF‡C>fζΊYeξαϊͺa£]8οZ_˜r‹IP7ΣΌwιβƒ’½Β‹ˆΙFμŠΧg›ξeӜϊπz»b€Ž‘^6έγ4\EKγθyq0άY‡ϊpqΰΊΨυο7Η (ύΣOςοΗ\Ϊi|1¨γ”ŒΗ·ehHnΐ1‚r‹: PΏ`υu#3n¨½ϋψ]Υ΅ΫNέsΫg['…§9SΖ4M +Σ tXkΊšš――·Nθjζ\•‘N(m.΅Τβ{4 +f1Pΐj΄θ ύΙSΓܟšzΏ©φaω*›ώ‹ΣSϋfLΗhg‚Ρv`΄ΪΥ[ΛΛUe‹χ灆˜":BS–”'‘jqOJBρθΒιcKfWe΅QPh £¦,)YΆkyα˜₯]ΣΎ}nζ9qκΜΜqŽHΟH+˜>vXpx°:(>2Μf Œ7­|vΝ²7V”tξ˜a½fEΒθzfeο<Ϋ€œˆΏΔC)eε„ pC‡ +MψΠ±°UO;#MUΚ |›ΎΥΗΧ(žώψ(ύ›ͺΑcˆ?ΜƒΣΑi%ŽΐS);Œ­ ˆΝ΄Ϋ3c<¦ΑU8ΪΥP4LGηN\©3κU*½QΗ2'ŒΚ―šPX€ηςZίyρ\=Ω°ΞΚ ή‡Œ½0Σq&―ομ.=«Ažά€g―ΙΛjœzgϊψα‘ U‘hψΕAό€ρŸ2|—νΕ_–†—ϐo&•ϊ’ΗΏΩBσp²˜Qρ€˜¬{VL@pBAb欃³ΦE₯X¬ΓΓuγ·ΦO]S3lhξ¬μψ±₯ύ;‡’±z°4―ΆΆh^w3ζ¬wJ‘ΐXγιΣ6˜³‘?ΔαU‘ŸFQN­qΌM>rπΐρ8•ςސŸF3ω/6W-²<ΏώτΚ§λ%'ŠBQ΄»j™΄$τΚgV-—ητ‡fΧη7δE‡e5Œ)hΘ‹b§:φo_²Φ»΄γΉυγΗυ^W69#eRΫ8dzΚΔ6Κ·°η˜Χϊσhτ +MΈ2κ,:‡N4ˆ:~`Ξu^VοΤ9SΗ'C­U‘ςBΕ3”gw=sκ£LλώSχK&ȏɿ—Zω“ŒJΨ‡;P§ ‰Œ +žŽ φ/λΑΔΪΖδγAgΠ+‚X₯SkΤζ„’΄ώ£ƒ ‡RΫ–=6Ρ(ͺ΅Ί€Πα8χpίiαE/Œ‚ΫhξΟ˜Ν†Β°₯{q­‡_φP±μ²UΖψ’η8 ^†ΩY™εeγ˜ώε>hυ+δό›εM >gp)„†1¦βα0ώxαΊp‹>ΘζS}mε°Α!ψ Cw>–vΕ‹<!Α/e†X#Νj•^₯\™ζΖνŸ8iωdφšcdlrΈξUI―TκMXOŽι˜^U₯ΦͺΥ‘ «ψT9,Ύ‚ηΒ|:Ρ'νψ$³3Ζΰτͺ$½2²*Aή x&μvΦΠaΐ?’ρe"oΎBœ‚7Ε€φΐCvθ¬3ΛηyήΘ!ƒxX™b‰O‰ΐ­>yښšxyκxώΩρ@h©—ΧMLΐΠ ΐwyλ―ζ +C†M…|$uƒλg6p§Έg-4λ=L« „¨h―P²;!Jα;Ξΐ(KU€.ΈJW­˜Υό/ζG ‹tDΒ'Ÿ‰?ώpEα³ψ»Ύ8ίx‘ζ92811‰%ζιœΜS’nX¨­­IΌΤΛ ‘ʁ#†ό€›¨~G<¨ +N™Z­8¦6E˜Yͺ*2PΜ΅ΩC5b@dxΠeΦhΒμ‘˜5A(ό₯Βσ ΖςWyjiσΒω³:ζ§—΄-δ£ΣΘ?žΖ +endstream +endobj +845 0 obj +<< /Filter /FlateDecode /Length1 22008 /Length 11130 >> +stream +x΅| p\Υ™ζΉ·Υj½₯v«[Φ£[·’ZοΦΫzΪ’ίŽ_X`c[caΒ;δEœΪ@₯&™²Κ&Τ&Ω*œΩM"pNe“ 5xf΅Θ©B›Ιsΐ~ί9χο(dCՌ฿>ηήsώϋΏΞώsnί4σŒ*Rg”GΉώπ)₯|ϋ•²>{δύ7EΜwΟQ|ΞΞΊζzχϋ“JΩ?ΊfξΆYσ=›RΫΤ΅3‡yήBι½ϊ›²Ίρ»φϊ›n5ίsΏ„Ο1wςˆΫžχΎΈώπ­ξψκy|œ8|ύŒΉ~οία³γΤΙorΏc<υW§ζgάλ-Πkη›ΆZΐ­j7žό¦T9TΎˆΖ¬±ΥΊύώ©;•ύVUyώλΏύγ—§ωωΣ[Λ/]Xϊ™ο{9wαk>5ΈΟσΤrRΥζ}ύ…εVίχάΎέf|΄.ͺ‚”υˆΚQIU”²ΓG'J½ +©4―I=¦Υ~΅Qυ‘6δΦ4©IΥ«šU]¦¦Y΅Όν.υ˜Š©χ"ι(φΎ££„JΏύΆGT +€ψS‹Κ‰L.«œXT¨yd(›tπ(@'(«WUͺLε₯…jP[ΥZPU₯Ώ«©9΅G Ά—ΰκ¨ͺQkp'Ώ—‚βΎ—γN~/Sqz6²!rνα£ηrβϊ 3g§Ϊ#ηΤξύΗποžύΡscSα œ™šD?9μ·ΰς³Sθα:·|κͺφKΈΘΫ²5rΞ“ΨΉ²ύηΞL„ύML…£ΡΘδΉ ;χŸ»0ŽNMαͺά ₯ ˜ +khφζάf΄η™^v£t1uφ,ϋά½ί‰ž»pφlψ,žΓύΎh©wVŒΉxpά‚Ÿ\΄ΞμΔ½ψp’aV8Q' +²¦&0T~ΛΦέϋ'AX”„όyfθΖ΅E ΆPsΈψί‰Γ% ‡K"—e(}‡ύ ΉŒ^“απXψœZΙα3ο`¨ϊwgy`Λ‘ζφ~ΥeΑI’tZ‘ε?ΰsnQ=(I”JΗΎoωφ΅Νώ'”ί«>7γΊmφˆΫhA%μϋ€ωy—ŠΪ—©<\3{#ϊώ#θύXkU>Ϊά7€ώ/, …u1ϋˆ΅¦}*bmΗύ¬χ©|O*Ά«T°}„P8YψE*Χ:‹ΟˆΊΰΦθjύνΞMό’£kΌ*Ÿ>•§Ώ­ώ'GΑΩς―HλιDΑY*ΈZ?άd¨Nˆ“ΚŸϊ«@e%j΅ +ΓMΧΒ9σ―%/Ϊ€O<πsͺ .κ?β/υΡι·Ο}Ε„šPŸWΏΆΚ¬iϋSž£ήίίΟσNεͺ ― ₯`_αίέ[\V²―¬€lCΩ⚜5/ώ‘όwΑ;B―V|‘RUŽUώ΄κ«Υ_GjΎP[Wϋ@]oύ―£o9ί™`,Ρ…Ίΰ=ΰAΖpUνπΛ(yeπ]QΪFsγoP[φ°ςΑq;“ϋα?ΫΓC :G¦ά +/+Ό*52υ0(γ–άbutΖύQΏ…b«₯£Φ?,©.ϋΨ₯Ώ±?{©£[ͺsyάͺ³Ÿƒ^άrjRˆ^rRη‘–k XD₯@PΉΤΈΠ«¬ƒδCwŠ "Υ‹²%ηΰΈΣj‘b¨hδDCzσH^žŠ’^]‘X‘TΕΘTGgE›§§»·/]bΛCί­λI†ΆN}¬cφ†;6έφκus/έqΗΛΗqƒ΅όΜd―hzo8r +9(¨D€ςˆς€ςI―Se‘ΊN2^ˆΊ"(s΅qE‘ΪŒβη½ΰΆy‘Τ%,K©KΊ¦4Pη –ηϊœ«§;‘ά[ם¬p)΅ŸλΈF“zμ8I=ΰν$ώ½ˆ‰ά£bηρ•aXNJF²Ι[ΘrίκθμKΙO|βcγ½=ψη*ϋΫ°αΟCE3­ΈΧw}†Ÿ>vζcgdƒ5-°D`;nKN܍bί1Φ›Zzˆ€‚Υ £3Π5bχ₯ύŽίι‰Mτ%s.ό7{SQMKδ„υ+=Bϊ› j΄^_TΝνηαN +αN($eΈ^Α“›ΑδΊη1ΐmp#¬PΠoՎŠ4.eυQP,ΰη¦\p=xͺ/>3Π ήΠΊ£@οσUα[Zόω?‚ρƒZόω>ğρηCόšΜp·ά€9<‡ξ³žΐ«Ζv’ 'Ζ‰Ч X3& nQ?L9οt(šίσ¦Φšs]<¨Ύaž€7<φύCε«ϋΎ†Bžώ= +Μ‹όIša?.€1ΌξeZ׈†UQ)ͺV[΄dQUO%Κ"Ÿ¦Ž‘h>ε0BnDιGΩ„B3‘ Β(PΣ&kΔκρτu·ΩNC‰νsΊGμtW,/ΙυEmοRƒ΅kcΓ–‘F+ΏΊ-ήΠ.Άζ‘©kΗφυ„ͺ[ϊ‡† Ÿ΅χς;;ͺΪ{G›©xUΈ},>xdSSσΨֆ扁ŽζD’©ΆQ +€ xT‰uό<|…^AΛɌ½TF‚»E5ΰb¨œZfαΣwQ_Q`.ƒyθ{ζΨ‘{iYΖ’m…*k\Ή¬Θ5Ζ–Az9πΦ%θW±ίzPͺΉ?J@Z +8# šϊ +νΜΑMyΌi‡66¨-³Qh4ε6³«QiΠΡi‘ΎΑ΄n0+ΰ›μχ›ΠoΰiY3ΘΗ6€CͺqŸΪϊHμυ(·]ΫU–p‚NO΄ΗJϋΣ§žΆξŠΉ₯§­–Ή+`£έKσ>xƒυε₯ ₯œε_k5ΩΟ-ͺό@%­Π~ΐ€gŸŽ ρe Ї4|‚trΔΧXs•€ πͺv€Qmu§Y;m}ΰ1Φς¦'YKπχμψ˜>υ9€― pΒΡω‘Κz/‹;ρj9αξ)SC"(!β¦Φ£nπ#Όpπς(š¬Γ¬G7>Ί§ΐι4ΐz”έ(šˆγ’„S€wPbΊH£ΐ ΰ£~”0UƒcμΦΰaΈ$ΧΣEΰι"Ζ‚σͺPšPP6£ΐΣ‘†FΠΠ Χαkξ’QG#£NΦξ˜Υ5Τ5Fγ9g"§εDNο §Νδ’@½+JNΣJiΐ’F@WKy ·’-h+£ςπ΄yp1yXv積( Ϊ«φΜ’ΨΑάWyώ5ϊFwΖJΣοΓ†Χ Σ0;e_aτF_aτF_aι+ŒΎΒθΛΨQJ(ϋˆ‘’G₯|ΔΖπθ` Œϊ*F,ΧΟΑρ΅ΩΙtΎΨ=Ϊ",ΈέΊnι1»b£Σ½«Ώ6ΌφΐXϊςuI{ή;Ρλͺ/.‹€b£%σφ·gΦ;£ϋΊš§6·‡{wτ W6χGj{+Γqe/,ό/˜[1ν{ώω<ΟΔ™ΥΒμAξMh\s5_ψ4ΉK£ΨC@5|Ej°˜7²¦³Rσ‡xϋ6Ή¦JjΠ'ΰKT‚ίHΝΥašΎ/L}P@‡€EškžΠbDΝσ~)ΰJ^C7ΛηoϋoRƒυ‘izVšn•šo θ‘&―ΤTiw Sψͺp45ΊΦΊrrΒxP[­ύΒ΄ηΕλΏ`Ž­­f[`|q)[hΏ­Γ(ΪαήΛZ>Y…iφΐ©pή0¦S%Zw/ӎœ VjN +@8eΩ+ $Ί‘Q$Œjω²¨D•(Ι_£μ% J’η­-8""Ϊc€ΣfBrΊά™ζϋΪ ,MSY‘€΅ΌfNj&…;υ(΄eψτ+i#—5o ΠΣώA+m9Ι¨Ο €=ŽUψ+πΡυK/­ΰϟ=ΈΤϋοX5V½ &tό@Ό½c smΓu =c!Šϊ<&Δ*ΊW½Žͺe< ?9˜τ}Δ—_–ƒ9!sB]’©½’γ½ΕΈ·Ψ0g§Ž‘Π܁E•~‚{$&Ύ  Μγr5r +EOmΊΕ¬cΓ v.€@τBšκA&}©υΰF6/ͺrΜ€ρL–ΟY'Eτꆳz}B,Ν0,~VώΦ#Θi4( ³ƒ‰m9U΅τ#]½X΅₯¬d΄+ΔU\°λΝΫ‰φIΛγOΆυ¦Ÿ\°œΎ©§sέDέΨΡΙ₯Ε…Ÿ ŒŽόΔΊ?έΡΫ2»ͺw{Π.9ΌΉysOύμΪή:RΚι!Θ‰ Μ ‹*Œ(ˆΆβš§΅ —ϋόΓ\(™ΥIΌ”O· Ÿδ€|F>Σ¨Ά2λθUξΐUυ"aο[frE΄ΰ>{BŒiΧ‚œεNˆ*ΟA ά¦@ jbyΓ3½₯¬mVγ[·ΕγΫnέ΅τΏnί|°'θ9Έωvϋ‰ώΓwnΩrηαώΩδδΑήήƒ“XΒ#¨Τςλφy¬Σ[¬,ͺVp€ϊΉ€Ζz¨1€7QΪ§ΠѬ訴BΝb ’)œ†5x]«:x ,]s\ΐ$%PγE,[ΰλ*Ϋq}έ €νTBšΛ:ψ0ΖΦΪ 0ƒ’οΙν GXΊGE@ηpžΡ­`MD׈3/₯F—ΠhWœz=¦C/w]―U]h~1Cρ0W‡&΄‰ώB›B›B›˜„61„61ϋ«b&L:-^υ¬fΟ’*E_ˆ!Ν²―Ar΅Q@Ί˜J”F”~”M(^7L2 Š:Ι‹qO»ή«pΪ<\Λλμtω¬μσ;]GΗwžήίήΎτΞΑc»z}πF»FvwWUuοθΨ’Ά΅:φ ΄¬½kϋΆ»f‡κ‡ !Ο°¦mϋΥιτΥΫΫ¬pzKλ!<ώfΨΥ]°«BυζOŒE™¬;ΝιUε{Ώ62€ΣΈP α~τŠΊ¦Cΐλ’ξθΠΐ,Λ|P?σ/ŠΌVY››vΛ&‡²“―;PΔ&T†fΓΦrRΘ¦€‘~c6%κw6/X“ Kημ'–^Άj.υZλ–ΈW’Η³zlEžυ])Ι†ΊwgΫϊ»Τkϊ±»Ρ¦»Ώ χ‰΄ΐό ―AZƒΡm Φρ₯JtωΕ9τΉr8K9Ψ;§+Δ΄cfί\9Dq•ζρzϋΝ–yx_ΦB9Ιφς[žp‹ž:Μμ:₯™₯@ό9 {Eο' €κ2ΌκŸHΌn~NΊϊ₯‡QΌXΫη©§Pτuυ¬e7€Θ Εi&&όŽ… εΖ[μ[>Όtήs φΧv 2ΫΕ—ώC%0ύζ €JZΜ +·‰9Η#€ ³N#Μ݁Ω;¬UŽ!  ΕΖT§ΫΝ‘$PzQ6 θπi@σ&mžΤΣΒ’jBίUΟγyvbη,ŸGOη7p@ΏΜ:Όβό_ŠKJqo)ζRxΎR3ΐ΅2]]‹u«i³Š ’zετ<ε+&—:VΤΡ1²bςΓυϊJ§F@sΚάβQπϊ +^_Αλ+ k’¨]͌S +0γ`Ζ)€92.Μ 0γΠ8 2 ΈΛငξtaύ•Έω”Βύ΄oη/Ÿ³C³wmΫ~ΧμZ™% 3ΖY€*/ώzQΥΒS†aˆŒ³π Ο}ή„YEj.ž9(‘V¬Rdu¬‚ΥA°:Vρ@ΒΖU>Ώ˜l,6lΤ6ξ†\UΜΫCznΘΕ9ΞSb§”… š$=­έ6έtOo{taα[Α–xEyCKΥΓφ-λލn_—Zϊ•ukQΈ΅Αi s׎Ίϋ˜Φέ*μdώqQu!;“FιΒSΊUV―/Ό ΓDLZΗ1!½G]#aΣ56CGλN₯€HΑΠ跜٧#+ϊF λ]½Υ΅Μ\#Ž’AFδ€›ΊζgτL?ιš"PΓ…e5– 52ΡՊ₯eΒs$υI=dR™`k‚]Ιι>@Ig€ΪdŒ' Ϋφ +8’RfΌnŒ8’w ,Il­ŠΏ²α† +κθΟDH:Ϊbb<[aP₯m%[‰ΑVb°•l…‘—‰ΞΪΔVΪΐ•6Ϊ[iƒ­΄‰­΄ΑVΪΠ‘qš'd›2rπ"ϋY­Nh]WΑ±Ÿ s?χ½#θβ’Ε—μC*Κ€άπ««·Ο~|ψΪΖ}Ν+ «¨2ώ—‘Λjx‰ε•7<WV”‹]ΡΞβ[7WνšάΦtm/0±ΑΌ|¨"HΒ^?:j—ΫK…P<Έ“!k’ n9w +ΑIΫ/σ^ +‘ϋ^ ά‰ι—ZtΞ `’ξTϋ†4ίc€“~&Η(₯›³(3-¬Y3 χ―υh‰j-ͺ5Ρ»Oΰπλ ~E+χMίSN +x +ˆ&ήΑ sΓISΛΜ§n昞.ƒΊ―#Ϊ°0Ÿv€;c(;Qτ<}€~ψ7œpJ@„€Ό+“šh”)Αq –!κ¨C±‘=°γq½†­―¦«^11 ²bΠΔ¨άΞh3βŠh)-ͺAX&5γϋ½Πg/τ™g‘ΌΠg=νΡχ{‘ΟΨE$‰$@?Ογβo>/ΰfiϊ†Τά¨Α’ͺΖ(˜oΝ:ā•qΎΥVζΐΚpс•9°2ǘΠ} σk4!ͺΝη4žπ&›ͺ50fVBu±I<κ$Š}°«=M%’mV’{Οξφ•Σ†ε ν)TQηΑ:;ΐ<οjΊΖMƒ±²x(1³₯m8VΪΊεͺŽώ#[[Ό{ΖCm‰ͺφDNίΔx‰Σνψ;wΞφ Ονμ°~Υ4χκ’εMΙΌ’ͺς’dGoMοŽξͺΪΑ½ύ“ωΑX8‘*΄JΧψs +ύ5C{{«κF―ƒ©pZΆ·`žσ©-ηαq͞<ž-#Τwͺ²Α<ΒQπΖ†Ω‰' |97,ω<{έK Ϋ~bξmξΉ`Ό Ψχ'1^•ϊtvΝΐ³»ώqyΕ<>dΔ/Ύ0@WRάY3»α‰^h ψ~ †Α*P’(4©U>h•ŽΖ)1$Ζ/zM"RCΠΗ€ γŠ‰_tύππϊ¨o‘yϋάΊusΫ›±¦šŽmY›H¬έ³Xκ›œlhΐ?`EϋT1;€~7YΟγh`!τL|ς‰π]Ν¨KΡ˜‚¨„ ¨D˜\i‚ΰΣZγώ '’^ZXΫΐΨ±ΤL<άβtŠš€έœΖP”ΐγ#Θ+ŒΰɌΉnΙ€Ό:h™ͺ6³­Ώ*ψπŠ€±έˆ δr«“E\“e"¦z`²\}Α@ s=`Gκ½ψK±ς0IΰE€_2ΛΠz«ΕŸ$ΰ<‰Qτά£Α’κΐp˜ν&a»IήΚh’G)y( ΫMΒvα8π`U3Cα“`ρi­EE5ζ ωΦeΖΘ00 œ ͺnΣt‘¦γn Ut`½‰Έ+^Jw₯»ϊ‚%ΘH$VL™Ψ§ΞlόxτΔς•ΐΈ]7`ύ²°ΜλσεηΚΛ}N›―αH›3ΦYλ lŒΥχ§ͺ‚υ ΗHΞΌ§y|W*±emlΎz[bMCui¨>^hIVί\cο°½9v­ ΔC ₯9Ήή’ͺͺPM©·±Ήk]cYYl ι~ŸΟS¨ͺKs *γά±„ΉZ½t¬=&Ώ‹4-tс.2=Π­²Ρ3κήa”Κ],νΣgP΄θΖt3Γr Žΰ^/ςδ%Θ]λ-~:ƒώtσFŒ ˜ΥS믎κc2BsΧ1¨»8#`L8„2™xάHV›³>€+ ψ(Dλ<‘U—Ρ7m©‘Z—<4=θ’Ίδ.QΝJ†‰N\βƒW $QϊP6’Έͺ^ Ug*Σ€8uŠ[2z[ +Λϊœo΄#ΤΥ\SνŽwηΝ—έvtψΰX42vh¬υ²ΡΖkGgiΣζ΅‘±ϊ‘ώ™γM―œέšͺhΫΠiΨZ~|g₯jΆ°β8#$ξ™=έB8‰ΒφρbνN2΅£Œ’μ@9„’K‰ž ύ§ρ€AAά;‰+€ΉM[†!ΉΜΥγ-έ½3"š nΛ›œL.«‚Ž£bEPƊ²ΎfU22›άΒX‡˜~!‘Ϋ πΒς‚ΨIζ’ +2Q€ŒGH@Š ‘bŒI@Š H1)b e#'tͺž«4œrΣΥζαΉ™rBΖγΞ‚u0κτ8οΘ>bύo‰w'8sΫΜ]±ΏsψΚ‘ΊΔθeΝ•]mΙb{~iήοτΖ}ŽϋΊΖ>§tCk2΅efpΓμΊϊόςϊ •·”ŽŽuΥEΊΗφMG{Φ»2­„MΰtӏΟCD… ‘ΆΘθΦX%w]+¬’‘ΗΪ΄<¦ψΎΆJͺ΄ζά 4V*§5 LΉ¨CΣθS·Œ ΠΛ1ς}L2ήJ€α šN“ƒΌ¦^ΐεr iΖWb‰’Ζπ ϊ†3\μMœBΡG€Yi “ΌCqVhΜΚ 8< uށDuόΘέ97;rάN^(Ž2’τ£lBΡ ½γœγΖ‰yα₯ +QηŠΐlθΐg«/ƒ—ΑκΛ`υe°ϊ2±ϊ2X=g1s=8I#Ή[8yDΐ9 θtϊΪg¦ϊ|Ο¦·}Φύv Ϊ^[Χι-Cw¬kΈt.Ώed{σπUγQgόΐΰπα±ZŸΥξŠWγ]΅[λϊ›«R©ζu­›Žυ!>LοΉ~ŒΑ2ΚΦ>βƟ»§γ>ˆ6-‡j·‹sˆP„r:σΠ‘ŒνΊ‡ϋ²ΑΤͺ­1Χv³ΦOYωWΘ*Ȋ Icψ !νC©ΕP ½λGαC8ΕN1„S αΠ‘’*ΖŒ%ι„*K¦MΤ\€{Ν­΅pr‘±eώήΚΖtM_Ua•Ώ"zψπό^Ϋ›κŽϊ¦<žP€gοN‹YBg9mWΓΎjΠξ[Tk]ŸιΧ6†Γ{ DΟwG„?nrΠ'ιNqΥN‚»τ—d]δ]/³ΧI<[Χ’5ŒΦpϋx)q¦†#Γ‡αˆΓπoa„'aε£e"ž“bPΧΙφΟ Μp&uhp“„šρŒD3άμJμ5N{ŸŽυEn8³Gβš½²s‹Sbρ"Œ°"²’ΒΓ +„{™p1Ċ…ŒώΗΔοΌh-Š{T&τ€"°’ϋn€ϋQh8¬ΖjΞ—O%0΄Z qύ>ξΘi_1#Œ™p£Λ‘Ίκ4#LΣω =³wB(|w¨ZΦ -λDΏ Œ¨N='„p§Ž/=HΉρτf‰'4·ςεZqa]ο°υΆι!T‘ξI{2gsL’ΔώΠνΙγ-9σήΚhchrΜάέ1rω@udθςΎΞ]!{ώR’'ZZν‰'Ί#%eΡtψζΛ#­•u~οΔ؎TGjλΥύέWmlnjΩΈ΄·>=ŽtΤDγ‘Θp\0Δ§lv²κV£dΗDk–ΑeEq!₯}ΐIΈT+/½ϊε‘jŸ”Ψ."ΰ€L'8 –ΉΎœzΘUL6CΌ*΅ιNπY―‘έ½BpΖ£`+½gκR‘g)δYJ  ­r˜RΘ³ς,…<™Ί~z–«JΝ.δ΄Lζcθ]š‡©BAx 7Bσ;CσvpwzοΥσvGΊΆ-²fΛΉί;νϋχ/ύgkrΈ>Άfι)Ζ\£˜[Ώ>σ|έ²œώ3όε‚Ϋ|MF_LΜ]0gπn,2UŠa΅¨ωiπ€¦sϋωŽNP° ϊtΫ2ΘΓn:Γμ_τϊα¨Bό*ZWξ ς”θ΅š8 xu‚ +½Mˆρΰ.~?»»§‡sŸ2£«5Jkζ)ΞWεν”·₯ʍΌίMG™ΎΕ’Yﺏε{ΡHŽX r3Qδ_›ΤΏ >Μχύ›³«χΌMΌΫμTηjύ‹aΝΦ†ω‹ϋJ1ΩW2ΫI^pΓΫ³€`’αωQ/dνΕ$γF:άδΡ +rZ³STŠs!:ͺg&΄ +™P½ΞbbΌJΧ„P6£HKDϊι\ε@ˆ&SS‹ƒU›1Ϋ 0ƒ’MΣΈφε·+•A˜mά~9ά>_¦,‡Ϋ/‡Ϋ/·_·_§ fα΅c6˜8†βϊΎΑY_P_P_P‡›;:½+β*¦Υ’οpΝVΣΘαυ޳ώπˆ|&ϊc~¬?α~–4!’Z{tS>Χασ +ΖλΡuιH$½.Ϊ0ήΡ~ayά…\Έ›Γ\ŒΈBΞ γT―;;]7ΙŸ]aIφOAΉzͺ\TQΖh\‘­άύΝ²w•:»«°μŒνŒΩ­]‡ΩΉrΉKΞή†ξΓΣζξ~ΤεΊήΆΞXk»ZΐD~Œ‚2Λ΄b8ΞΔΜW·sbN¨ΛQ°ej7‘ޏΒΓ$²TΣΞmBzά"Ατέ5μIϋsύ€9‹hΠ2c%Υ2:gWξλβκ,ά·g Κ¬Ξ¬O‰υ:›6=N²Χ)΅½\Ÿm=:Π}υΦVΟ––¬ο:λυGw'ΧλΙΦψ.λ8δRίΞϊwυF92Q~]ΦΨ?ΣΰŽ4+6χ•Ÿl…»ε—­ΘΞ‹τBA}(™ηέήΛ ©½PšI© £KμZΛ:n{k›ΊͺΛKσŠͺŠjΣ‰ΠΌν=lŒ–ΫΦ ΫSΡΊ.υ;= Ÿi½τΫΞ#v*D΄Ο*χCk4b’_£6 ž†ξg¦°Σ29œ“…W—±Ί†`Zj.Κ5§ά# ]je+•ΜΡ l-‹Ž΄ΓkθkΔ`C ›AOr·‡°8ϊ)Ω½)Εγι¦Ϋ2>ΏG²ώtHϊœ€iΊ‰wι&‚SrΧN²Ίg†0..hΧ5’+n„Υ„4Cξ΄IΘA5“ΰ³ΩMUτ5η„Ν>ΦnSΓ%χŠ$°Χ,yΨƒ›γ‹Qθ˜t§ΨΗνzœΣΧ`ΡŸVÁά,ciμnq%|›°/E`ϊqΑG₯ιœ€˜žœπNš9γήζY±S‚ƒ_ξNI½rŸδSήumeM± +»}¨>²Ά½¦it»ΣΈym{KήΪDQ΄ΆΌΆ}(R?Τ^ΫΌ~WcηξaΗz°«Ρ[n©ρω|ε =Ιζ§ΔŸN5ΥδύΑjSߝhk\j]ί¦ω€ς–ί΄ϊμ 2_3ϋ#\OΏύW=³ύžyUδη.˜αˆ]gΎ2YΙw)sΑΔχš~€+ΗVΧE=υ’n e΅κE²\Δ +[2ϋζ―ΉΖS +UbΦΎϊBkŸν½οΎ½K―­©^“oyφΫ9ώhWΔꀞ¨!ΔΆΝ#)šw6Ν™`Ι;BΦ'EŸ".8)ͺY9λ‹©`&Ν¨Χ 'ΕN/Θ¬$2,]KΈ>1kΩΰΒei6‘hόdG§€υ=άΧAΗMύiΫ>……@jφΚωξφXG]ρό¦dϋΜ¬΅iι{ύ‘xwΥ0§²ψ,ovxΫ™4jΡjΞνKΌοτžτ!K+Δ£VΦ fI£0ɚέΕP‘νrƒ€― +ΘTί*¦‘}ν’&њ7xFΐο$ h 7»Eΐ³€©RsΒ*ζ'―>dίρώ‡Οΰ=Ό­ΔΥ†…4޲/W₯žΑ•ΌZ‘iΙπ +ω@3τW„ςFσ ΩHΚ3ΟςC©yFΐ€€ΝBgζE‹‹ˆ_άΫy‚ ύ\ԩďc‚ϊ1ŠΞ(lΠΧ}E³ υN³Wύι‡g$±`Υ²(«†«L}ΥτΎjΩ[B§^’σ(βœ]T€3. “Λ'Ϊ‘έπ8Χ!κ/χ•‰—ΔOp­DFΒl ˜ΐϋΊιUo Ψ.@§β9Ϊed6³·{fPΐ?/ήυρͺߚ/ΜDΉΪ¦_5c‚V‚λDBΫΌjδαΑΌνξ½*`JdΑ=R}{€· ]ΧKM‰€IΓΎιNa|[χ£΄τpΧ‹°‹€€ζ_Δμπ’"5Π0p€ΖŒa,DΧΈœοΎ\&2δV·&Θ•!–»5‡4ΡΖΐŒ‘yS|ی}ψΖƒφ±›Ύxσ΄}θVά֝,—ή΄Ξ,!εΐξ‚°»BΟKΩwΈ³>ΚΌΟΕΉλmλξWEOΎ%ΰ7Ζ(yΤ‚rησk ώ§€=dΨ‹Rσ~²€gB·k¦οr™R#5ϊ-1s—Ϋ„<«Ž©ΑΒΣ σŒ€I^σ $Ό΄θx°«Ÿ‰BV9―μ‹ώu^θm= »½’¦μϊ§Erd’ƒŒζόB&“βαkš5O‹ίω†¬/LΣΛ"ρ{όH„x―€ˆfLΐ xY@ζφ½Rσˆ€DT|q_φ9g2ΐΧk^oγ™m}†ωnʍ‚~HΐΣ"Šo Ȉλ¦&έdwi™eΥ―ίί-Χ?€‰€ΙtE_Ϊp’>η‡ύμά >χƒθτG­²ίήλK―>φηbφ><π φOWΞΕpΰ*=ίφδϋΣθχ„μoΌB@?'ϋk~"ΰ„4έN@}Sjžš‘šqχŠ;π9#f―ήςε‹vΪ2³―‰άK@ +~(5?—š’― ψ4υ‚#Υ ΔΉST£4υPXΏ‘š3rρ(b^έt\@˜₯;l!ΰΣ ΰν―HΣ§uΩͺ¬γ*#sΧϋΩ4{θ53"†r ―žhx gΣxnΦΖΪΞ=ηŽ—½q‰Ω΅Η«K&Αζ9~DQ~d^όΘΌψ%ΑζGζΕ63Ύτͺ―BF hΆ_gΖς Ζ΅Λλ$ŠΨ#μβiV\—­q“ΜdοGwΨΣn±žY]5»drέ-`«πΆFx{π|aN†¦Μ HΉ©šμ1«ΘQΛ³π1ϋC·<^NΆ6&KͺKΚ±Δσ›Ήσ Ηί£›AΔ™ށm‡ΊKE@ϋΉ¨kx\yQνΙdΤ‡9 ―8ζeEΤμϋκ$e + =ώRΜ.h—9ΆΫ +„΅ +ΡΓθO§.Μ©]VΌ—: l‹θΡ6ϊZϊœ8]eŽ#PWπ<ΘU σΈΨEͺ‡]ΔΑΏ8μ".v‡]ΈGnΐ©nέg5­|\jNIΝjρLϊšγ€$g¦Α³Ό½R³’››θ‘V(A(ΑوάΑ³gυω’\€8Qp2Ϋ}§ΤlLbƒ ―œb…ΟC¦½}m8²ν5υς·Žκμzό’ +IΘΊΝξLΥcΟkhd¨£Φ_“ …k’}“mοh°ζrΊ‰ΞΊŽ‘Ht΄³~{fNίΖXjsoύ³αf ΎYέPΣlΧ545TΖ£΅‘Xk¬²5Qη«XΫΦ<– +…»·uυŒΔ[Β)'β/K΄υΗϊ7+Ϊ"£©ŠκΞ -ΟKΥζϊƒ…³…υ‘χΛ† ς‹άc«±rΟ#Η)Ώ %女wάΣ°#ΖΖ°β›·ΊΚ€šƒP£J$>ψλJΥ™“ λ͞dp=mΆbΥΑΡUΞyUž!»οε†DΩs)$ΝV\²―pΑά³πcΟ‚žϋ'y•((ύ(›PΈΕ„jb"A‡jΐΑ+GŽυ.'¬ΜΟ/έ½κψήΙϋSG H–Ώηρ§ώZQΙ­‹"Γίh«‰ό­΅$L¦πRi;ζœ.δ»α@ϊ@φΌΖFΎΦ·½‹Γ]Θ5ξΕnύεψωΠ)Έ“+13~T}=[To=l.rhjœλR›fζή?sΣ±#‡ί7sσLλφ™£Ηn揸ώ?ΥΫ±D +endstream +endobj +846 0 obj +<< /Filter /FlateDecode /Length1 9380 /Length 5521 >> +stream +xΝZ{l[Χy?ηR$EJ’HρύΙKςŠ‘’(ΎD½-Ι’+’mΪ’mY±-Ώ2»j/·4υ²5‰½%M€6­ΣλΦ.mΦzS;4‘U€54©Σ3 +,A3£θλ² Ί<Φ€’χϋξ%Y͊ Λ£όρ|χά{ΟωΞχύΎΗ9τιώα!ΦΜΞ2:xbmLώhŽ£yώΰν§ύΚ5ZΗαێœ¨]_f¬‘γΘρ3‡•k흌yΎpτΠώEεšύmξ(:”kήƒ6tτΔι;”kυ/ΡώΩρ“kχ΅\žΨGm~v Χώμ?qHy>„ρYΗm'O];Π.ήφΡC΅ηω·*χΦ}sπΆ k£ ϋˆiθyΊϊΥ]ކ…ΦΎ7ΉIEr±Η7έB ϋιΟ:MΏ™¬ΆkŸnΘβRWA~G΅|=Ξ<ίΔύ³Ϊ§i”>ef‰σΌ!0[œ" °<‹³vfΖƒž8ϋξlΉ±k…5ΰΟ_fά?φρcŽΡe¦ΗήbΜB¦Ψn°¦λ½¬IP³fα9fΒνΔΤ2ΣΝΜ}“σ*Λόϊ'—G™χ€U-μλΔP ΏμΨθΏB±8UΒ?Ύ€ +o›+ώσώσ“‹ηύγώ£ϋ—Βr‹‡ΞWRώ%Ά}ξΎwΜ–†*ξ5φP₯‹qhΌ‚ΗΟW0Β­΅ΠΚ]©U<€NLω—T‘™ΉΩΉ₯³£ξ₯‘ъ;π-]ž™[Ί<κT*xJ³&)$¦ε+2k!³&†ϋΚ(Ϋ1†¨œ?OcβJˆ–.Ÿ?ο>•Θ=b`™³ZVJΟ¨ΒcΛ|hfŽn ‰7uˆ19*£[—˜Ϊ>7I$‰ώ·TΚFΧ©΄iMP<Ϋ ρšd•Ά|H*5|•Ά~ •Χ$½A₯&Θl$•Ά½ΏJΕί‘Π5 ½†Ο*>ϋ>6ί aΛοΦ°uMniƒ΄VYΓφIÎ’aη°kM4μ†Μ.°gMΓCξ%ΆZhψμΘ²ΓΏ―Κ½λTΞί`nCμK°] ‡pίυw„‹lZψ1‹ —Ρ½ ώ>΄Ν"ς½ϋXXp²0Λ6Υڐά /Α†ΨO٘ͺΜ†ˆ¨Ορ­¬s΅σΣQ?xƒ a:΄ˆ]Ό)›™†}Χ~D΄±έ>B-‚oθ^»l`jŒ§•gΠ­υΦ=kB†k©_ΎOk`­ΜΘL¬ χ̈³ΦΪ36fgζΔ•‹Ή™‡y²BΈ_Ύ?Α&ΨGΨ[<Β_ζ„ΛΒ―Tw«^iψ’:©~JσΌvQϋBγιΖkΊt?Σλυ_ΡΏΪτΝ‘ζmΝo΅L΄|eΉεη†id€ γόyαXŸ–’œ0—BL5χ‚R—π κ τ/!?§»ΖΘΗζSnκΤTjjκP³κhΐ 09^PƒC.z£+Ν¦€Ω0ρ ΥρL¦zDxdυ3Β…Υ’π,,± &ZΎΟτL\Α%’e##Ν&‹΅"k™:΅Ζtΐ$T’5cβ‹ΓΪH~4΄,|Ώ£ΗΧ4ZmΐΛŒυƊ³‡–YgŠΖc΅ρpΥ―!t^V³—`‘W@Βό°s6AεM, +*‚&AΠ1ΠΠ9ΠΠc K + –yŒ†mΠ— ‚rππŽ[VL»Β$`„$— Ή92έ>Αj1ZƒJΘTωUΆ')ˆAƒ(σΦ`6μιΫ읣αќ¨ζTύ΄Fʏ]^±o6Y˜+yψsώΑξφΆ`Κ.t₯<Άξ‘ΚΰT “,Φdq"Υ}SΖΩ΄VΈώΎDΞVAΰ04ΪAΥ TΓ W’©f€ωLmΕ΄›gΜ*žΙμ~εŒπ±―W_Ω?Γ“ΣΗͺΑ%Ύ΅ϊόΛ«Ή_ĘP,›ΖψgΑλQσ$8Šމϋ­@£ΚN£¬o-τ­­λ[+ߍ’£šU@Η@g@η@@.€ZζW`Ό1—Y4%ŘΡϊ˜QŒ•ŠψžU@Η@g@η@@.€Θ†mΧV˜ R·Ιγ’ƒ°bΪδ5X2?¦‘₯&,ψ&πŒ‰kΪ³g²φΜYΕΐ‘›έBΆ'"e85I•¨Z»{ρ»νω˜cσlυe^ΙΞζ=γ#™|Z4vφdν_ω‰―wmδŸ}YλHDŠEžYΝ‰Ε-Θ^ƒ`Ω^Μm±κυjΎzΟΫMξ€Ψ7 +I8‹_]ψ;α_`γ+Š |F@[ƒK(ʁΖA;A‡A·ƒξ= zτ8θPΛ<”ϋΟ`ώ $ΜC NΔ*ZΉ+ΧΤV¦yΛt­ Λ“o“X6!έ&Œ.4φL/Ά}’œˆn»sϋΉ/TΎμdjhoΙγ)νš:Α«Έ«ΧW:|nzϊΎΕβΓχ&nΚ·w͞θ/νΊ K2 ccHρρeζ–½ΊU‡¬εΩΰΥZΕ«Ο(ΓjαkΆm­ΩΌ«Ν›jξΫ ͺΐΚζ/~idwΡιŽes]ΖΥ7+wοθψΦίΞ±πζCC#σ!³Zψ£Υ\χŸοώΖΧh=\^ΟΦΣΔ2+ˆΑ~#²„Eg_υٟ ΎΞBߞΏ§4?ά±ΉΡ.θσ³Gz˟(Η“swΝ ήY©π;fεΑPκζ#…β‰r9RŠVzέƒ·ή?=~χb)]ΗΤΨ ΉχJ=SυΜƒkhΖL©L©eΤυ(£ ՈjD.5’ŒQF(£F”Q#Κ¨eԈ2jD5’ŒZŽ2„Τ¦«ˆΐzL₯«aJM°Z|a²mα4V‹F⦌)cΛ!lˆWf§¦f«Ύπρ“'σΣ\³ihh”σ«9β·-.ž€υD?-όφΆ±Nφ―ˆ\°h +”Δ¬Ψ"BΧΰ=ˆux˜yδXAN±»oκpC»λ`wμƒέu°»vΧΑξ:Ψ]»λ`w]έξ:Ψ›TΨέƒΡ̊ΎΜΠ—Ή/3τe†ΎΜΠ—ϊ2C_fθΛ }Aω   Η@—@W@-σΠ“zj@WP²‚όbω!FkΧ¬Η +T¦΅η%гJ&UΒQž?Υ}sWfx_ŸΗΣ·o˜ΪŽΨ©}Ή\|ηΩrωμΞ8΅'žο΄dvΛεŽνΘP;r&φdί–ά§·Mή{t`ΰ轓hϋc°”‹(Ÿϊ!Ω_*ž…Π/ &bύΒUΕ«ΌΠ³ ύ^YΟ’’ Z‘κZ‘  Z‘  Z‘  Z‘  Z‘  Z‘  Z‘­PEd€fΦgŸ ς0i‰2” |PΙD!vŸŠ\ ι§–ˆD•ˆ4˜„ι…_Š1WsƒZ΄ϊ[[[AΥd—Ϊ‡K§ǟyuSŸ»³ O[Ε”Λ•KEšΪR=9—«Kςh„ΆΓ»«Oόz4'₯½zˆT‡w‡Ev?ε‘vY0‹’Δ%!£Π’T«Α,Π‰₯ tb‘Ÿ,’U@Η@g@η@@.€(wΓOiΤekμδi”<•—ω yJC+—Ρ#ΚI‹’xRSΛXό?w ΡΝϋςιωΙ„4Ό­ΌmXςχ ϋ;n*Š»S³Π—>8“©υ»;{}[ψ―Kε‚Λ™Ή9—Ž:ΝmήdXκφ6[bÝà ύ>{fΆ˜OΊ-&W4 ¦<Νc€'…Ώ)θλb#|/Υΰίa˜…9ΐ!d&Ϊ²c]κ«T² ›w πΌ΄/-ΐK πΌ΄/-°FΚڟσydΈ΅ϋ%² +r;»jά2Kcά4ΰΓ£k΅}˜Jω0k\«νκΐιΥZG#u4RΗ +b›ΐΒ2ΖΓΫƒb"œs.ζ!t#¨TM€ζ@GAw€!g΄ΓŠΖH&”yI¨ Ψ'q^•d›A»@G@Ιk½ΜΓ •aWM’–^“©*BE$kΰυp±Vnk΄β€JNη&‹-ӝ— j%j‚ή&$q>'&έΎτ`@μςXQKnBUB}[βX!¨΅4΅ž7φ{“^“;d‰υEΪ„–p,6σR’ Άi΄Ϊ§ΓlΣD‹]#Ρ6}{‘³ϊ–Ο£~ΆΉI«³„ύVo[£]ŒBo&:[ΘΪ)†Σ6B‰-hˆb ΅ h©Œ§Ψn€Φ¨―Ύ1€w*ΡΖ ΟrΦ=Λ Φ ΟrΒ1π,'<Λ ΟrΒ³œ0˜sΒ³œπ,'<Λ){,^₯Δρ!Τ““C›aRβΝJήΚf|Π¦μKp.(ΧΔλn&Χ‚>aSY0D“)s©RςϊJ•Ύάn«ΐKΖP)‘ΪL‘Ύx΄_2SΈ³··5Ζ&δσ¦Rœ7Uϋ#…` ·9Ού9r"N>Δ߁‰μΜ +€ΤΌ₯ζυΐS<”θΰ)xŠžβ§xΰ)δ3€γA>σ ŸyΟφδ +➲1MΚbᴦ&Uψ¬£s4ΰ΅‚l%κ΅^•kό$’„N’@HI!I $ „$%¨>ŠφqΠ3 B’@ˆm˜;)FόpRΘe`7F&QάK‹³&β΅ΰ£8λ">Jυ›I΄ŠIxΚ{ΗL’(kMZ· [Ό:ΓνΫ›φήޝ½έ›`ŠτΖ¬IΙΝΛBGTθΞΣ―Ε +Aƒ)άν(ˆF Ι–xB  f¨έŒ΄G/šρ’£ΦΞP‘)ϋb+q +F(‡ΚζHQJΚ¦Ψhƞξ ιΉθ +‰₯„£ΠUύΎ#6ΦνΡ[n‚A)ϊΡ~˜Ξο0GMF +S~ ^C>©–PΐΞ)RgE/m.)—…GΞ―Ύ‹u”u«¬ΐVŒ=±‚•T₯εΪ°Š- 0ασ`>UgόufIf”έ’ƒžύ’–όμBρΧ™%™4P‹xΟ(#ˆfι€Μ5J3Λ½΄«OΘU tj9q;RΣ+m\訌׈χ_₯}qM»8_#6§δφš’5Z‚›VΜ•[Ω°7lΣ•'Ϋ%[cΩΩ9(eΚ.γtΧ‰^AP―ΎΛGZ;c>S{ΜY½ΘGϊ&ΪΪcpάQ ™’±Ž9ω\’ŽςΔΨzέˆZbRp΄ΥV»ό6RΦ­ΐΆ†Yΰuθ8•U$܍}ΫκρΞšƒHϊ€S2΄ΐ ,ΰ?Ό!Χρπ{½RmθQmθλΥ† ΧΓ–zTzTzTzTzTzTzTzTzTzTz9ζ:e SΜkΉC$ΜnΘ{e»|lz—3Q +J g½ένΪΧ?0?δχΝτοςs!5ΩνruO¦R“i—+=™*˜ŒF'‹'c±Ιƒ0 Υ_%Τ_&€τcύΕ χjυ΅˜‚œυWVύϋΥ_jlχτ7`γS.Τ…˜Ρ―­‚‘Q"ό+G£rΜΪZ¨f$™u5ŸΥ‘%MyŸ• @Μφ—-‰‰ΜΨξvΣΏθޚσμϊρs?gν¨ν^€-zΩΧi³«‰ωkœΰύr€H+'Αi ₯ΏršŸΣ@`Li 0 ¦ΐ4˜Σ@`Li φ€JCk5Αsϊ(§εj@‡ipK›–z5¨l`PεΘΉK)‚±αVφ”7e5ΤΣάiΪGLΗ₯›%oͺίολλj·ψ%³5΄ eU°Έ%Λ‹έSsSݎpΒβJKŽ/wmŠΆ΅FRαξ€[ ³ΧfqΆjt–€35n5‰©»Πn²Žv£Fo— 6/r―π%Δ(T@3’½΄h)sΨ±ζπ”=,”=Θ~­΅2ƒΌς½S;( +ͺρ •boω?Ιΐ1K<+o΅`ιΗΛsso*0,YœυAύωΟOUŸ%Ί)•Ύ­•O)8‚­_ErPΞ₯ΝH½Ν˜n£ΌΚ.‡ +sqΩφ$υZΗT:Σ£!θ΄Ώ™d?ˆd‰S©3Vώκϊœ[άα«Χ”|ΛχUΏ|λξμBT%θπυ ΘG?Y(‘›~A‘ΤΫ@χ!ζ1γXE%šyτΙΚύΒC —1ή,‡Υwk6XΕϋMˆΛP6ή₯š€mΐδ8ZŒ‘œ +}ͺ2y{F•΅’ψΓ§Ž<ψΰ‰|λπŸάy‚«―Ώπ·Ώφνoγ­ΖλiYo6vμ'ΩθΧ9νΟ±ρ^Ϋ°[©ΓΊ£φs†vπ:Z&½ΝΧvήΝPp3dmΖΞ»;οfμΌ›±σnΖΞ»ΉΎσnΖ6o`ηm©Π•VΣα?~οπq”½ƒ\•1γ7΅zΕΈ§Υghςκ]’NόΡ-ŸΪ=ι ΄6¨§ΥZ§}— T+ό«ƒεΟυ/°n…Ϋπΐ΅ +ZQ‡Q H¨…c¨1{X>:ΚΖPΝnΖΓό―•›Ψ ›eΫΨvVF}» ‡Ά‡αG1h D ΖbΓτΩŸ8tόφC§ά?};‰ύU&Δ +endstream +endobj +847 0 obj +<< /Filter /FlateDecode /Length1 3988 /Length 2293 >> +stream +x­Wmh[Χ~ΟΡWό₯+Y_ΆdIWΊ–μH²%YWς—μ8ŽμΨ±[œ8Š“Έ±{“΄sΊd¬4Œ•$f £ϋ“ΪΒ`ω5 ΪM1! ϋΧώXΝ؏±ycŒΑJa0Θ +ƒ6φžs―μΖ]Ϊ…­ΧΌχΌη=ηΎο{žη=ηΘΛ/^˜§zΊL:΅4{ž΄ΗΌŽζW§^Z–υ>?ŒΆηΉσ§—ͺύΫOŸΉτœή·Tˆ,ζgητ>}†6·ƒήg*ΪΦ…₯ε‹zίό.Ϊφ3ηNUΗ-o‘ο^š½XO"Ύ|vvi-i―ΔωsίZΦΊ$₯ΠφœqΎ:ŸΡT{δΝ Χ qΝ¦Ώ„±]³ˆqΘoœοή:!ε?aΝ†„ύϋώ5)Ϊ?ύΉΓώ™y#hΊiΌ€y5U?Ϊ7†7ΫΘoZΗψ·M7…—Om…œqΆJF²‘;ΞΪ(H^jΨD-ρ{ΠΩaqPλŽ9t¬”Β$™<۟I€R|‡ΕF±Žh•œμ‹W¨Ayy±©P‘Ϊ8¬ ¨Ytε.”vΈjF.uΤΏ‹―Zɏ`vΐΦ§»ψ$ο!˜¬ΪŽ^˜|δ@>˜°J&|cG$ڊ„δœ΄D―ιM¬ΤF›V2q+}ΐν„‘ezžΎOΧιΊ‰aϋζjΰ&²ςΘ/‰‰ +ΥLΞΨυR…mΎZ)67œ8ήQ!–ε‘ΕB™D‡'`ˆ… ςhΩ=TTJςŠΌ2>·"Κ ³secDk10ΏRJΚeš..β}Έ*•|Ϋκ|©Τ?FαŸ`ϊJ ΎYυ€V3%b’)1!— Ρ©βΑbωrΑW*”|‘ΡγΡPωώʊo+Ρ,J¨Β¨jΐJΕCd€Β†¦ŠbhH ω„A )!δQ*ΐwMbbΊ8‚LB"“Ϊ€” +@Z·(ζΦ#½: †― Rλ“@*=€ΆνLw@jGΞ6iγγ!UΎΠm„‡ƒπeαˏAΨ±aηW#μΪΞIΊ‘­KCΨσ5!άτ$7?ΒήνLw μCΞ^pΛ6ΒCΎ2m-Ύό…’₯/­αr#³’ΉqŽ'θΔ±Clόyψ ωω)Κ±“dΖωΒπ'žzτĝ#Σ‘ͺE3Ι‹γ`4βhϋύΏ=–ΗNΨU΅ΦT[7Ήq‡έg―±ίς‹όΧ†3†;α8€ μ/\œζπΓBφΓ²³+±¦g6’όΔρgŽσ_ +ΰu•ίΑ±ˆ{*Ξlf#ΉI(ΙUψΨ₯-ΆT:dW¬†6WΖΞΎ`ŽvDΚόN{.ΤΐžίθI1r!Ξμ(ύΈBνψ\ΐ¬ϋDίV!:’υ½&ϊ=‚},ξΞ™½>45Έmjh7€2)A!— Χ ―CnAnCή‡4Μΐ<:Χ ˆβΙioμΕεγΖ=$‚Ίm«Έ•ΜšFώŽŒ:Θ3]n—ΣΚ-Vƒ+” š²j'WΒVΉZη;ϋύqΥΣq0―pΎρž·0ΆΏ'ά6|,΅gvHf?kιMϊ{3rW{ΘΜοΖΐwZsΙTd6s¨'괎}σχƒ,“Vq=JZx+Ri¦lΣΦoΖϊΝ[λ7k£»aθ…ŒCJEΘ%Θ5Θλ[Ϋχ! 3«Έ“ρΦΌŠΦ΅ +Ιΐ"†¨VΨ+q`.©΄Ž{qΣ[qӈ›ξiΔM#nqӈ›Fά4β¦7ΈiΔM#nZΓ½޺ׁt nnt –‘NM@΅ μέ/ŠΙWW€»œf%άΙΫs\ ς¬mΛ²-,™hJΡ_½½©ΰp*Ψ°±+,R˜νΟ‹χ₯Σ±}‡cΉβ…_aέΉΆΌΏCeoΟ±φLΏ;’i8Ύ7θ;ڝ?Ϊΐ­Og’c]ΝΚP)—ΫŸκ΄:ŒΨ#6Τi κή‚&Vρ+G―:¬‹ Ζ5\xhΝΥΦΆŽa”uZI΅΅11bΑ$‘Χ¬₯Μ₯t;\™l&«Ψζb=•χ Wff^ΩXγ΅lάxη μ' ίψx6Α_ΰ z{ 0ΑcPs.8Ί¬±¨&΅ŠQΑœΊΕœ +ζT0§‚9Μ©`Ns*˜SΑœ +ζT0§‚9Μ©suλ’:tBτκp‘:ΔZχ>{QF‚·^O―¦G°sΊ?g Τu²ΪF +pO¦Ί{\Nt±³»ε¬λΘΉώρsΡΜθ~ηΎι€ΝŒ:ό騇ίHNξIœLFς“νm“ωHK,ε +φΕ½>9θNηŸκtνn·Χε 8wΥ6ΕδΑc=ΝήάΑξτH§ΏΆ±©ΥοSœ)Ψ.ύΨo­άs*L{-IΠ²†5‰²bMR©Ί> ϊηΗ]…DAϋΧΑŸ2h@υYB9ύPΛΚtεΊ+SΒQφΪίX―#¬fώΘ.[φ<;ά?;ž(~7π²e0žb}R4μιωQώμΡLl|/?7Φ65Σg§άζ/ WHΈW‘ΚΙƒCS€ηA +¦5½ΜΠeπ"ι;Vοο–#w dIΰ]οx—ΐ»ή%π.w ΌKΰ]xήΫα]œΙ»Ρ£Υ•°Ζ`%Qί. Epˆdτ=μΣt±‡›πΫ^μaρq-E΄έΟ[u xgvΤ}€ι{ZΐgΆ(YΦNωκύI%œ ˆvγwΧΉχ`ͺχHoKK_q 5εε3-ω œοτϋ“ω`0ίΡΒ~ϊpτ“h[βΐI΅{~²3ϋΕΌω);ΔίΒς§yΈŒ‘¨Ψx"vͺΠέkX‰Έ·Δ"b‚Ω‹;[θb·&Φ£•ΠJΨΡ&θμ\GVq)ΈΪͺ„g5Β³XΗ׏υvτ‡\‰ΖZw}kΣ7½ρΖ‰ΫRήn8ΛyS+£HG{6ί€.]ϋΒ»}NKn'εhŒ¦ι˜6‡αί,lH<β ½β)ΔΗζΟΌ4ΏΌxjφιω σΓηΞΜΡΏχšH +endstream +endobj +848 0 obj +<< /Filter /FlateDecode /Length1 1144 /Length2 42889 /Length3 0 /Length 43673 >> +stream +xΪt·ctfQΊ-ΫvήΨΆνTlΫycΫ¬ΈbΫfΕ¬ŠmΫ¨ΨΉΥέχœώNŸϋύc―5η³Ξ5φΨ$Jͺ "f&ζ’@WFf^€‚΅½‰›‹ͺ1PžAΕά πδ0†£ P³v΅3_τ_BΜΩάΨΥΪ(nμϊ—W³r|3v°2X˜y™Ήy9Xώ™ΩώΛΠΑ™ δlmοΰ P2w5wΆ³ώ₯ΔLέμ́ͺnŽŽvΦζf*ζ.nΞ¦ζ.Ό‹Ώ™ύο¨1G/gkK+W΅ΊŠ& ύΏ€‰Χ1qskK €ςοΒέάΞΑρ‘ώΊ2š;MΪμΆJΖfΦ(@mεκκΘΛΔδhalώct±`š»2ΡόMTh&ζ`.pθ™Έ΅³Ήιί’Ό˜ώ³oΆ@ Ο‚-¬f,ΙΜΝ‘Ihνδf.#ώBpΖ,Ν]Μ¬Μ<Μ¬s'€Ή§©Σ?Bͺy9š“dωl 4σσqtpXΫΉ˜ϋY[˜}ΑωΈ»›\έΜύ|ώΏΔάΑ±°Μ¬M]&ζ–Ηπoοas‹νΏ»:[{t™™™YΜxώ{₯w f@;―›+Ϋ›˜Τ₯T%EΤθώ³φΆuψ뒁…‹ΐΐΚΝρW)=ςp°ύ§G%cλ›σΏΛ-† ΐu ΘέΛ)―.υœ+ωuG_TΚC)ΝθH' Ά`DˆΎΊ˜ΓT3툨4Ν’Ύƒ―ηKK.ξΕ<ΏE©nEΆ¨•-nZσc#Α +©@Šλ’q؏?k8νͺ4S(β#‡ΰCρ.i£b~ 5dbΚ=Έ£Υ³Ιή{—›€ (L(€kP5$}lχΈ5=Ω F8o" ©…Λh-aϋ‘ ­*9ν„(qδΗ ›‘η 6§SΓ20;°-’—ΟΡ~3Υ"!ΕA{ι‘ΤEˆΰΩδG5—Ο|ψφΥ#ΐ•‚‹ΜΐDνE ή³NŠΈυ‰jσŠZrΥ-© Σ$=ΏΕ^Β`iΛoβF€ΦIς`ό‰ΓΜ‘ƒΩΤΡ°ξεέμαΝs•d£|mΧΡ&pØΡ’vzς©°QΟΡε4o#νΜ‹xΠAςΥyIg9Q³Sαυ 0ƒ*•άζO#°v1Ή»{,G˜= ©~’­%owΌžέΫG‹οΣΤΰβ#οHδφάtMΜθ–υh‡„j7ΐ4O`Ζ­Mk©yŸΧ¬* +ΫΨψ£\‘{ ΊPΗΔG§ΕF9’ωmχβžυΥκUα$HνφTNyςΌ²Σψ‰oΆ‚Xτ’φ-θ)ά3NωΗΫHΌΧhZΗΟk“5Eθι;’3l‹[7ݟŽs…edKAΤ l]Ο€ϋOΪΌ«ƒ5‚.½ -€WX†eϊ„Ύcα9 λζSxG½ͺρφ|{6“›9/?“Δ*Y–7ψkH”ΒΛφN“2“@¨FͺœΟœ|Η‘rgnκΧ+Ω)’Sͺζ :k‘YY.KKΌ6ΘZ+ Wγχό(Ρ—uπK,(zr퐖Ȼ+9ψ„Ν°o–EoΏΘμ\ωVδjΗ0Τz9uq…$ *\¨ϊy˝œ±­&‘!ΐμΆ%0vœΙ°šl*³]; +‘ψΰϋDΏ BŸδΑΆάυ6R<ξ5½¦Σ3"kΉeD\~1('τ•‹ap‚ΘΡŸ Gθ‹”‚\b§Μ 4Ψ›>Ν_”žfϋŠ6o­šψ‰IiŽxA&hΐσېx'ŽίIeβ(0w[Zαgγ_ Mλnΐ?;[­ Ώ„…‹}LŸ°FŠ>₯SΛ†Ξ†«ρζΨ<_>„PžπqkΉtύΕακ’€ +τΉ–¨X 2¨ soΠYΰ2ϊ’ε€c!wώΡδνe>Ό…›ˆάΌˆ.™^΅μηWzS7 n0D³e 0κΝ‡έ_§όπΪ’Τ‘τ(οΒϊyTtBn˜― υ6–¬…™ΤΪΒ“L9θߝ[(fίͺK«i<\οτ9κ‚FŸšJHφΈε·,τ,χ~²Ή£ώ‚ΉΦ&ίΛX΅―"3́‹-2­NΖφ r_aAκf¦ΎαiΦ΄ŽΏ™"γ,½Z(&Χ½M|;ι‰νεŽGŠ”Š)Le2&7«υ«ΉCEΰL£αͺ¦mϊ|›–K««σμw3°}o΄ή~G`Ν΅$“1&†d,ž{’Z(μjΗJρΩTή%ζΖζ•ΐ|­‹υjα”>Žξέρl‡ΪΡJΔJΛ^ΚD!RΣN^x-vUmτ­\³…I1«έΡΛGΔΎ—Ž•Χ§x 1ΰ³pΙ썩Piu~ήό^ή‘Ιίο+άΪ λ85σSs²d«2V΄πCΕIΟ-ή›*e»Mΰ ^9•‚π΅?ΝXŸ +©–V/Ό[λΤH)ΚΚό‡@ΪΩ'—ΟΑ±~O=ϊ˜F*Μωf^Χ1qNBˆUδ lΖT"π•ν»Eψ bpXΚ]ΞI +<"s Τ›W=Ξ`Cγ›•ξŽ8]Υ[ΈΝ~εtaΜNJNxΩ¨}γώ»ŽHρςZWΤ4ΘbL™ί€ΟΌf’ZP³ςι,¨7e€Αrμ1ΰΤaύ ŸˆΪm2Ζp.N2Ρ΅ςήο”d2eΡΣςΚ ΙΔLΩSH)#¨.ξ¨$%ƒδ€uι]U2΅09λΏPρ]>yχdDNΤR~―έψ΅Hˆ­e‰y7΄…hg€ ~ήm˜‘ .[oόΰ„aμ‚φ‰)Έ½=KOτό]τς›ΛZΐ•Ί'W ±β³{%T@D£»ωΥ\0Ο4BάgΩ+a3―9RτgΖΒ’@[°½σ€ΐϊρΦT·ς£… 4zY>fPΔh\.(y’ +τ€(β +1G"VŒbŠζ„qP’φΣΰ ξwθXˆkl}λ”«F[u‰EρχG*f‰œ…v#1σp_ΌUΫΛrΓ€MSΟ;ŒΪs χm1&ΈiΗ―¦˜‹“Nxr+e€Ώι οΟ±yVε<Υ™9΄ΤODΩ( ’;ΨμI―l*œTΒ^~žΠbχηˆ*N· T;ιΆ1ZΖ?₯ζLρ^’XΚΎ”œ!ιm,p^ζδ‹•J°ΏPIfΗ`Ÿ Œ–ι BΥ6‹†}€ΪvŠO NΌuξ’)ž ήAμΞjΦ¦—οΣ\ί†8²Ρ―²–²+‘” ]ΩνRTξΊCΟΝ{ί₯KF© £ΏΪ“ξ…yωS€Hα#sKδοψΦΗs^¬ω2έ†Νy°‘Ž`ͺ*Dvͺt,!ϊNΊ~'&ϐ2(k Σ “šS°>ψ“λ¬8€ˆMψCV?Χ(ŠbΛ%ΘE$αΠŽ#¨ˆ2FεΑΫ‘M₯Β™Ν·kδežj<@ν(½:ϊω9άv΄ЬŠ vΖ Ϋf0mΫΫ†uΎϊ–=μfΆW6/˜εΚχ¨ψZ­’ΐa:ΈB‚›pm—΄|ΞβmM:³6%ˆ +?ь7d)ρΈEΣΠfd’Όθ$fP§]ΝΞkYΣα5†κΩΎΫ +…Όˆθ0 Ό΅³A/°°ήΛ$YhpοVφΣθCSŒG‡ŸH“gB8Qr­ΘK( •F +βA°4@„έO!?|oαmΕ€*[‡2\ε(φλQ‹›ϊͺ…ΥyIVBγξέf&ω ΈlYTwĝL7©ƒ‘>4zΒLž6?‚NlJβjlϊ½Φ[τTξ½Cτα•RόώR­τ’ΡJIι+žY;l¨ν`yŒ1hΉΪ' €f3ζ{C‘οˆFΉηlΌχdŸ4tM:2I9άΣt6’ˆ―Π'<Μάιn{΄ΏΝ’Π졇™EFρN3τΎ|KBτι‘άDϋ.­}\7V`L7¦Π}x¬ΨΦνG™­BŸ4 + ¬νvˆγΝβΓQR¦‹DΒώˆ€²KgΆxKκl2ž ΐ3£“ΌzΝΔΰ₯η"La­ͺΗIΆ·ώΕΝkm¨A8άθƒΊ$ό›†ƒž*ΩΧX:κ4‡ΞϋKΓ$ΐπ€(ΗΈŽ“ΠΆ}OŽϋ­ΨΗ<ω{BDε8‰##}ξύοC«ΑQ©e’ΣJ.•O sΘMσάΑ"|ψψφ߈Οη·B4 a;δLΧr"F½τ βbΛyωG<2[­‚iKΩωŸ1Ε§Ζ«χžVKŽŒ| ”Έ2Α­0(ά~Έώ”“tUΑIrYv‡€mUΨnΩgmz7vt<ωί˜Φ%Oe„Ήm™ν {ƒYˆ*δžc ΩΛp–Œω +Ÿ!‘M*8αWΩΓ—j™Ppφ­/–OM+֊d˜†Γo24ͺ]‚ψ ςζΜθtΎNΘ=L_«αJ7όΧ‘r)Υΐα ίΡTEhT΅³ιy<„o%ασ9Έ?z¬H’ι³6”xŒ©°ˆpcΑe‚΄6.i="οχ°ΑΦΜκ\n‰4άκΏk-ˆfsLς°­έπKΠVͺ’~κ@μ‡~,υt•ŒΝΜ³‹/œϋ°ίλ‘:­ΝΈιΥ/Υlε8.~VxŠτ 1ψΝ-M:9Σ`dbv#μ’mh{uΨΒο'άΙδ§Κ ³4HΌύΥ·)+ Σ\₯ρ)Nζ΅ΫXG£U1edφ8ό¨p[-ςI(l₯΄+Nφ|„`nΌ3…Υ„.`y@ζΆ*6›³‰/c"&<ό¨[»’S7€°WXNς}΄Z“’ +pΈΌξΣΊƒέV1ΧτΦηόμaο66±‡+‚r‡ωΠςΓΚ"γ˜•bͺ?Γ#Ÿυ`½y}Ϊ/w2κŸ;œ¦ +o‘e=n +yΏ ,<ρ¬₯Ξ7ušξ°ΧΗ•δΩΝς‹wd•II<ΤΨΜαq~½–σ―Ϊ7Νiϊ…9<όD8&—ΪΐΗCx¦œΊFζώ<>&θ~4,ΐ};@S†‡ΟVΉΗ‘‚ώFξl[L―• +<¬ΞŒQQsˆΠP 5ΪH^F…ώ9·’Ω_4’λIYBΗΝ +™ΗN^ΈWΜgΚΕxgΔΨ8 ]θΊ!ΦΖ6ύγ°»’|ζs5δͺνΜ WΒΦ0OΜ“`S»{ƒΥΞϋΥ@œž*%δ©(|}ͺγ_c•ΓΘω8}©:΄›ρ‘˜ΛHο&,§z¦(,ŒςγκBΗΟ恲όσ‘†₯¨kRθΟ΄Λύ +Υ©,ͺu!E ‡|9ΨΑ»}„¦Κm€ΩηςυςςΉi-ΆUqr ΏH Λ^ΗροI2Ήι žΉ1ͺ&iC|dg(Yh&»}<Αž½:Φ‘•ΘεΈn›Άn{{ϊΆ8(i°9\¦jνΖ·¬©ύƒR«»όΐπ3hή£a€zπ°SjΖy +΅ίΣ² υέά[^ŸŒΔΖσx·ύ™¬3Ϊ‰ +ΎC₯._όάGΑ―¬ Ίœ³DΆhδώ lΘ› ’φ…lζ·4jδΒθ.κeΈδΞ©Ύχ ΆNΈsxy! κ–€¬ύ¨oApB*«Žtή7γTsh›γŸνb$“°‚!8ΰΪ>ζΠD^’+(=.#xA"φMΰδ›§§+™x§±c­Λ χ γ +€ρ^Φ«ΒυλNνq +ηSΡύP–y7ζ`υ^6o§£u +*m§ΈΔΡΤD¦EΖ:Βφ%A͏ݓ‰ά&Tt5Ι=ιc0›zg»Τx>α’raDΓ0EBζ »φHξΓ8ιjΔVΎbŸΓΌ”<=pSφJu{ΗSVizHφΏΓŸ ζT0JCj1NφΦpΚPΠγŸ 2wLμ₯XŠWφϊ&9—€±MXާ— ^[$σF'‚š­ΆΑJSr«c]Έ_(αU¬A{^ΦF^ώ”“έ‹~‘ΊΏDbwχnόJDΨ_Z3WšH~FϊpG‚g\ oΗ576~―“|9Zd|šΙŸMŠ… wΟ F›~V€β\υpm8SͺΊxKΐ˜>t…π¦Œ%AΎNΏΠp+±³ZΗX~#―C1λh'p’ϋγ & (ΐ}r0… Η^™SΠξΩςV.Vπ³CuA¨ά„Ν~؊C©Τ»›ωΦΐ3υοHaέ;υφKδ„ΐμ©ξ[Λ8ΛͺΑvΉpΦΨx΅2Qsa•ιέHA~’g6ΧΓίoφ νƒζ”‡|’yΒ ”¦] +N;:ί:)€IΙςV޳S"’ƒΙϊ (1τtJΊJ‰{ ›R)ιx0dωΖυώ*FΚώ)λazόλ#7όRαjjf~}oΩP~˜ $;x1ΆjS`Ι qžΛo€ΔωΈ‘nΉ +Ψεk4 Φx«εΨwFψVRkL―μVQPΏΥδG‘ +Ή4nωΞ$1o:“>§jh+؟€8sqJXΜ§VC&›‹Šυμό4ΧΑ‘/δ'Γ$α¦>§ΩibdQΚ!?/Hϋ8Όά–ŒJΐΘYΠΓQX1έΌͺζFOsh4)Џι[˜Θο{^Š`–l<ΌΊ8ηώιΫζώ²o‘•j“DΎ;/‰`MS;’ΓύΑβi a‰‘HšxχK‚…ΞTq!/uQ°b„Ϊ€ΖΠ@αμ;ΙN, ΄˜Ή‘l”ξΐzΦΈKέ’Ί=g8ӎBγ†ύ ωErΏΏ$9O)―ŒJ½<――gŒ“ΖFk―:ESem5Gs^sςƒ―—ύšvνv=’/ηψt―ϊι<˜ŸUΨΰšΪη‡*π*Λ‡Naι¨[†“ ;*FοŒkΪdΘ3mάPΐ»ΕβQOάγg΄<τν‹u-ΐ ›OώKΡ ž,’œΣΣm:ΰώΊϊRΘΪλζΓ\H–ΪK©`5Ι*”~RΦ†—Θ§ 4"¬ »ιά†'Χkgj)όΒήƒM σ±sq(¦J Tπ6έ`Z…Σ·xΛVς§3aˆ +η[{%1_Ξ΄Άœ~_";{»™lςuΝ’²%M™νT|ΘΎsϋuς2πΊΫ»Ϋ•.;Α<~RκθP*cΰVιV‰Ψ¬δέA}فž€‡μpY3²ΞΜΖΏ\Kœ€U Μ“FQγ+F"O#¬T Φ3Wπ­η0Ζ1βϊJΦTlvνQόΫƒοQν*8ΚΚψ›έ&’ώ’Νγ½Sxι:Γι’Q`Š•ߟgŽΝU?ˆ"ihžd[j‚„2Στύ’@%Hγjbτ7Λ’_ΡΞ‚Ώ*gΰ«$hγ z₯ +•WΈ/ζΏMŸ‚ν;N$ΒΫ[S3RnwMΔ+'–eφΦ‹vLχ¨‘$,ΤFΐp"[’sΐ’…1§El₯EێKt§ŽΣ!_Ύς~6ΎΈ―N+ΫP Mο¬xΎ{VaL„UΗ¦v+^|o€¨‘°Ž+“XΆœ«1‡Κ{θΤRνX Ϋ/·ί0~«9]3’%–}B +Rν¨ƒΜ`Ν?­.‚T0ί΅'ϊ£Y)<•Š3/Wρu9ŽrZ(3N3δΎ·Σ ΔU–Ž,nώΪMcr{υq…μ“ό…φB~QΟeΎ7[‘ΣWγ^ΊœT^έd½ίk‰€βu"-γ*ΰζY݌ˆ ΰ=RφLK)€οl <|ClκO™>νΖϊΥΌΐYrσǁπδ; ·B¦CΡΜ°Ιτζ:>sCi•SgΖO,³€_^)6«Z±Υ}%Αt4ƒx\w\ίΗΨ$ψ_οP5Wέ„λ+BΑ’UΞ%Ϋω `ƒfΒΟ(²§/RH`BRgtœΌήp„sβ-’’f<θ;ΒΏˆφDFΤ– ‘)eP’ςk«ζζΐz½ͺμFšdK§H˜€(XGΌŽ!,Vz­Vž|¦!·lxkΰ8ν€p«l‘τΡ%±ΐŠgχ8ΫkΓP%3{oΨη{6­!½IΖο ‚œaσρ€Λ3Zξ²ωsVάοrZΎH€_ŽxuF5τE| ¦HϞ]ρψ†² ‘‚$ˆXΏ†γκbώHΦe4ο+a`CtΗΓ£4Wά}0mirκˆγ²πXr5Y«{%:Žςuχ4[η Ήδ|ͺd’N]υ2₯­Ξ±&_†ω}(“―ΫγSvv ότΠμzζr€^vλ”ίΏx¦Œ«xŸ~4>ZL¬5²5ΘΝφϋZnΦ DfώXΪμŒwƒΪY8PxͺμΓ»iKΉzΦ6 P†­ΧΨhμZγ]€€EΘ‹Ž¦ήΧζL:Λ₯­·ήγˆad‘­q>GΫ_K$4Οφ½L;$0Κ(ΈJύΤήa¨ ΟE¬Δ^κυGCοΟζn4ΐŽU«nοX’’ MΌ?i]qΊΟτηζ.˜Q΅Z@ K +₯ή€Zθψ•φιOy"Ψ!oBZ© Γ}ΥΤ2ζ ΈηηS«1ΡΚ·W`0ΙΆΉβ~ΊSe.ΫƝi+‡QΡ°1Ox€jΗx=[ζΡ;M—?¦\Η4ςN\uƒqv”qQDpρh?rΠRzήcSEψ`Ždkoxθ“μkHθ،Λ5ŸZΌ!ΔΗZό—֝Ή|gκ #)?Β“$i΄Ϊh¦)+n( ΝΘ΅€…`όΗ²ϊN–ϋΡ£¦Ά.HΥι„Φ•ΐo_(n):γΑ2‘|έ+ΨΞτlp [ͺ™γ`qΦgJBΚ|Œ*³_¦ |πd(ŒRξ=~ΩT8bαŸ²'79ΌωKTΙ1\aΉY8šfMŠIˆ€‡ˆK~Œ†¦λΕHtsœz&σ·F>³—&u›kf6±]ŒσΨHZ4ϊνεd κι!0™”‡’›'ςύΊ΄Œ9β_δΓ48 —ξΏΠW­_¨Β†λΖd·―Ώά=}δ¬ίς`²Ž[«&ΩxΕεΝ¦=Ο³uκσ&K€¦šž» p…›—Ίέ&ΙEt%FοατέΌ#0Ύˆ•zκ„ ±μΘu‘§}»½οqϋΙΠ׍6 |žίX= ΜnέΫ @#sNδ–@gξΰ­ΓV|k«']Š΄UΝJΒMPZ―όχ7*Β(ΖΓωxlsspΘ}΅›“rγ#)»-7ν!―&θδ°χ]ΩΠΝLΠ+ŽKόΰn +#€b»šψΔP›ρj…BΫQ΅!iρΧύχV8³\­λ˜aΞΊ]Ν«Ώ XM%¨5q$§.£ΩŽ'νΡΕα<<ε¨ΡΗβμv>ΟΪf©›yŽβσσΏλ·ήΛc†•ωοcoεˆ8ή”)™:Κ±jrg‘7Ζg|ρ³ŠύFž–q–#Άx†xBΠ?χB%\m^κJ~˜^ο]―‘A!k#šV”†ΞΰΎbW>"Δ'όlnΘͺLόΖFU›τƒ),W€Τ­Ώ«œkλ&#ZΗ³^Ϊ8C6hΩ\dέ¨ΉΦZη`zΓεAθΓbrδΥAΩl1΅dη'eξψ}CdΜbTΟE*d?©N΅^ΘΩkn·†β§­I$0Ψ$Y6η]Ι±β†_uμψα"wznT؝em`βΊ7aVrύŠr½δQσρ=K $H¬w„ώ ₯ΐ΅¬2]=™ΣelO>―R!ΘLΤ=zŒj|Ϊ*ύΕ¨˜,M8‘LͺΫfβŽ1Bb―-›δ–/MΎDίI:°ΔpΈπΗλϊ'S£ΧoG Τ:¦RΪΘϊλ—Š9<לž’F1Ib r½ ζ]ٍδ,Κ£ϋ&¬HΞ–UΖ"3’+Ύ*2ΥC\ϋ@²AΕ’ΟUΘΤaκ΅ωΤ΅.c[t+/ηΕβλπ] RX7mό­žέ·WΫF9έXΊQ£ Ψ;%T‡9•lέœ…±+σiδάΰ<ζλΊ Zu^΄livQE§€δˊŸΠΜ;d†Δ(£ΐo“Ν»Ε2½tA= '&AρΛ!*ψΥ+}.ƒΠΡίX”4§˜j!³νnα”ιžUW_MΎ’Γ>NdΎ]κΥ΅ΉώόΖ"xΆ§$ΥΣ¦Q}ΆcSό«Οtτ—έ°κφcJ)9¦fΏλη‚ρ fYΊ‚Ό5Œ\Jš€Ή+fK˜ΒΠ›!B)7φΉŒ$₯d΅Α,nΗoε6©,εŠάpO»'…:έΠ‚pϊšœo΅\«‘φΤφpδΝ.°()Ι©α ­Ύ)Χ{Hέ‰F;Ηz‡6ο W6¦Έ²cΫ±™)š)ͺ8΄ι“φ~ΕΑΪψBKμΈΆbςJNd8o€έ(πάP +lγ‘±U€€‡ƒ–:ͺϋ+Ξ!Μu€AzHžNΐu>$hλΙ–ΜΛ;!a’ΈO% +{@VγoŠ?H§ZΊ‘{¨?kό‡?Γα±kρUρˆ―„QŠtᬙ“Ii“u3[<WΟ™|2XΟΧσaΞ9C΄m‰Z[˜–Γήof άb.ιέpA%© 㲈7Bš9λσ­l„Ξt{[ώ°΄…θ%.°vε ό=+l ΏέEE מ»YΕrƒν`-Ob‹N9Θ_ŒU‘Ωf dyhnX°}“4ΜbAœ CβΙŠέ !ŠQ7Yδή­lk’σθX³I,4U\;|Η‹¦’dτ άΨ± $ώƌ\π/™8'"LB%€υ·ρDΞ9jγA"ΉΎBί± p¦*“Π{³—f οͺΝ~8ύPψφ΅γS0(S?’bTξ­…@`Γ1PΡ₯±n°SŽOC—&άe–ωy›ΉtΏΡ^Ψi+Ή2μ;τΞΤδ/!7ψλΖ‚e§Vό²θ£ s\JnTνωΫΫ8;±₯Υζ=PΈ{ξGPcCΥ΄Κ +₯C#΅A‚4ΩVeφ°ˆ«Σ—ΞωΒΓGπ•ν!εxtΊ°»dω2К²Ύ0 ‘Šφ―%K/ΜN˜bΈ΄v|·ˆ^) ]Ν9ϊ@„…«μ©έ/imm+Rβ[δ/”ŸW›Η€Ίp ‡§@M«ϋΙ΅UΥΆ³bς\K-m@ό‘qŒMg°4°α8 ΎΜώQΓ€Q+¨δν°ΜΉ Ίή. +ϋS«=<Šν3ƒΣ‹-yΨ=‘ή ½Χd8η_KυΩ*1~3GΜΡΉ8tόŸδ•j9ΎΌRdΊ±­ΙC‘2%χiaBPθHρ«˜42<ιVGC‘΄ƒ Cλ%™lί‹6Ψb—~‰A>ρτςg(γ,ωϋω€(6% Ι»­ +JOw₯A‘ DΉFδq_Ÿ$O FTaHŠBΚ1ΡΉ”.ψuv:¬DΞG±χͺΝŽ"¦Ξ8ΦۊΆ­4ΎΗF#ΞxγaΗw9•r*§’p]&bέΠε·²ζΈaτu €Σ w:ύ„³d©FqΠbγ?U¦ ?υρ4X½Ÿ­°*΅1%7©κ£td ,9±neMbΕ2§ΏΘ+‘•‰πψΡΩ/νψ/ωsνΉTόXΑbHΨUόΝ―· iκιqκ£’Θ,1g;gιύ–Iδ ν‹^κs’ežσ2mAY3Ώ―g¦ΦΫ†¦εy†άωζ^α΅ΰlܚ'nθǟΰZˆΓΙ…Ω'δΡΌr…§Q,ΖΆΉεΫΰƒΟ -EyοJϋ„(ΐE¨χTbšωΠΪ ‰Ueu°βb'Ϋv2…ͺy\ρΠ–]‘νέΌ›T˜Q[–“gΖΓΓςmώΐ«¨Ψμγ6ο·&χύ.ϊYM.ΩrFEΡ\‚!€‡w/δ-t›ΥΆYp7€Eς₯ΣΌƒ^WΙ>,τ°5ωcrŸψžμgΣ~³φYŽ&ϋZšsŽ+Ÿμ@τgίkλ EΒ†άΣσ•K‡ŒΈDότ0ž„ +O<Žλ|‘€y’.Ρ‹z?‘„­#–©k^ϊtΑoϊUέ=ŒO%Hu^£;!Šœ5ωvXνβΠ_JΈ2ͺr0IŠ-<ϊ•k†tca²ΡύύχzƒtεΗυ›πeΦΆΘmŸ†t΄Λƒ‡'’υ$Π6ϊF %΄–„Z€Υ…£Ο\–Β0%δCΓΡUiάΙφ¬»λΑY榟w”}ZzpΞλfΰ£t¨™~Πg³υŸ₯ΤΪγhΩ–PwΑG>%2 δεωzδ:ΪBΏδκh―Ά…ΐ^ρzέY¦β-6δŸφΦ@˜~»3:]~G֜ΰo8Ό­Œ­ž·ί2=ύPσ„KΑK¨6ΣQ@*»AΩ{_/²C€2`Αqς•lͺ>8ƒNζ %@•υ@,$πδ+lΐ`$“νt‚l”εώqW[zήe—˜5όΩ`δ`ΥΫΫͺ_˜%J8/Iγδ‚FA7˜­©ξšθΙ΁8₯(\ͺI~O˜j„ξΧΪΊπ* [šύ[σv JL ¦ίΘ]-9Ί²¬-F£˜žR”Ε/ k +—8RκΖSŠΌΏδ@β8«XΦmƒ2κ7­:+ŽO›¦dκΞ……-ΆΦΊw5’fͺΑ†W«•¦Ι kπβƒ'Οη y—2K˜JΏY”Θ,²ΏΕGG―<Β«<5HLΙΈjΑ4|Έ{IEz—εœΨHΝPfΰ¦H‹eOo—Fᯎdρ΄“Qζ°S&rΆw +Š”Μ“t¬-ύ1βW«?;ΐ֜h…[‚ΘyΊΊΑΗ{ΚΡی φA8gήφdD_° χτ[γιδΥP>Y‰ζ‘ZΝ/JΚζΥ©G ΰ*Έπs+Μυi³Jι%Α$ύsννθvΚ CTŸN+”μ„•ΰONb‹cχpGΞU²΄ΓΘwΥΔ€ΡΠΓ₯ϊ+G"΄η»ReίΟ¦›΄#ν†τ]’Ϋ[ƒΫΑmbIΖ(‘₯υ>Σv-–ΜˆΘ,&ι‹Oˆ±Κ₯K,€A|žiW^KΖΧQ+ωΑψ΄.ϋjβžHϊβV ψ””=σj‰!ƚ‚ΐ Βe:‹νηCyζ’«£'£Ψ½μκωu£μΡjΤα!―wΫ –π>΄ψΦuλo #Pf‚e{B^oΦ~,¬šύΒ•Φp’α³μœνO•τvLqH/ec’rΫr½NώΞΘ*zJψXv^IqΫ™€l Όά0qοΰξΫ©Άχΐ—ΏJΟ|m¨?ψ­ ήΉqΧPΡΧ(i ¦ΓIsή#“E›‹oRΠΪZ7ŒHPP }h΅ ψΙhEΈ7J©yΌO"£Ήσβ ΘΣΜraς(›js£Š­¬‡cQ–X,uRŒx…³Ύ‚ŠŒa4Ε%]2€ϊt°Τ-N&ƒωˆTQΟ8¦όκ!4¨(Ξ‘]TqoL‘Tb—•Ά_ϋΘg―Ÿ€Θ;β{pέτo»9κžΓσΐTŒν”ί…bS@<C…”&5;)Fhν―ιΑίςoS—Ξ₯ΜΥ*ΤΩqœ’0–Φ`Μ`-Γj:#X… Ή|XζQoό•[Έ§Νϊ­Π狝±Šεα5H7±WδΝ‹ZΛΗiΌ$8{H/c᎚BIίrŽ‚’„#_2rϋ4s:_ψωœ€ΈυμK|΄‚IqN½.ηϊ‡ΪθΥ΅=δ|β§#ΊIv’8χJt‹ήρ «X μXωLdpΙI…pš(KΕ&―¦ΗQi xκ)6DσΦ½klŽU”& œΖL9Š€™½οJΘFΜ'δW(t”Ζ‡Η7ω5HΫT·‘|Ήύφί(MwΙ² ΔYέu> +s‚"o18”>’I˜ΝΡ^Ύ_n΄%zΰfλ²?ΉΤ΄%μSσ°ζNοoZδŠHc¨˜ΌL·―½Δτ¨„‚ο#Q…*»dΡ‰³r’7Ψ-0²ίrZΪ*9ZΟή‹Δ¦Υ[&šM±Ÿ~BI6Β–Ρ=w‘NΩΚ χ†Σoˆ”ΝvvΉC‰ΉΩ=σ³ΤμeAη_–ΚžΖ:M’‹m—Β=φ=£Ξέ?p+Pθμθ7JCž.–;VΊ± |Ϋ€38Ά™½Όα˜ΉοχΩhΌ˜`YβνΩ*tΝHέHI‘ΎR«˜&™ς ΥDHλ…έŒϋ3S8‚έCEΔcd\v.ψ»=V+Ν䆋K7ΛLΨ~Ρj€ΕΫ\•\nΫ€ZvΤ‹όψ,Ψd…„U‘–­rΞ7ΪkθεPφ—=₯ζ―£βΤ|λόσ.βMTΗY°ωΩχυβ;$ΩΗλ!Ή$ͺΑ6g{.]‡έ’” % ΧPσ6Ι’2Η=:‚‘Q¦s>ΫψœŠΎ«‡‹,£@Ή}ΆuΫ’{|ŸͺψH §ΔQŸ”ωFΐ;l™ε6δ=u9—rͺŒΕ<.–œO|Έτ՜Ϊ ƒ΄ΣΜ¨Ϊ―ΐΟt¨Οα!ΆχΑφίψΜςΈMs©ρXxvDΧΛ}6EmσT‘>–Ψsͺt“Ύ>œe_–ˆ³ϊΝi;Z€ ΠtΈ#Βo­E°Œe€e‰`ˆ‚}%…¦VΡC’εΜΉQœ5B;R΅«ΩxΈΕυ³υΎ»ΞΟλΣ υΎωZcΘEΛ;ρœ[ΟLN.o^ΰϋh!:”†ϊ!QzχiW™ΐœ$/¨Θξ +|"u„ΰΜΖ<ό„VuζN‹Œ$<0Ν~Rίukτ"¨„TΑZ<—KŽ―˜ ͺEΓ(oδ•f<\Ώ½Αj[ξΕVυAVš£-1 O\Ό%/χ™‰Λ1KΈr¬ƒΆB πΠ —η΅λ8|%Ζy“—ή ’κa‘˜ΊAF[V"―ςΓQŒͺwš†Έ”F-‰βΛwκkάf hΡ‡δœ°ΨsΆZ­\…^ΒιH’€ΈqΥ~ g Œό]*ΔΧwόƒώρΖ[iyϋTͺ’am>|σΟ°1UΦΌ¬5`Oφ)`¦ςM‘j˜-%~©G0ΕΫkΝΑŠυν²ecb;Ζ²3γϊΊI”{%γξΝ<Ρ/q –X₯ΨpΊΊΗρ/¬θΑηw”ύΪy1]K Bb₯Λο%΄¨ςJξ*()«΄τc€¨-iω™«(₯©“ϋΐΞ²άP²BWήΥͺι#·Ω—σ έ'€l‡  4·r΅·gαrŽœΖό·ΡίϋD9‘«oΆ ¬0Œž₯νύ飩ζΌ<ΎΤ~h‰χCDΛƒΪδ̍»δzή’M”ί―\<§Τy@hI+€Ό—j£fbλže•v½”a$δΠΧ†Ξ¨RρXQAε4y,c£V:[ϋ醨d΄fμu߈§ γψFοΉΜ*0(Μ’@?^ # Χ[ωΦ(κ~YίwQ % }¬~“£ψ wΛΌW°{ƒε¦£œνξ†Ήζc³ή|«½`ΝΰΠoŸΠι9 ΤκΗΡg7P ^טQ‚04kβv€ώdΟ™Ω5wΗx|-€0O{ηZUjZ7‡ gAY·§Δ£ΦhZ’K\r3ni&Z°‘ζ|§Γλ5a˜G΄―nTSmTHo8~χΪOpγθŠ.ϊ­χ=‰Ϊβ=]ΗzNβ¨Ταξκ,Ε­χΝ‹;&₯Ώπl퍐<έ“ΟΈΆΆqϋˆ₯αλU6„Σ )κίœΊ „€`&x6‰ώb·&3Λ΅ςσTό•Ο ή–·“θ0„Ά9;ζaݚΈ%–DjΞζ4JΦbϋ¬Ÿ7(«Ο}cUz·m£βƒϊqΘ]bη‚,›IδŠΪΨΡϋφa"εβΒ„˜Αx¬‚Ά’Wτ­A[YοŽ]Τ=”ŽλΕώDΌ$ΰ˜VπWMN ©Ύ μ½ FYΉ{ΞqΊs0ΞhPi}¬ν#™¨ζ6#Ζ6Ζ`-#οSxgΠ‘©hβ™‘όFwή8ρ₯΄‡ͺZnk«nΜΩ^βo¨ύV“^πώ\ m Ο―„™0W?—?άΉΥ<1δ",•«I’:ΖέΊ›m7£)ΉΌΐ_Ώ&›Eσ‰ΟA €ΩΦΟ΄˜*ά&ιn“ΚΘDfΈ:Κh$ˆ8μχσ:P2ͺI‹ΘBϊZωήΦχΌ(ξ40OΆ4€yΝtžžΚ+pΧΌΔ‰ύξρwτt*“ϋ/―sσu2ϊ5T’Ε jΥ‹Œl:σyԐ‘0X)QΦό˜€Ί%{UίάΏζGKj[Q;q;κχ"Σ>ο4;€Δ―°َδPDο"sύ+!b‘Α\}εƒ$³|φΛ»Ω«ΒL΅…qθkΪnb^ ²˜lίR>R6¨QΑό%ypˆ/€©p¦αŸΎT5ΰ(FΛ]rHxϋͺκίεCήΐ§μ^e‘ žS$ «`[ex‡…WοΌ*Ψs*…Hy3‰ϋχΞΧW\ώ @φΏCŒ +cΐ!λWkb.lv`YXυο]‘zΈε»rkaΊ €ςά_4ψ3σΣU·μuΨθKΡΛCƒΨΧo|χσTCbtηf ৈ2A-rΫη* ή`?bƒΒHEκθ‘ySI…žΩ'i@3 ε•;6*­ +Ώ•}=ΨψΗΎ«΄Y]κIχ¬΅βyΩ\ΰίοβ*ŠKηŸJΣV^yOlΚRμ-©Fϋθ•?sήφNDrcBφΙ³šά3―%’’•ZΊΩ‚ΟΞ9¦t˜΅θajδθζ΅Yu4pΈPQνMF„π―­v₯‰yY―…δΓ1ηφqΝ8Š30ΰξ^iͺΥι›σΌ‚• +œ.™Κ)œ +\CdΖNΏω&I•KΙχ νeg~ΫQΝu‰πCŠO&8Z1©3A³h—±q§ΞΥN6©ΒšΩNŠά§‡FΖ‰+ŠΖ½_RSšυ­ήpΒσθn’ΐ΄ 1ίR_ντ±BcΫ@λί0;·Νf _ϋŽΝ“o2Λ­Χx 6VuΤd~ Ÿw[΄ ―±€NΨ֞`kξCΈŽ”Υ‚ήΗ…Πžρίκ?3 ˜»B0ή_‡lXΎV~‰kΦp<ι’Ο™ηšŸω§ΎΰΐaE»%g*R5‹€Ϊ4θvyǞЬ₯₯‹£Ž{«ξƒ 5ΏolΪBeωθˆ™ΝρvNΜΓβΕh›œ={ωΟ„;ΣK,hqύ'‰ΰŸm―=‰&|Š9βfIL|’PAή;εΤG΅’OvΗm;eρq{ls_šrSVΦxL=‚l%ΩL]⬂δlvn– H˜g3gN(3νV@nΈξλ ‚wΆJ§ξY^} ޞύlG©{`ί_Α•γΪΒδσΘ›xͺb^υ Ž­υ LΉn―\θ~n€FδΞϊŸί{Γ†’˜U m‡w%IN[@QπŒkΚ=Ιgσ€?½Ώ9”€μhΟΑςeEv2ΎΆ‹uF(ΤΝΥr.υ(jζΈB†Ό7oΓ»7›έš°1Ρ&8K™ώLΞ}lŸ₯—πg\όΎ–+4RŸΦ(H’0%²α}~87•<«guΟUQΛΠ Ύ€—ΒΓό‚Ε&*0–ώwŽ…»Uvl7~Χη υ=cΫ{- +m3ŠΟ:³}…0ΘvγW$B½'φ8ž7+ΐ0£Β*Δ0žxƒ6—χ[WΉ ȟ{Wύ ΅e{qΏψμT”'R΅r&Ψqήΐ[ΔYΤ;θΞώQ7}ΐ°‡¬»yM;8Υγbέ\DΏΊΨuΝ,#.‘³ξr7Ή1gΤωϊυn_f 1ςΞΌmE²Ο«~Ά<μθ€iδΪ―5x:ς³χ±Ξ@Φ#Řθ|ΕU88\s•Θςoλ_ηΏ((pύ–9²© Ά™²°†x 9^œΓ,ŒΚΡχj¦YππμD±‘|E“XV­£Q#ΚμΦΆ‰p_Οξνr78^‘ Ϋ" (hžQZM›’žyQŠtέOŽσφjΪζύ(’Β%Λ’Βιλq(όΥ‘ΕYφ_yμ06~oω₯ 1ώ02…έ*L³ΛΎ=ΌΩdK\Ω₯ΓwOήζΞ1Lξό…lMθlλŠ†]”’εžΌ#ιI{²Tύ¬Ϋ΄*xmβ!ΑοΊ½ηύ‹λ$κΆ"lΗη’MζΎeϋ~μΚσΕεΫ° *§ΊoGŽH΄žˆ £‰΄–ƒ<δl8%`μ‰ξ΄ΐuΐeOΠΉλξΪ ψ'±`ggv jΪk7"ŒάC\£Ίϋ-ώͺΒlƒΔ';Ύω<ΐGΜ­ΞηχΟ*€§πχR _=FT„œž§”CT³/°+ξ1W°θπΗξυœ$>9Οϋ‡…π™Νθ…ŠΛb±CɊΎ»ΨΛ3¦s¦_39uuΐΘο§ν1^5ζόΡ'm–ΞπρΗθβ_ΈO`«nΔ|uκ€i~5€?εΙ-6©†8½όήΑœ17š‰,ϋΖφ(2υ*]@εν™17šΟΆωƒ-Ύ άUiq>†^Cεt[WS΅šΉZK+S_βXRSuSS6lqυ_@ 1w«†δ£ΠJΎΜlvgMπo&Nq1D¬;zξrAΙ’TΤβZΆCTIGιRaψ Δ +1γ₯Iω”›€Χžd0Θ]ϋ1‘π}rΞρ>Ρφ‚TθΜΦΜβγg±­5Rp3Κt˜χ¬δŒ›ͺ²Ξΐπžw š“!VM“Α?iγ D?/Ρ€ΰ{Su› τ$[Žkl$O½―#„D…+θπQΩ--‘hv ›ΏˆςL¨‹#‘ !ν[υ ψ}i±”]¦“›λπ +Κ%ΪNΜ†EΚδT?·]0Ζh*%Ξν‘tFϊCϋXwΜCς‘gαR tβ’Ρήw%ˆLθ4OH)φμΚCωXec?vΩΉ’f―Έ«… sψΗ“(ƒ•ϋ£δ>Θdρ?^‚εšqκGί:ODg*Χ7ΦCsͺ΄ΰnΝ{–o‘'€Μξχfσ"εuι…ί  ~67Α]‚^! 8β’Zs&b'ΨϋVΚΚyά£€dνΟ‡R³Γ©ODέΦXγ—kόMε³>«εͺ y†Ei֞y ΝΉ„Cβ†+αi²Cv© ³¦ŽΖ[8¨WάΡ½zoσcυΚ3Œg5!άjcΪΤ[<;ύc0H6ΊΑxA¨΄Q¬žΘο¦Χψ0tω α¬4ϋRwΠ]‡ΕΏŒ¦ι9νρεyΆlνΤπώΠ© eΉτΘ‹ό~)Ϊ…8Ξ_οŽό2υΕLε¨zΟnVHi θ'Ώ―γ?BΤkςn5­Ίΐ@¬Ε—p(Žx„ ]±ŠpaΡ¦Ή=ηω?@Vk7Ia2v˜νƒ,Η.Y’ζ<9ζΥ’θ8aŸΧΓχ&Ÿ +Ÿ8 +v£sΉΎ’«Oγ€ά;ƒZ!$L!ΪδΖTΙ•€sωeƒ²ψϊ-ΤL9΅κ…ϋ:ϋΐHGΜ ΈώBφXž»b©5@‰•8 +«‰ϊˆ}³eW‡]Y +%κ‰xW_Ώ}ŠΔ‘V™8 °β&ΣEPΣΟY …aΪ'67G›Ύ@ΏΘ°Y€σ”°ώτΔͺγJ)VKs7Ρyz―ցΫύΰ`΄΅·Έ•ω«Ξέσ¦œHBθΖ™·Κι‘ρ}z„1ΘzEo2λbCœZΞ4%$εΙΡ‡ώ}ΐΧΎ1­ΆΦΞ”Υxq]³ΕšSΉΕZΝ~?qδ aσΐ»Ο.O| +-Ο'}ώqέ>+Ιrφ ΧεV₯‰ο+ώέ$3”%Ρ——ιΡΥrΜ³naβχkžIԜY‹eΨcσšΡκ!`Ηm&ή„°ΎGUBd"•Ÿόλ@λέ΄¨Ž•¬jZoζΟ,mrΤ₯ ζˆ‹Κ¨UβΩρf@?\βώδL#ΫIίη0¬λψ™Hε2ΊGωI§qΜkΟ΅“‹ω8Ί‘Ο―A=Σά’k|œ-³1ΦRάΈ<εmεhŸ­eωAY«d_γο΄(Yu<ί‰ΐκ™k[¨})ΈƒoΎUΓρC¨ύ7οk6΄τ½F+[¨n’‡{žΜ€Βe¬){Ν§έΫw·"^Κ e-•y:ΨεO˜S˜™Ϊ]žΪη$=m°•E,Sݏc™gw?…KHΏ©½π¬α–ΡVΣΘCΜ+ϊ1X x™Gϊ5`τš@l“O΅J)Ννξϊ›«jάdΰI$j–J"ί#…OCεY{ͺ"8₯©ΕŒ|­•ά‹Ω(ρΎ߾5¨QrΦχeτzBΩQζQ:€TH[ˆ)jͺ]²C8Δ©mƒώ…}ŠΗπ@―iγ4Jχ.Ϊ3 »‹"ΌœVΖVa] ~ψ„³ΗϊΞ–ωZΨQ{YΎcI +x +έΤ ž]€δΆΘh2q<Λ₯@%Ꮠΰ;JΗǁiΉrΘbY£ΡYλ +ΆύͺkέH /EΟθ Τ"‹΅š‚yοβεT9|¦C‚PΘ―lφέ‰ΗƒΏζ%”ŸS3K"ΞL{³UρYΘ䊱Ž΅Δ΅¦Q²‘ ΈΊέ++eΫΓΤR4CD$Εxh²ήTsVJ3ž4 +ΠSfyΓfω cΑ/Η?σS~wΛ«κΪPӟ63»œB/ΕdΊbͺ=#‰VΙ ϋ„΄%A£9Gκ°Ο‚l Ձιξ JώΔΡ,τ€·xn[Ρθ›…€βŸrhΏΉlοάΙΐ¨0“g&’ΖΩX€±ΧDΧb,―“u%OσάυŒbއ&$v[°i\’ύδ3ΞY3)‹Nμ’~…λ€ίbΩδ\$gV­ 4Λ7£΄‹ˆ¨ΖεYVFštΥ5-εY€]o ΣP"D¦σqU©!ήڝC²Νκ;ορ­έαRηΌθΛwιΕeεwRόΌο‡U Š― Θ*ΥcK•y„¨ΕΗΚ9[Θ5–ζ[νZϊΦ@Λμ-vΈΪVΣrΟ^σ¦5β$βΐΠηΙ2Ni «RΜ?@Υ™£fO]ιaΆ­λ―Dsι°~¦ω’V Dlitož’Ο!>|Hίςΐί\gŠ.»ΦΉ+„Ϊq­ZtWƒBιQ’­­l¨Σ,nuω DΩσε“°”SΞtqΕχσ$υ§L-ΨWΈS£„0HΟΠ–ΙM‚wνε4ϋ-τ†!΄~O°~54°²Xωζ’ϋ©—Βz)€:lΑ.ΈvτΤGψPΏ?ΪΛ< #N†"΄Rƒe3Ιp˜9°Ν’s°…mχ{γ‚Y―Θ©ΝEuιΓ2G>™Β(˜ΩΌ` ΥbΰT­οX +d~δN2?•eE_?:Fΰ#QαSM%dα’—uΪ§Cӟy΅8D6η`Ώ,ς6 tΚDΨsΆ}ΌώΠ% +ͺ +œΝ|¨{«εN'eyϋ†rΌΌ—*XύΖ;pχΦ~Ώ~A†ωω7SaβqΦ²ϊ!Ι[BΪζYFKΔ¨`‡ΰ`Ì·ƒΥ_Mζ—±jδcέ FHώ ΈΝK".$ Mυ;§ŒnH΄Η§γŽ#†I›ήyΈywϊΥΎ-ζb±Ύ2Φ!CμάΑΩ–Χυΐώη­ΩΙsΖt!,2w%θχΞδ%:4}θΊκΌ6x'‘,GΎΨs7Hιr­!=ӐΝ'χ>ΕUqHk±ΧΏχΈ’mΆIE2Φ°«υΣ-ϊρύω\VQ PfΔAD“‡μ^)εΖ"γ1φνl8iraώ6y(Ιb]΄Χ#·+9 Οΐ)‘l\·yœ2(Έ–g¦ξ"{ v§ITiΚT”ΚHΞws*–|e‚Α—Κ=Pn—Ž₯π Ιϊ7`ρHY±ΨεΚ”wπ©;0LΪ ‘z ·tHΊ‰5™?‘ςtŠnƒ JuόŽhN HαάλΧΩ’΄BQN™€Uνnf#&U%‰΅Ga‡τζ˜β-8?ΰeρ ϟXΫί +¨²bΫΡzλ"W6―Έ‹Pi~g¦xΪ£TΏešΚ16;­½›ΝˆK―=*ϊ’£:STfΰFƒt/_’ΑμΕ[…@θqΒœΚۊ ƒB‹IŸV—fΤl0·0V! Ώτ‰‘κ§HH H[ΜzΑšςˆΜa«π%;9ξϊ΅Ϋε[μΑ +`.ρΖe›Ώ‹ZxόˆPn²)HCVMμ^kQ§¬eυ°7O* ϊ.¬₯ώΠ»υÜ ηhΕNf=+χ:3§τy¨Ζ™; ί‘„§£εhWΞΗΫ+‹‡Ία&υa π»Y ώƒcγaσƒqΜz–ˆ'I¨§ωεγάP6Φ‹œΝί WK?—ψ`¦θv,lΝ:*ΡπΧBj‘t…ΌψIφξ L\vV·Θ —6ζβΕMΛO†D_ _|οgQά0η"Βχ°0ˆπΘςξΠΧtΏΠςŸά¦ΒΑэΪ7ΐΧ7Tfϊψv¬ ’f΄ΐͺϋˆ γ―Ψψ‡υυg@‡`φβZςΒkτ—bX8Γ°JK°©V'A] μMΫ’wؘυτΌqzδ‘™γȎM£lΚϊο,(>Ÿfά>Ε  p¦’Ύ»n‰u‰ +9ξΪ ΰŸ;\Ύ_9ξBλwRΒ`εΟΐtjyHΎ/~$‡΅α£ ϋDώ`ΚofώOσ4Ÿά΄N ΊlΠA;1xλ)‹·Κ +ΏH9΅ETΡ “­€£A©ΙθeΒ³+gώž‘ΧΣιΤήZΝ₯(ύ`s5£DMpΙόΖo6ΪShΈΧςήBU + o€~±–ύ41ϋ~WF0,fγ°$ι%ΘΣ_o‡;΄xb -«R]ίΪ*f΅'Θ{͍Ωt?€'-ιs–63Ϋ8l ΎVpTιš‡΅0€€šΪβςTZCΧs7JCε€qΣ~°Ή·ΠYΫjJq¨ληΘU"‰ΙXΓ2’€Υw³Z"D5ΪOs—ŸO’τ™H‚³%¨Yΐ’‘ύόpΓΥ¬ΥΙ?ž˜TΘ,{­hwž–ΎKŒ‡½qΞ¨=„ΛwΗn Oš‘ώ9Z#8 Φ†9{…ψχΤ)3 Ÿ»Βά¦Λ)‘4Wž#¬Υύγ1ΖOς[x΄"‹V―υΩ;YΉ§‡ηΙhk\Pͺ―“†πK%πήΏ†K΄ψτ“ρΚPΟkσnοz;kφ?|4μ~=™2—1!۝β₯.`}œΘγω‡σΗ§τΥΏ_…eρΑzΊ5η±$ψιΏS5τ~σ΄lo^ΊΟ%ΑIΊ»Ž$™LόA›­*ΠΗΦΊqAZ sκΛψ^©ΆΐJf…)}wόζμςύζν‘Ž{-*iλ0·˜XKj+†g†κ\0ŠfέΨ1΄Uθ+‰¬J—ͺ46͍ΦΒ~gJ:·Χϋw=YTκυQσ::uJT„ FM}Ÿ  <Ž€aΈ‰>8·‡wΥΧb…N.3bxi6#Rp ψνΣΨΤ(Ί’- 3i\Χ₯ΰ+cΈωM•.‘fœΉ ψd³₯οΩ•²$ζKƒ)—b₯‰’ΜZσͺΗ`x9&“ωάt½–νΔΎ Σ‚έΓ0u9»©q­Δ=šΪ #σ#Έ_kb{cώΟA›hΧBvετΔΌ ;sΧ¬ύ1°I%Έθ–ΧγώΛ4kZxYΣϊŒΥ±•άUt΄³)v>,=ηIiμ(FωEτΊ.bΰμm{bŸ Ν!`ΨάaJX…—# + ν­;ϋθzŸύ_NeΘ3–ΐ9‘e D8>Œν3ΰαLI•†c^-DΪC4Ζ\ωА^ςd˜ “c» `‘ŽŒΚ¨&&§n\h{ΏΦ‘——Ÿa- A1ϊ‹Ϋΰcˆτσgž²$7E”θ»'auΝU§κ©΅ϋΤ}D07nœZ€ΛΥΆ,ΧθRΫ©ΞQ"#5΅P Ηπ=™ΩŠ9^KY•οi χ|ΥΪ€I’„Ύ σήQ_NαSi0¨Dcξ––(ΫΒmΊm(ψ+ŽRY#¨ˆ“Ή„lΖZŽΧK ;Y IΑΐοZοΌiΪ~Ήί¨ D›?@ŒM§4Χ<`3 1ΆOŸ˜ΗTN]‡-•Gΰ[ov&_jΊθTA·­ίPςr’m"6΅ϋΩBά³μjγΉΜ@KΎW5vηήΓa I_ΧZmΔK±αV'ΡNΟΖμ>žd]υΣzΧˆ?W%r‹η©©³—ψ9PžK- h>ފFΗ~πJι!‚§ιΛ«ιlf¦?aA·!¨&‚½1 wHƒ¦υRΡnRΎΑ10ž|ΘZqΩϊ—“Σ2§ŸΌΊz)P8…‘τFΊ¬*2,‚΅HΆ‡+ŽK―Α–όyg…Γqq@yδύ‹XΛ Ήέ]mœE€1. α¬Ϋ+Sϊ€;qΨ­.[ +γXx›Τqί6YΩ ΏͺΖγ΄9)Έ 6'ΘCώϋtivΣ«aάΜŸOb㱍ιϋ˜ΈzwϊtM΅½MΔ>rΤΫά½α@OΦΰΗPΣΫ`σlq'Σ€ώΩΖΧs:j\E3 Kd_OΩrΪ“‡Αδξϊ6=QΑΣ'‘υΖ>Ιρ&Β,Ϙ8(rˆ •fσ~μΏ<‹οώg|YMΐZ` ”L#\hΎv”‹ραW£sžΰ;έ€TK?τΰwJ θq•[WhˆœX,ΞφϋΡAԏ]˜ΎιΞΓ'q±M™(]Ρu€ύVFΧZHH#8ΉϋΙ4rΥ¦ΐ“=Ιƍ“Χ¬ΛΆ•wrv/ιKJ +EορbžšŸο—^9‡G·3UΜεWnψΜzϋȜΦkFpνm +»Rt +p뎚&ΊΕξλF+(ίΩ6ίx ͺ7Ĝ8q• MΏ,±‘sΊeaBμμ‡εˆFRΨεΩ»ΰΚ:’ ―HΜ]¬YΌ―Μ)6“^κ 'P•όΉ˜Rν +m]b—ξχ‹Ÿj•‹ŒUœΙO³Ρzu1‚&RΒ‚w1Ή.u;γt{τ’Qœ#‹z(Ρ’η€aΊ9hΩΜhύž_Υ +G‘#Ίr’ήξb¨aT\! ’ω+{ω윾­"Q»yϋ2kΑ…$‘•dό„a\3LVΨ%Υ&] /KM=kvR€΄BœsΏA:q@YrρΔφ>όYΐ|:Ν4CqƒjΗΙXη=Μυ-Ž;ξGVŠ χξ ˆN¬G …«W%whg"Η籆#ΒάΛντ. !Cw'Φ£i"ς7»…‘:„ν°βCθ΅²;±f8‡²‰λ…IεQήw=»BiΫηωΦ]*{UΒJcήτhtοΪ~‘S\|κ—ΰNσ™³yN1§PDΈ€`™ΠgςsΑUνϋ°tzpn/sΌΡžfP/gPΈ šΒq°’ύΧ„Ίώ w]^‡=΅%gΞ€eΣƌ<θγώΦB;ώHαΓ]5RΝJ OΓG"ν‰ϊX΄1TOέ›(ž(1rε|γ-H³R!Ή +εω}œ)φ% ΅ο!ζ&Β·X‹3x +‘ ‘κ–PHqo%sμŒ;·*^½F9!g;y•°ΫSς©όΈT=έΚ_•E9_»6”01Έ§cξυ½υJ-…,R ίΎvlμ{f°GΒ›xPΕ ΅Μ%$-HήJ1gW6†S«οΩ>kκνa΄άΥ;ρΕώΉΨT·?ΤP'ΨKoν°5—]؁ΐ;ŒgTωΛ€VΏhζ­£³₯Ψ0|ςΏ0@@wτJρTAR°ynH‡f›x 8€ΝzΩ΅gPδΞ΄΄VνΣάwR:|Υξt~θ{³-3$]κ|«o™φzΔε瑃™υ‹ΐ@zT΅1Ή—΅0Ο½6VL1€βλΫjΈβ- φσz*ͺθ΄G0³HτZΎςΪ{κ\όΨ+iAD£όΒg˜Α#…ξ%nψeMΉcξλ2QWv-ψŽε9mtP\?gΡ$_ϊΐΞGΌx‘ +[_9˜ΛUΎ«bΏ±+βD!Υ1<›Λΐa“ό ΕΩ:-9ΌαΏv‹|ψƒ +Υ!…ίΊw@Ι3OΠνF€ΧωPKΆ3 5!›y[$₯’A2ιΖ/œέ%κuΖ"Λͺ|ŠχαΊ―Σ‚g ͺ…\•α‘wΌσm†e2τ'K‚γA"{-4ΪquSΣy’8Υ!cΆͺC#:4Η·Ε*z§εψ=TΨ ΜuεnLΉΒ²ΈΈ n'b ήAφ΄•βθ/aύHΘ“>)κŠΤ;Cδ³@.δ­ ˜'"Γ>]$RM0]‘Zw»π>+ΑQJυnΘ†©ύ„Ίx]ˆŽΏνΊOXiμν퍖²JΜΐ)ιι7πυꦙ{Ξ΅΅hk½όz“o8ШHF]ν{9žκΫ0Ω!ίΕόv4ι—χ,QWσ˜"iqTŽ5γ?ydLχͺ3 –°Hn|–Π`†›Ψ‡‡Υρ·δexξ?‡υΊΛb†μ߁μΟpτ…πI…EΑ¦»ςEΤίψWΰΣcπsψά5?†|kE]ˆŒ—?£Z―χ1β–"„]Η F)ŸΘ§ΌIžJŒΈΛώΞ\L€ΰ―Sβ½€ρ κΎ„T.δždMdτvv—c—Νΐ’΄ˆyWα C¨§8EέSeU>e μB±D™Ύ¦,VΪϊz‚τUJΐzzm£cΕCPWϊIίΓίΣν&ύ;Ξ/N9ΊΘ#z₯΅ζυ”·ξgά °Φ¨L›X$s'\˜›>9$5ϊ•Α… ·Ό-PA,JTV#—±u†H 0άΊ+Ώ2Μ}PM.Dδ±ξQ―wsχ.ΞŸΑξ V’₯vvg9Œ­|HγΚkΌΠ .-4ga‰ς)^15ž˜ A³YjΖ°³Ό[Ό}$₯/ώ]^%·ΖT0Bϋz³βΪ'τ~='΅ίR|lυ ΪAΤ]W½Δ½a(E¬‰ύ2ΩμWωεL$Β₯MφMη₯O―΄}Λ΅Šυ½Χͺqε½Ώbύtnh7J)ΕΚ¬‘x Ϊ³ζΖΧΐO—dF’ύ¬SŒ!H°ήβ|Ώ5ZAsb‡B‚l j―©Ή§ƒ©N©pX’^3»–ΡΈŽFϊ|£9]ˆVςϊ·|(‹ώx:›΅‹˜CΓθSZήAkjήKCDΰΛ}š1βW›‚AθFδ“X9Α³ΞΑΥ‰ύΠEΨ΄Tκά&ηY»<Θxs‘Ά +”"…IάΪ+δ& σή@›NςΝ’*?¬HvΛ] £Mˆ€Έ"˜|l‘}.ŸK|P›0°ΧM-τ){›‰*²˜‹0·£|ΏC² ’ίΩf{U9Κ:ηY—Ή©˜±σΘ@„T*!ϋίχΑ9ρŸ$Ω‹­EνI/ή<•tyνΫM]jKδΏ΅θ‘ Pe[„_H;8q£ˆx~7ηYƒ†WSαλ- ύ Iεœπb·/Ώ•@%Œ9idž‰Θχœ³.ωλΙc’}ΪcΑΌ’’α«ω…,Ι’†]`kΙeχ62L²π υmˆŸOΣκήi4h¨}ρΔ‰CJιΐe4ΧβuΊg½yN]°ˆφ±Μ˜>Νποͺ#[l€Ϋο"#’}½ šπΝr―ΐI²ΑVνqξ4=r,Vhά Ι­υp}ΤaG »Ν'3Ζ§³…¬€©_h7½ι/hOκV ›μ‹κz\z†n_ήAhΝοΩNπpΛ₯2gοKπV—žΏ$Σb,λEš"I3 σ8ψ±΄@κ/H4Ιj"³s. +—™ΟTό¬1"”QV#š:O(12½«‡±/Hu€dbΐ4˜Gj`ς­ ˜λ;ΖF*S- oδΏ„²xΩ‡ξR]Έ‚…σ1ΰρqοͺ₯θxPD™n6qŒΜ,e\φψN\δD ߎ`dΐo°xε«‹-ŸθV98zYSdO‘ΊΥΖP‡_”iτλjΛΏ™·©’Ž τΤ§½{Z*^βe³Isno΅Τ°€pό$°'ΡBSάAΡ2šσΔϊ‘•ghWWƒνœ4zΎΔ²’χΘͺΪζ₯f ΓΛΜ“T–›]HΑZ+yΕ+bVSLφ'Ι9©Ρ¬Αs ΅ΛηƒCΡΉe—qι1}αΤω}œl*Oπ©VΓ ΔΐΦΞ’†η+ ‘hιΓμΝ>yŠ»5C>ΩΟΛvML)D†δ™hζ1Ρύω o«ΖΤ`VpŒο^4¦₯[ΫΫ€vwS䝏44Ϊͺή¦WujX’υWΑ&(N|θ΄\E΄ς—€Σ4TX-\ͺΧ9«8ˆ­RΠ€]4κP•?¬ΰvy¨V“ο&eΔδ˜ςΆyuƒ,θΉ"(ͺr‰R™΅έλrΒΫ—‚ΘEˆΰΰ₯Λ{Ω½±η‹Ζz}ΛΒ¦DAˆ!μςϋΔπv >~„όΏΛ‘NδΏςžΑΕs%bΕς4TD§#ήηί\’–-jΨΈ0+ξsoψRŽdkLΟh£aξK°Ώ§L<6?ν“7›%Ω£τR―PήλZCΔη\Φ€[i¦ΚΨ&,_ώ+|•μiΚqp""' ρΛίψΉ·oπ§; !μ -¬ŽEI8Jž[ΎA45Dβ­‰ΚΥ­I—Κ`ΐ«ˆΩ±ΑΥ&ή•|Βl€·K₯"»©·`„a˜ΙWN뇇ί]υΠnwWNΪxL©.ˆrx\lΐρEΪί‰΅°Œ²ΌΣš>ιΘ51GAl'»‘&΅δΆ”A9­Ϊ„pαEZξήΌ―ϋŸzϊΘFτ€½vθpxχŒ¬ZΞχNzvm]ο3‘b‘I/ύ\&ߍ.€zΜΈμ‚Lš™=L2λ£'Nα$³°ΎPΏœΔ.σήϋ­u7(!(ςΓRΞ¦”§˜6½κμΔ”!ο νρ&ψ4 "Onː"BΚršΨ•ύΒrŽ,ΰάq~Ώ,ιPUjκ‘}νlΟΠ¦9‚I4†Rχ~ω³XΩL; εš΄Χ ΅χΈQ@PzΧΘ›+)F™δMŠΩhͺΪ°7²ƒΡ…JΞξ›žYγw“P\ϋ¦ώ>ΖΛγ•C©X6 KAxB ZεΡ_I@bƒγˆmμc¬η^ϊ₯οY+ςŽ΅ΏL?₯cϋƒδ°τε˜=Ak¬ρ³œ5"  ‚Ύ4hίc΅ 56KP6 œ‘œΧS(ΎWαRέAw–‘\­@y ™„ΐ+wЇέM^°μœ8L€ΝN–o:“₯­‚΅*;#/Γΰ ³γ ?yΕΥ-!δμΐθ w_βWzpc%z‘h5žΤA0y˜oμ41±4νKίO“Kƒ{‡'ŒΛkzU₯_Ψr·ƒR©‚{IτTθό>W`JtΆlσ’Žv\™‚κΎNʓ着π'΅ΥYaΜΚ€ ώΙΠ3Ζ}+(/7εV1ΚUθ=ΥΛό{iηκΫnΨ5˜έ¬§ˆάn·ζΫ³ t™[Ά+|Hυά;*σξk]­­ŽA… ›GΓͺгΜΦΊΆNο$ίΌΨκAUnχ‘QCΗ+μθlΜ\ΡΔ 0έ¨—†Uς=&τ{+΄,ΩgDΨ£YYVΓςσJ #Ό’„o2z©οAd!/Y–v𹯊I>8{ψlΥ>Ο{1ώL™· 'w5fτβΰ&’SR^βB`¬ΣΏkP΄ζv½P#`ΓΐΒͺW!MI¬tB¨’₯­…†3” <δ‹AζΕ#˜Ζ€@Φs_d―ΓΔΞS–ΐ±ΤAρY2«WRΒ¨”}²½xœ @rί63Οδ‘ς:@FΗΖΡΚyd…±ή` LƈI°%ΪΞ 6Xφε;`PTE;ͺτŒƒLO§wυρ¦`Τ²ά4O°Qλε%τ9Š΅Ά~™Έ½1Q ϋΧW7ά’˜G½ -NΉOю3“YGŸzΓ‹A©Ω₯^“€>Ιι|κ3GΗ³ΞΑBΎ'ij"ΚϋXOΕ χ€ͺdϊ£tιΔ«’”Έaω>sΣαюh*KQu™>—n’M ύ1ΣΘΪ’±έ±ΆΙuͺV=τ »sσ +HY`ΣENΈΩΎŒtΤ聳}¨γ4δP°£gΖ;φμΖn΄€±yώ.­ρ›΄Rι9U%§y:<ΰΎηI!^κ§ΥοΈU„‰©©ŸoiN9H§]4GgΦ,˜ωGaΩ]–^³Xή#-@ν‹;ρ/•z±ΩŠ=•ΗΈώδζν°•ήΗ…€Ψ(p“ΦξaΛω6ε_Ažε}l­ΚρΜίπ‰οω€ΐ7i‰Ό΅ο±ouy·šdo€4―»+,%‹¬Ϋ2ΖTBώΚΆKω#nƒxhϋΰ-˜ρ}N“ΪΔ&ž' +ΦaXέί»Έ/Μ™ζ!†›_΅}ΖKIFυωΝkτX{\€`ΰš}ט‹g>(¦«χ“½ΧΎͺ'σω(|œjΎi%ρ”™]aό5±r*•ϊΝz!ώ's©Blκί8τΨ”QύΙ}Τάm-JD|ήyGΙΨΖAU%\'Τά&Žω†’oΛί"±\=ύnx?ΪKQ¨΄ΟwΩN; :βιH9ΊΫ ΰq<₯X_£υ βδa +Υ–¦QlK}γαβ¬}}?p Ϋ¬π +cQ@4ώ1cg…xWΰ&Α"±Λ€qœ,ΌPΘ₯ϊα‚έ―ΏV’μxΧABΕΜ=¨y€μςN‚!0Β?£Οϋš¨mϊΏžη5κγ/€Ωϋ]ΒΧ7XXΝFŠ…~WTut?€|>»”G‡XsŌAό5LρYι –Ω]+3’(Z”€-ήάZX˜Q’"ϊ³RΠ7„y›Ψ$p¨Ά“Νή=»Ξ ωόιeωhe'Σ +Tξ¨Υϋ“Ο«?ԁ9ϊ&^Ι=‚°Χ'7QvΦβν%±Υϋι4ήX%Φ \oœ¬V=Π6ψ7W­€ΰh C2zΰ·¬‰»Ίμo9ŽQgΆU·dΩξΒ1Wo)ψ’Η¨†π< Ο)Dc/Q Φm&Ή"pέΌΆφdδ-±Π,6Θco,rp§…1gΞΡy τ{¨»›ν’1 ^(άpdίγΎ~iΦ…Œ‡τ–<“UV0xύ7Υ+—½8"ϊ9A-ΆΫΩΛ†7F–69Yβ~d»e8ˆ8u»ω―YŠ…ˆfBjЎ1 +ώ 2ηiIΫΞ™z-4rp*u–tmΟ+Gš<ΟeXέ1]‡=rΪ@η‡Ά΄[Ίqλ±RΓBŽΤδdaemGvs#ωŒM3sϊp!Ɏ šρ―gmrB‘Ѝ>‘vdoριό³ΌΗΏ»")’ Ht8Ά³»S±`0αGνqίŽΐ‚`ΏiM•z―ΉTξΰΚFD[’ΚGc)„Xά-υoάξΟυ¬v%οjcJŒ]Υ.”F»#—h§~‡ΰχσzΥڜqΩΒ«^޽c'Ρ΄KζŠ‘ΆόΗα@ωe*‚_dœ‘΄nλNΠ£ψ7㖍Ί™k`h—F±„+<0Λ›·.’1 ؞±ζGτΝηΗΊRΊs˜–Η]nΒάGJ·sYΥ¦U ε $Ϊ*ΐ4VΑλ"——Υόί]"Wˆy@γ-ΐaJIΎ,i?IBω&€]ŸρLjΞ+ϋκ9§]Ξγλq.νœμΟΝA*msePžU|ταώΪϋδœω||ƒHύ4σ2ΥΧD\}ˆƒ½·ΐŒ1‰ιόW₯ΖυΛAθ“”FXxin—ˆ,‚Πδ˜<"Œt° κ˜.Ϊλ:‘Jn:p‹p_Ϊ]τ‘ρ0άs=ρ ƒLσ6aiψo”ε —™°¨§6άJ+”φωΟ]?D‘‚΁‘:{(ΩμKͺ‰ #Ζ*Ή«{Έθΰ«?ρ±Z₯Ύpσ9ϋ·λΝdŽWj„ο1 Gjqn¬©z +h|€Ξ*ΣfO<κc#ίΛJHΈύ»+MY΅ΐ:ξΞZΑ“qAžη•Γj•΄E§ΤAl‘ϊn…„¨΄jυŽBκa€Ύzs œ;ojΈDaωlφx1ΞοQΘ=γ—™^“ΫšΌ™εύRΊ5 TλυΒ-8θΌ ΝiυΙ6 U„μΤ⾍ž?Ή[ιΨΫ/”χQ©τξΫtGXgF­†ύV ΐ}˜_<§]ρ€ngΕuρLOθžήi:›χYύη9sό¨QXωΞ―έzpηίσφRφάe%Ίϋ_ -α’ysΣΒ­EΣMς‡ΊΔ#fF¨Ξ=%M2’Θζ@˜E ‰―]ΞFΤId€eΈ@“Ωxϋ}ށξZ Α#K{OEιišγ½lϞMφŽ0$“―ΑoΩϋM-#Χϋ'*” kςyyγr +H„Υσ"ΎJ·Gυ–%U”}a½… ΈΑ챴I’%‡ƒ/ΰ•¬Β»σΆBΆύ]°ΪeMΏπ†>XσŒ›έKŠμ•Fj¦ml,+xo6aω4f9Θ˜ͺ„ι }5(a GΈκށΜ_/­`Κιq²1 Α©ΏξnσwLa~:†>—ε_ +CŠg8™ΊjΤΘι&]γ3αb Π“₯ΜΈ·υ"D02jJ7[ΟA–K†/H]ψ~°½ψκp*@J&?²σ˜5T©ζ½Β³‹DbFž88RwrΘΊCœ?bΧ„uͺ΄.ώٌHΣ('Iηq’F«Ϋa@Βα, +žϊ·o㑏”j+©M›ώ@K”ΕΖX„<ΛμΤΆmOά³Š {wa–³αΛρ °Ώ?Έ U-ΔLΜͺδ/eΒ}‰™?έ{Ε–ΔΛ8θc·A’Se¦h{Ί°Fͺs]Μ7―CΨ’JήΉΐe‰Hp”ž=tb€Vχ{]§8lΛCΑΣfdρΘdγAn§ΰ"ŠΛ_’ƒƒLiκσmρϋi¨rέFΙ7mVώΥ‘\¨N4*°˜ΛΈ•}|©Xθττυ6„ qΚ8ΖΗ€.I”‘[0Uψ½gήNΑvΰ1Ζ'χσΉIΙ׏ΕμΫ"˜η΅Ζ†ΑΩ/ΫυQ %»#ε@~·ƒNό―‡Φ2 yΜ?^·Ω£hO=€1ιx|\5―yβYRΕΙC±N6 ΪDeqύZ’vMT‰Kpς5λοπ—2^ΖΦ“β.;’^RϋΙTα ·n€l:C 2\‡xr½ΈΛά:•μGςͺ(π3±’ν +f UD…‚3½Kge―θ«Ωe#°e ΎμXŒιέΡ§€.󴭐2Ηͺ—R +Ν„2¦”Tώ^ψ˜αš›Φ¬ δYΠΒφΘ8ŸtEψΨσζ‹-=&^”wυGlΑ0ρk·ΞTL.•IAAbAέ ι§;Ψν‘κΨZΧ~*MBC±>\§žοŸwXžˆΦ|§zΨeh•Hϊ»›ΔΥ}°».Ε3„˜…ήΫτ,Β˜Ϊ !1Γ―4τξp €!p›0Yχ;I°pθ΄΄Ποe51 λΗjώ‚ Ϊ_pΐ ;z ž¬¨§ŽOΥQ—r·ΥHOINJrKΘ„]Δ‘vx†“”ι N©θ“F+cΛϋεI`Pz–M«…,»–V†:Μ  Oγɜ ‚}eΠΪ8Ε_gΣΨΖ¦MmRτύΨΉ•kμ4oe0ӏαT,‡;R™ι ι€}θό”q)˜3₯―K½οʈ>s€&Ϊ± Δ!GΞ–ΜrΚκC»ΖBegbιuΖζ"Π€kŒ/2iX…n&Ε•¬H²‚oNΞ΅ΕεuH6Ýb Ηh#›’†ΔVΕΆ`YΤzΎϋΰqΜkWΚ°›ύ–ΰφ /Ή«άX’Σ|’ΦφFχ;υoh:xξfσ0‚Þ₯φ2hBŸi΄_65gƒ°Ξ,₯Š–}β «TKΌ …»™Vξ8x’ΩF@Z.»cjίΗ%ςΙν€έ,vJ+Ϊ³š˜₯ Η71" Ι ’!hτΗΔ.›ˆ NSz~xΨίD θιΕfΚi €α¨‹;Φm;ΐΈ°1¨σΦO.4яqΥΡe±ιΞΫ»B3†?ŒgΥ§κΗύ©»…Ί£Ϊμ‘€’%Θ΅ό•sΑ6,&H§‰ΆE‚Όίt#%3Y KtδύŒP\·[ μ™Ίή ά0“”΄Y2kΉΖ’Αύ”Ζέ)@Πd ‹ςύTUΚ ”bΌα”P6XΒΪΣ²»υŽ΄fšΰCY³%ύŽθccηEϊΩR¬—›‡Ύ=‹μ©±›ϊqΐXΗΞβΥ0ΌώτŠο%n΄ι(³ςDπΛΥ£χzυYdœ‘ό­GΆ˜] ™ΰٚΦΖ“‹–ΔάζŠ9zœI HνG5˜0²5EŒ^ƒŸΓe-ϊ°Ε"xŸ’ηWΚ–:Žš%Ή¨b-!‹΅ρpΖ~umΝU£2’ŒŒ2VHv{\pΖLβ)Bš’Uό%RϋD7£ήΪ§ŽξQΏΓj²# ¦Ρ8σξ,}x*οβόΕ€ΈΏtn–Oι؁6ιnάΟd]ψΑΗ9ν|€ΜjŽ›ΐ T‘¨\ίhΰΦ*Δέ¦ +až±* ;›p£>k}[+†Πš΄™Δm΅‘9ΖΌΟ06ba¬ΥLV0‡xΕOϊ Fέ)ύΉu›x^’Ϊ¬¬gίβ…eΪΓ Ζu ŠΫ6¨ΠK2&]‰ αΑ9σ₯θ1Š}ύΫ†gΜμλRξ›5eΧ%#&#'}J£ίvU§Q‘°φπΘγ0±4™œo-(”!.υpmWΚzj±tτ•+WΏœΫια^f΅n›™·›fωzΒaj>Η‡(L.ΠΩ.(Bδτ•τ;‘Q€ιΟ %Μ©@EŸŽ””γμΓι{R2¦l6ξAfΩ—RΊ[/t‚—­25@ΜS*p³Ψp >Λͺ¦΅₯paλ†έ΅IσΓFg`‰ςŸšχωx“»ͺΛΖ—u°0r =Kž `u@ EPoΦχ‰ ™fNwy^hχ’‰,o-NOg+RCδ}OΏ‚ΦWθA«8Ўpΐ₯6ιέ@$Έ?„w"Ϋ‘‘φcJ―ι‘ϋN`‡NΘιΜΡ»2axYμςσ*xΧίƒ½α#)4§+N•(Ι’…Ή±K₯υ¨ ;ςƒΖ‰ύe‡–ƒΰνdν(ώπbuΦΥ‰€:Ο|s”ΧfΜSάTΜEG{Έ#Ίc]7A‹χDΩγ,²6άΒΘχ…|MΠΧ*—˜ο‚]l‰KOv”‡x΄ΰ~²»π +."†kΨl‹”[\-ύ£ΰdΥ|(δm; +Eemmpδ0ΠψLα T=lΎuν+αΡ#ˆ€ΉΥ}θuΔy—λmρݏΈM­Ίύ’β?ν’ ε5ΧD·ί‹!¦μ$K{ΔB'5©I†Ϊe§ΕΆό?«ΞWΩόπhαo…_=π½x~SQ΄κj%2 5±³ΚΠ_tώ,Χp›–ˆ +mε01Ϋtt 8‘ƒQlV€°SΑ©Ϊο²\‡ϊιώS1– Μφ1 =hΕ”Qs9ο'KΘΟe―‡Š(ήΊ>cΖ9?£„ήψ5D‡‘ΒŸϋj\α-’,έMώ2†ΓȐς`Vmφ†Βώg2(0Θ‚ΌΆBλmpΦK“Xf”tπ,cxΦΈfˆ\z_@.Β&—X:CtgΊ&θθωTιΈψI₯Bοqϐ)suτ„εΟQΣω]d\PόDGψ_ˆ†$ΰ7tΠ€,'0–ΗrΓZWRιd§M*΄£Ατό²jΚΣi%ψΈ(I€ΪE±aO2­ωα#r9‹΄Πœxp~OYβ,β³ϊώS–Ώ{§ŸΙ{†l\š’ΨCs αkmŠϊDΌQϊΎEξ#©\γπ NMΛ5ΉšΈ₯Q7•k,X°^±Š2w³£Φϋuή@ΟR5ώ―ώ#βπΡhnέyΉΜŒΛςJiz…}Α8—VŠΏb)'ῚΎά9”―ΜPώ±fΌό<›ρΓ7τχί„Mεx!Ρj6?NΕφY?g£Ύž+ϋX,,l$›t/d՚{V4³ΐΔ#ίn²ΣΜaρεXΜλE/ε\d-œ΅}F²€oxȍFŸι>Εα&³Ρ€§3/Œ—boΜXΓ1=ͺ‘ΙέΣQHL™ύ«ΛAυZΤ ³”B1 –σ΄λχ_K <φ`ͺNfΫό§_ξίRc +4wκρ„ΞΗΝΝΒμ‚>Γ£By‹ϋΏl4^δgš”$Υ³c+*π{^fΊζ +F ”νύ'…‡šσ‡ΑΛΉ’ΑΨ£Ξxƒ9₯·Φ±˜—gK°*ΎώΣϐpΩΚn0oΞΙ㐽`cΕA„ΜΪσΑ @‘>ΚΕ>JvΘς ξM;βRψK"`Œά’g[DŒη ™{κΣ„3ηiD8 ε€…¨ηh&IMŸσβ}Γγ$yύ„­t–ΰˆΆΝzΌ œ`2hqI{RŒΉβύΜhΔ• °ΠΚλ ™~V]δ\AΉΨκu,»μ[΄SύχpTD/κl~u% s%—q[Λ³. Α"ΚbDm<;²D¬μ`ϊͺ!Π;!š•Ζ#Œϊ:ΉέδXN;ƒ•“Znφ›^΄Ra`g]PγRΞΤπk°Τœ…=δ}υˆͺνQ[ϊ’oν`I2δλMPipδδžSΟζiμ©ΤΏ¦Ό₯«»Β†CsτΔίΟΖ‚]H 0+{KΌS—A »Ϋ”ƒώ€€·8_mυΆ₯I GoO¬ιi“̝~¬=šΕoΈ•κ ΔΒ¨ν}fΤ’H‘ςs .I ψ.Άλ«A+Υ C§ ΏškΪέ{ͺ)Ρ1΅Q7­@β1½\ˆfη]X»ΪIΕu‘n‹€Ω βž.άsΧ5GOcΪJ˜ +Š—ώ)ωνd=ΛH.c+T('5?—«cIi7TΥG£OXΆ₯Ά—,†φ9l˜θ³Žέšϊb7«ΖIGΤσΜƒOΜΫΗcΊσζ6‘΄―iΨ" +DXν[VDΓ―ΰο!BΘζ;₯Ό~ζΣ HKβθcδ ΒΊ±δJGΡ€Τ!Φ7·u†¦iŒˆ`Ν*­>³7α―Χi<ι*υgoό†X†οK9©'Bl°ξ ηš‹<Όj―βi BZ5Ч0’q'›bό.†2£΄ ―ΰ0†K$JήzΦ‡ ΪCΐZ5RςΘέ7e'φʍžCrYΏi0MׁkΎ. Έ^°ΩZE²8V΄ƒΝV†εήfΝν rΟίR‘yΚ’5γwƒ·4Ÿe2YΆœZ°’BΑϊ*Δ„wόt—ΦΈŽ +?Κ±―Α‡@d‚θNΑ1ŠŠͺTÞ<λVΕ±&}’ξt8―χοfΡχ\, υ.Τ²ΏΞΊΜfœϊς»ΣΌrΒ'(Iνfν­½I2='X–Œ…rκ5β6V/%’*U«υϋΰσ&+g5€f—~ΈΡ/7ψΒ•klE“t€Kπ3$kΧ§ύUΒώd\κӟňǿ~ι[Ϊ,‹.UΚ|y£ΎΑ’‘]A7|κKύg™.eΎ6ψ#Ρw^]=΄8/ΖΎΘθ †kS^Š™h σe7•ϋΖπ+ΗνfΰPŠΕ―ΧkASЧBaŸ~ΠΙ“!μΎΰΖ4εΘv£#¨wΉo§ήτοcΆpI«~‘0πNϊυΥ‘^H»xYΆxlξ:, λΘπ+Η’1ύˆ#6΄rϊxdžμΪτc ώ‹ωr.~δΏl²ΑΘΥ²–M―œύ81¨*ΊωRuΨΉzQL„χώ ΦN55²jβ& *}X)Κμ,²iκNΪqU˜}>˜<Ά€’–΅€ζ Θe€mDφXž:ΆGh]%;UώyfnCb«=ν₯„Y–—ΓΫ]†ψώϊdΦ%Uu Ω—ώΆm:kφεW#?J–―˜xφΚ Ž‘έΜ€)u^ΞΘ₯}ΕF`uSQX’¦ΦQ δ―€ΟF}†d»β7°©«ε!ōT&ΎZ ž»ϋ‚O†ΰ¨BzT€¬Α=θχHιΫ0AΏ˜葊’ύκμήUΜk›Ρ{ΞΜ—-ΒD έƒρa~c"OςΝЌZ&Ήσ‚ήͺUUΨg‰cNΠ$‚•ηΰ„G.υ^D\’YžΛjΛRΥt3ηE‚˜“£§ΜΝ3ΫΓby:CzΣαBΙE,‡=t }hxΈΓ >„4Τ)Jb*Φpi‰HͺKd̝چ—‰2˜ΠΛW-&Ρ.x]=/< δqފUJρ¨“…IŽkbηΐ‰ΛˆΏ‰Οp{ŒΑ³|τ aPεχα‹CΗΚ¦w‘σWέβθ“ώƒF^4+K©¬ΈΨd€}΄Hέ5G‘•§»Υξο/1D²ΫΞ…wχ¦e`]34QPε/π6#ΩR€‘³_'δ0#6w" b’~8ϊ†ΈνγVα9Ξ1Ζ£ΔFτΩm]gΉ멜)g ‚)+|GPν@e<ΛζχuΜ­γ^<°^’‰φ aŽ!ˆΟ¨Ϊθ>ΣޞΘS_π΄VžcQή +Q^T5SRW‡Q`πΧǍqxN²0.9eψJbβΠν&Ό±Œ@'‚ TΤV1Σ-;>vŸxz_ίm!MΠ”Ο0†6μ:"­YΓ]%ο³@%³άRΚg·Œ_"U‡ $Γ{Ή,,Βν<ž&Ί"\>©€ψ” z΄Ο?W?αLρ:RΆ+‹Oό΄Ž’άΜΒρa΄°ΑΗ΄>Dq‘μ…’fΉτε³CŒ|±Ώ₯Rzΰα 2F€Œ ;ATηL9Σξ"¨ΖY>ΥhΙ΄† ,;“lˆ‘@>„σ£ΊWR*υ-?ύx‚#Βk³XPυi p£Ρ”·MtRœZ)Γ₯€GtΟI²<ΜeWP ηhΚ /=ΜΪGlγ―ξNϊ{Υwϊg“3§š"XΦ\πΐʞ#”fσΗω2lB19Ι¬©<­¦ψ BDΉ©¦A—@ΰώ²ν€φ7(1mΠ, αηϊιj˜¦ν΅τIΥX‘*W€ξ°ΫϋβΞ!R΅³ΊέΐνގΪ%Σqi–>>lζ†ˆφω‚t“iό6 +Ώ/BΡ‘¦“―Γjuy1ΖMH­=θ8΄άν²‘t¨֏²:‘.π‚©­q+ Έžpwςeͺ›t‰ήF° σ3ώuπ…mΆ$ξΑao88@ϋνŽβ‘›œ:GЍ{­Όiϋ©AϞqC7!"Ψί_kς©6ŽυEha¦ΫxρΘΥu‹νƒŸ»γ5sΗpΒϋrζ‰Η Ά] γqpΊ„ΫΠEκζδ'P9΄enυ·Ά #ά€l&<βf”·Œ'η΅Σ ΙΆvj#>cε¦ +ΙAQuΫ•Έξ ς(υ\/Ϊg’ϋg>Κb‘I‘jΜt‘—Co>έΎχφ˜4]fή„ΟβdrM£-«O]δL>ΏόχZ‘'6Θ(PηIucΘ ύΙhf:œΓTu`iֻ܍λn:ŽΞ σ9’\‘8/Kžd–\:–XζwhN )’h,ͺzh 9 Jά{i•ΨηΒΙ‘˜,{bΡ΅”—KάkΈΌxF/»χYUƒQ5QΑFxWkab”N΅rΘvwΎ‘φί…qN»“Λ²Gžψ=΅· +Οa±Λ¬₯»βμΆ(@Ϊ aέ(…Γ‹ώs@ ΝD¬˜ΥαXΑΦZZπ;=\ƒtr/Ό`Ά† ‹„γWpΨοW¦η>;ΗΨ£•ΣζDμΡ]n½βcΔι~<λ^0–γύό}† τΫO,?‰Rο= IcΙν†ˆMŽ7»Ψ‡–X*’{GξIθ!Q4v ψt½‹Άμι2€«§Ώl +€ ΦlΫΎ8β1j:'Ξψ{άVΩ}]4Mύ²•†§9­;΄5ž•sx Pž.»RμlαEžZX–%?νŠvv~Ή†LΡΪOFΆ½ΨZlκ„R₯-=¨©WθeδŒΫb nul€β-.½’cŽ’=ώPβωφ°­αοFpq3σ[[TZΰˆ* ωΝCΔΚ€Χi²WU,ZΥ­ϋ،Σ㚺ƒ§[ΪγΔf*Ξ'>Šn0«…Ήτ *κZτŒ– +’Iπ°zy΅•WΨφ#8S€φΞ^Μ}<Οpͺ5τjrυxώΘς‰'L8Κͺτϊ+φξ»nCΝώQΗe"όλΨz¬§€x˜ ΖΌ]XΨzΡAΎΞΝs0mq“Μ_mUωQ\ €E7μυE?RέΐΖ₯˜££/*”ΩδC„θπzν†Ψ} WkžΔ)7PQΘΟ>₯mMlϋpΩ8Ϊ4ΔΈ³†pήύ^ψžfJG¨ΈίρΚτφ»‘―’NόΣι\€ ΑρCΣΫ‰eF(μ†šθE=ζ瑇ΗΤ<—o~'ϋ_γ -r8hn$Ω±””“ΫΌš–RΠ½ΪF#ΘΈ,y›[ϊ-O-σvN¬θ rΊΎΫ"5σ^Ÿ¨›"„Χω^‹όoμ―–EΟ G•ς'V πΦ’”iΓDηeπ?ά)ε'+ˆΌήνoΦMh'Τw₯ωš†ΉΗf»hύ‹'‘YύI€6³£Μ9E +u»Αο€Ι&Ս΅e5Hόκy?/σBπbL°2h±W‘;3τΣv΅άJϋ » 4‰£– +4ρ+Ξ-CκvS1nŠ΅ζVτŸ²ξ―ŠiΚΔCΙ΅ρŠˆ­Χ Τ<ΆΡ²¬g§ΣΒ,θ-–Έ*°ΘΊ»³†ˆ/xP˜κŒύ,1λC`ΓςΠ,LlQ 1{Ά[p\~w2dPLOΨB*κ—ΡWό7ΞKπ_ZN¬Ξv‚•hινˆutκr†ΕIBœ0QίMΖγ_κι‰ΝΘυΏLΉL=hQ«uόμύšEq«xT°vJˆP½Ρџύ†ΙA/Β.»κΟΣUΛͺΖ6ΪΨqΡYJΫ§KJͺNEΌλo)Ά%΅£‡NώVι‹ιώξ_ι$Χ~£Xπ ΏΓ}s6Φ5ϊ=Λalošχτ<κ°υ<@7‹DήEΪ›R ΊNπS1Έ,ί8›«[xΚ–dz©Πώ‹vς™fъθplΝkz²6@Ί°»K—·ό† Ίzά%χx7½1!Hύ'ƒΛ%ŘΟ@5%CϞϋmΛIݎύάU{,OlE +gώy¬dE-n—JΤO»S=~Yj‰ο›\zυ΅φπ'Ph‰ τZκ7B’Omί–\/8.ςJA\N.ΉΛ§[DΓS¦H©ή΅2π½nu“Α!soΧ%wΈI7½€i vξq(#¦φΑ²{ό‹y£c’ψ+8EωΦŸΒeΎ’ιδ +εˆ`k€τ€ΡU•’=‰ΖΌ― ψ υPαl¦Ψ™£Λκ ΎΥ™—@_,·ρ/¨AcŠ&ɞ–D˜rw;ρ„v˜–πΗ“ΦQhCaφrϋ5QYΏsτ?ώXηh%€Nό4‡sξ’žΪ§&h[ΊθOιDr7œ \Mš›3Ύ…ΦO+Wδ…Ÿ€ ΐ°wͺηα0{£_‡α{mV™tϊU—θ΄ΊD‰eω:œŽ7™+1Χ"Χ‘™ΨΓw‡PΚ―9ϋ<"§+!…ύ•C 3ΚΓκrΓ“7{¨=λw‰Rΰ έ7’2"?N2όό*ΔΓt ‰oγσΊϊ‰ps¬¬‘ρξ³]³ Γ><Θ$Ο¬·Τ½zY§ΘωL['ΉB2!ŒŒ΅7YšΥ»•€jŠκΣœβ’™;HN’*]ώwΗL5μ&Πn9cZΥ;§Ό²Sϋx°)ά ŠΞΔ₯ Uν%EmΖ.ΣuQρOrRCΏf‘,σQΪΣ`Aˆsπk₯:ŽGκΦΩ—ι¬ΥNN&₯™ ό‹Ρpρη}ζΆώ‰ ‹eΚNIH7SΆτ2rΆ@Ύ»ΪΥMίΚΪ[0㜹¬žpsαΈάρ*ύ‚΅Φ‘Λ:―uT—–Xɍ†2ˆ5ςNγ‡š*ˆ©OCx{φ»&Ν™)§'ΪϊΦO<šDΉ70¬…^Τ†“T κ…ώY•EOοˆ//g‹Žν ΌurŠΪύΈX‘#:!η…8;Β„–y3*―•<ϊβ‹.ͺ4-ŌλyΖO’ίKψdœι‰½],ϋ(½ΌOώΰΏK€TΑ<$oϋ0xƒ­HΰG°…Φ0ς‘ͺδ$^ƒri–΅…ΡΦΏΘPΟΦΝζμή/œ„ξM6ς{4χq­oˆϋ‰a­–9ΝΩSЁΝμ%υMΚγ°Ί »3@'ϊ~²FΆΒ!Ξg JΗ­n{Ά4£Ά?C–k# Θ%΅1Ω»œΞBU+Œγͺ…Ψ―σ§ŠKpn­ω΄₯ξ-ٞ.ΉiΓΡ?νξCEb»‹ΛΌ˜ΔέΗ yβΫ@gζrbν·•aŒ}[hΥ"-fT΅N‹φY‘φ`γmaχXSPLΏ g›,=i₯ΖΚ―ηπΛΫZδ«šτUB–δc?NΈΔqe!‰ΘΤ8 š^«CϋωŽ–λ«‘BR€Ν§ΒΠΡ§|ŸΥb)“ΐs^/iμ½…ν·b΄·}άΛxέ+αYΚήέΠ*Τ1/[BͺٚXκΰkeΫσ™˜TO ϋ—/υ3ΫΩ•ƒƒ‘Η7»‘Ρό(54^₯Φϊ?όžIJ ‰ΘΘZ-7΄γ6OaC-4nZ³7ΉK…ΓJϊ`W Εή Š±ψΟc}ίΊΚ’™ό@u±t“ςΥρ;€-4L£‘˜Β»GK%2πγNmT2ˆJ fRφ’ Β²ΆΟΥQΚυSζρp?wCƒ·TosZψ-£ώα]Eq‰Q΄όašΒύF‡±.|#3A:ζ―`Λ.ΣWH΍’—2‚m―-Fz “ „S{ΐ«Ν.Άγ3pφpˆ}’|4Ÿ-Ζ<ώd­QΕ‡/˜:h¨ΤίpdŒυ:uWΉhΪ—΅­3|i΅ Y‡Ϋ%RG΅ΐΧ0μœς‡X—~ΩeHxςξΝς-–*XΰU,Cl`‰£Dτ%€ΈιD ŽΦΫ=$Gβ>οy±wΨ}˜sx’ό0UB1VCCν·7W³ κ­mΰΎ»M”O„_r'Ρ&ςζ6αPs\α½χjxό5u±₯β' ΄XBa"°Y>ˌ>φ΅ΰ€«Ϊσ$_XyH/Ρ™pO•«WhWY|Ξe«³ +=Ёηz‹?e΅WU ΪΣh˜έΛΕ`Bt ©Ϋ‡εœ½Uυ 3ΐϊοͺ™:Ν3|Αη^y$εΰσGύόBάε鬀R=ψψ€ +[™^.mAdΕp½ί›–ι|Pm”l­§ΊΧ―t)β.¬σ―ιP” ΎoΗ*ͺ2{PΨU5™+ψ dχ%ΑŽ-žΙšΰVΟ|@Ast΄*AOΓ;#2©a—Šƒ«³mοΗ 9ΏQs±~ά±)»Aǘ`?܍ψβ}C<ƭםuΝύ΅#ΧF“CϊO–3Τ3ΪnΥΦ­C{€WšάC­!˜ŒΚ•ώ'8Κφαήƒ9HYΑQ\썋κ:ξR©š»:ΔVL°.w‘PΆβ•rθͺ-3X§Ί`ͺΩT A5¬U)ŎΈ·Ύ Y9ΆˆΊΨT°ƒ¨ΐ3ϊ¨žaͺ…"ξy¦ηζ°Τ­BT΅Ό‡ΐΛ1”‚  ψΙΜ&E²·wά ΜAkΥρΰXΰνt*έΖd<)<ωχύ οκΉΟω?mo +ΘρfέίF•γW^ϋΓr†fπmOaώa©wχμφ2pK:ήμέΡyPΜΫƒN¬9ΩEϋΘTcVΥ`,—_g?`…±δΪ2ν:O NOZVv„΄ύΜ»B+XQ¦«½s t³₯jЌ:^ΣXRσO·A―₯=Σγ»Ζνf+¦²n˜Χκ;₯NΓ Gaf³˜1sΓΣ©=Κ%YžΥ Θ|gόΡδΈ1žΨΫΟ=υ:ΰ™ζMj°$φόΕβδϋ$Κχ’nύαά™cb!`Ύf"Y|Υφ‘<b0wΤ`}–λώHί_7 ^ž5Υ¬ͺVIRΞ_pφV›ϊQj+;!TΧ°I–ƒuτ%Q;ϋΉ"•π?dέ; †_N4_φΚ-yχfΥιf 6χuΘ9^>ЏwΙΪ‘½RϊŸ4—mcΊMj³šΐˆηΨ€ΕσJΊ]χΐ«Ζ=΅9•νηKF…Ϋ1©κ‘Κ0ι&|\x6ƒ€gΐ&­Ϋί9‰z!¬ΗX”œͺ +ˆ+Ϋ% Τ,7B Ž¬mΞ©4 €ν–@Ÿ{ω!Ÿ‰cΥcFτ l-χ:†\πEBKΙ•mN7%Š₯Ν‡„σdΘgδ¦πΖ£SΛ‚D  + *.5/―‰ζδZ†Oξ„ά…”@΄Cg‚ΉωτxΎ]±›9Rm‘ˆΘ™­7vνC¦NK._kμVΙ0‹2ψΐύ…ΰθ±6[ž½Ν-ΣΦ„ς«ˆNΛjPωŠZ)›Ή―ΰσοYŸWΔtŸά{Xqύ@bΫ7Ή½\EΠKSΠfμmf 2βΒ΄ςϋτ9_ :‚’ΔΰV&HΒ­-_ +™¨α>©]ΐp’/ŠΦΕqΏΰΟ.ε6Υ°ΝΝG$Ι”ϊeΕ‘Zc™BθzΌσΣκΈλU·ΖΆΞ‹ŠIΛθ’†Žκ•ϋ^Ν«ρβο@Αμ/ζωΏQx0hΟ3Ζ@ϋ‘1n+8‹ O]₯"ίϋUά? ‡>ˆœ +Σ1Β_©œίSά\–=Z:SΦΑΙ λο±2yIΝ@;½)λΕn»ή1ΗN†*ρ+T]MΡϋ2 Φζ¦Όα_;ΎεC}Œαfm·l06Ξψ-ΙΒΓ–: ϋͺ,Γ˜f1•₯.Ά€π€xցŠCχ.0ηˆφΧΐπšόϊ B +Wc…v–εϊΐw”kΡZή +τΒXΨj1oρ*“Ή€pΉ Ψ<£ι`½OΟΝΦ$Ε›π@ΉzMϊγs₯€ΌΌΖl¨ΆΛXΏYQωΊ"_‘nϊut;q©5\›Ώώ3#Ώέš·/]ˆ°8LBΛ¦t–—zCΧοŽΝ­MͺΝωα|kΜ04Ό£z§‰ ΔΫχˆ9_ζ³ΎŽΆσ₯PπΟ¨Œ†gζa\aγaΔ†5ΰϊj±PΙg•FvΣ^Σvη}cΠ‘bΑem3}d懆<Ηώ)½ήfT#’a:8ϊ„`6Κ&‰€Dsθƒ1=U³¨.[=dκ©[BMui”dσy‹vB6„ό) +7oCΠ΅₯•ω³3­˜ώ˜EzŒSš +ΕNΠmkτΗ3\VqΥ™itj[ΛΛσΏ<„=Β"L/ϊνiΠ˜-A€·“„s\Γϊ‘!aΑϋ»Ρ|ΗΒάΦPΓqaοΥ8(ˆZΥ»EξPΎ +@yŽK€΄Σ΅#Ό OHT-•ΓΔ”aζπO…/©ΧPά«>¦bρWΥΑpiρψ˜%2πΰƒžΡ§αaΨ;––&8ŽK–?d ΕrΗƒMOςύ+ΐp±ΌέΆ§ΖkΑ…$‘½K¦8ΐO³ £€3Qd˜„ΠίΩ‘uΛΚ¬QO9­š+?ΔA\Δ Ψn}ˆœΐbM—{:Ύ6₯dπώΟ\š$^ίiσλΧpΣ؎^·8ͺΚΒύ3²v¨sί}Ÿ&ή* CΰŠK‚Cxύ’“ΫQζ‹ΝΛΨ?LxΗ5e8TΈ/Aιoχπ€Kψqγ%¨ηόΣΨ +!JRvξω©%Wκ΅Άη/³•KiF€ 6BψIAΏ5ΠΨGπΗ#― ±wQx&omI»βap@―.[Ν9F6†²›Ο  A°IV6Š:’dАϊ\„]ΥG’ν!χByn‚ƒ[η%’ŠΙj‰–nUDΩΜ8Ξμΐ₯ΐΫ?_ χΞβ[μΞΫαo7Ό7Ι‡@™±ΠψŠxΟ(Ύ_ypxMs)Ψ0ΦΜΡΆπ¬ύ> +stream +xڌΈP\ΫΊ-Œ[pχ4wwwwwkwwwwχΰ\ƒCξξ/{Ÿ}ξ½ηώΥ{ΥU«ΧόΎρٜc¬ξZδ$JͺτΒf  Θή…ž™‰ *―ͺΝΜ`bbe`bbA 'W³r±ώێ@trΆΩσό„¨ΠΨεMΜΨεPdq΅0³˜9x˜9y˜˜,LLά‚œxbΖnVfy€ ΘθŒ@. +rπt²²°tωSηί·*Sj377'έίαa; “•©±=@ήΨΕhχ§’©±-@djtρόT|–..<ŒŒξξξ ΖvΞ ' j:€»•‹%@θ tršώ `lόg4r€š₯•σΏͺ swc' ΰΑΦΚhοό'ΔΥή θψS *-PtΪ ,χ/ΰŸΝ030WΊ’Jdew°±©)ΘΞΑΨήΣΚή`ne (JΘ1ΈxΈΠŒνΝώΫ:ƒώΔ»[Ω›όόέΊ1@BX`όgΒζs6u²rpqfpΆ²ύkFΖΏόΩfq{3QΠήΕα―ώΔ¬œ€¦φέ“ρŸΓ΅±ΉΫ{{eneofώΧfŒκφVŽ@i±0Lm³ΊΨ™Έ98Ψ™@GΠΓΤ’ρ―jžΐΏ›Μΰλνr˜θkeόσ…ΰνlμΈ8Ή}½§γ?WΜΜ3+S€ ΠΒΚαΏ³1Ν΅ώsώNV]¦?τc0ύυω―;ύ? 3ΩΫzώ7όο#fTΠ–U£ύgδrŠˆ€<ήτ,άzn&333€““ΰϋŸy”Œ­ώιγΔJΫ›ƒάjχΟ>ύ»e·8@υ@¨™Kτ‡Ή@Υ]‰ΙτΟ…ω™ξ‡ό±ό―,W’οŽ$\mmφSύ πρΫYΩzώƒψΓ\W—?*ύΡ‚ύ†j%]y ™•«έφJ»Qƒ°½ΕFΣ3³10±ύΛnε,aε4S²r1΅όkώeWKoΆVφ@%³Υ_O˜?QLLΛχGd¦6ž"Ξ¨ω· ψGCYWάήdφ—ΨXΨ9ΖNNƞΞϊϊΰΝόG•f@ΏΙ `d°Ήό ό™Ρ`rBψλ`™ώÎπΉM]œώνoό)όουίͺ=€¦+‹ SήλƐηzawϊƒ)˜­žΘναvŠ₯oK9Ψ,ΙŽ"Ff_q'•–+Γζ}h.Ÿ§=τϊs7Σ€ς]ΐΔ‡χDι Η~έυ[‰Ν@ψ3œι‘{hW‰ΚD(FΊPΊ”Ιš‹ΏC9XjKήzœtΈΟv€²GT’4—X἞3ˆhm²—qm=Τ8t'έ W5iυ™7ˆh«Έ'‘Ό_ž{Ίq™γίsF†ΣYo‹dλ8Ύ†aΰazɐδM$βa%ζΘ΄9ƒQˆ°?ŠV¦&β•Ϊ…QlBqnΎΞ|Keum±P ό€θ7ΩκΪb_δΟΨΌ%4άΟVϋň„ΫžiIWŸN&B^gφ8žqUΗj›?…M €iκYϋŸ•WΕf%2³θ§ςΫrn‹p(Ω;L˜kώζK`ΰ“c(IC*XοΈN|‘cx(ΆͺΦϊœlάώ½k| όDvI„υ#0A«;λlθ4±Mšύρ'š8‡Kƒμ¨Χ:>/~ή_%?Jι[$―Žχς,iΌ+J‘Ϊ’šάX†Ό6O¨pΐ<*2†Νe%Qνj ΗdP;*W“ίΪμ5Έ˜ςyaŸΉ,‡Bώ–f…­(‰6ŸΈ)\‘Jrψ"–#]Βζηι·0 +γ$τΕ€ΠϊΥSνwh,άlEIVΤΨrɚhbzm:ε^πΏJ’zSV±M„iΙΚ¨zqSΒπΌΧ›ZWΌ~Θ κ„ψtDЁ°Ρ£όΪaΏphΒΧQ}iΏέ ―‹hϊΎƒ·ά.ΌŸΡίνbΫΣ0-υV¨ ¦θα<-_DͺIΣ\VΖ—™—υ€ΉΫ¬†γ?K1SsΒUΔ[‘ΖΞ^‘1-Ώ6WjZυ’£Gzέq ϋτ– +˜}X{ͺeΕΆh؍¦οΰ9–OZˆŽγ%ή%^-χufs{A;P‹έτgrρλLΞƒ.Ρ\€snj8Ν,HG‚¬λΤ/d1<¬ ;ΰι`¦žiM˜λo₯³mΕδ”u YŸZ•θνAχ]·έ”|³Ϊέ[0@ϊsfΖgδ˜|Ξΐqφ/ιfα7% Έ4o}Ρ*²wH x/ ’ΩšbP'Ψ<΄uΤOz…Χΐ‹©©― [ψaYwe‹X‡f δ †Ε¨ˆά遳D9HΏϋοΏŸν0m§‘τΙ1ΟHˌΎUΦ*·Yϊγο;τ=ǏΓΒ:'A°{2hΗCώ˜ͺ₯Ύ9$,R+Ό+{4c–Ν/²©žŒ²ήπ―.‚#°gςΒ©’¬%ΪωμΩf\©©|Bξ2+ΣΨ±¬U‚_4ΐκ"νΙ4ΤSΜ5%rX7ωK Q°ΨρŸAIu4Υ ›nWNŠσfΣφœφŸJΠ~@šsΣΤœkρ<τ ŒveρΠLmΚ™CLΕb r'Q'Yσyc}c +‡§ž]N²¬—₯9 Τ’€XΣh₯τZΡΉ›&Z₯†·Ώ|œωΤ΄ Φ•‚6˜™°&7½…,svu{b fUρ¦[vΝγΜ—»ήΞ2@rƒΉ©ώβ“ –φζ»FΘ™ωœzΤ©£R οΣο>ό1YώΟ °+ϊ( Β;gqί”Ηωξ$Σ ΡZ8ΠΥΣυσϋXW³°£‰Žqf?, &]₯Υ΅€/’ϊ3œ0JTŒuL0₯Ή¨δμtFA0Ί-ρρz‘• +€ϋ‹v·—2α f‘ϊ†έ΄ΐΓλSαώ¬ε»ΚX'½"Δχ~dΧάβEΨ ΦόΡϊ‚€κγ\owθ:CvΦ§R‰K‹ωή&Ά\‘ΜγύμsαŠjτnΗΖ™λ ”Ο[}›sl-°QΔα% ³—Μ΅JζΈ?αkξθU‡σ9ξχΊ  œJΚu7ϊ݌Ά»Z^cqξ{ύ=ž$Ι>ς BJOg| ;¦?M¦΄Uΰ¬ΟτHLe¦˜Φ7d%ργ~ν7'ΑL3Ρ]ΕΓxδ3©ΒΥI%΄²ε…dΪή-4¨@!m—ž·Q, Eςέ.x3.4m‹¬ωAϊCCTκψ9—λ§ ΞB"4ŸηsŽ‚+’q +μiiEχ0CΩΘ„‹BsιP„-K#j»°΄~85Žό3Š«jΡα‡vڊ} ~P, LfόEAΡh"εoS!0”]άίmc>|[ΣΓ%ιO=Δsψυθ§+U΄)¦Θ_Zω +ύ<)νΝω2ΥOΉH { Κ•»Vφ)Λ²%›―v 3!†+¦ϊ¬υ›ΡνοX΅d +½WΔ.ψžCΑ»Mμ‘ˆb½qL΄©HWΈhŽ£{Ζ¦l*] \#§‘χ•"ΔΟ<)Νγ5+ήΙ€srθΖ«gΖ΄~Λ—@ΓGν0 u&rI½€|ΪΥKλt †ϊJύσžcψ9 ­ˆΊaͺγ%»΄ε“Ό|'BKS’γˆEkέψ“θΦ«X«œŽs“»š’«w"ΆsWηLύ³R^4-}ξΧΚBυθτ‰υg%o²Σaa΄Υ]?΄{€œ‚LΥT©pVfYrΝΌ6°άύθTΪτ 1ΆF΅—ΌF XG‡Β³pR<Ν΅;ΪRD&ΜΙ-»ί€F½ΫέƒGU ΄η²πι +Θ$*~¦I‰­άjΰ :άΒzΞ₯w`*m’Kπ  ‡y\y,””]V²lςShΦm‡’7<…»VΓφΪόaΟ7Ÿ–σ;܈νΟ―²8σ’ eΧΫΞ™‘― +< JμͺΏ^EΪ‹y‘ρΘU†“5Χυ΄ωΦmη΄χή_vMΣ;G~~“Up)>ΆRώV&/&šΫη„PΤ h­]^θ‘ΟIπ‘εZρ J;Ό‚ω$j%†OmΞΨ琏κϊϋ΅ΑIόsZQοŸή’Ψ›ϋ’œδ]<ωλΒhφMD!{ό Ί]† „ +m)[Ÿ‡ΛR߁λέ?-ηΚBœ*%SpΊΈ @<ΊJzρΚZ°­ήΝΕσΆΉR“ž4ρΎ¬«¦¦ ΈuO\~υkμ• ώƘ$AΚ‘ΞPΟ$pqO».«<Β„G:>_=Ηxγ|²yμ’ΡίYͺ"™tΝ‘ΈΑΰΰr6αώv]€±;QΩD[ƒVΪVσ΅ο7gT“ΊsPVνbεˆηΡΑψP2¬Z~Œ(ϋoI'‹]u˜!TφI²ψπτ +Ÿϊψ–ή(8ΏΤ ΙͺύeΌτS€Β:y·ƒ-*k—ςΰ½-ΝL‘ύΝEΆώ΄ώ°2Δ€&n¨βr"~„2CLqάΧkxέœΩ Ÿυ—CΈwΚ £‘.ζ―ΛΘ}ΌΎŸQCυδύsM©x–ρ ‚‘a{:™lΔ²αP•ΓΗ%•G(B19j5)€JγGRςY#eYίν’ο{·ba9Ι‘ΘΎwoC\ά o‹εߎLπέDLiύΕ{iΗΨΛ"(lΉο΅:”fν-ξα₯%Žx\R–`TΥ§MG­YDνtK·«9m4_lJ‹*»!&΄κ(ƒ/‰Κ°sP°ψΤΛ—ΦΈδ@fΦkΞKΘ¬¨αˆ™_πoζ\(΅RΝ°ΈiΕ9φaΰ2ΝbΈΌ‡υocμ:rΓρ>ά ζ}[ŠΓ*5ΓwΓ‹Φ†ƒΡN{|μrΐΈ?Ι$ΙRΚε”αΞ Y+‘pοή~%Νξ·H‰Κ™^QΊŒ»η1PŽW¬LfύΥK#ς‰΅)ΨΥώ$YΫ–τςιGΎ‘" πέθ'Βη ²Ύ-£ _zn,eΖ‘ω&`@žΰrθΨιϊpbΤsΛ}Hύ¨2“λ^ψ·“o§Υ†§,|sΟSi#΄P¨η.„UυΊ39 x2…ο•ŠB%δς‰A›'H.e-Λn0Αa zψŒ&{sƒ·ςXγLμΣ΄6,šάζiΎ•mΤΩη‘Φ{V—πΦκΞBλͺ°²AςζΓϋΛ„E„όΡ1]¬yΉ(Λ|ˆ8mt¬&κe;§ ΦΦ{ςrX©$=0ΊiŸ{η:g‹R©<> Y΄½ύπeθ© ΛςO:³Έ‘`vt›s–ͺτš:θρȐΣ2Έ”mυOγόϋλξ +ΉMΖΆU<‘dϊμ[Jϋ!³‘βLQ$²›¬‘³€sΏ,ίΉ™ΜΕG¨xHωίΗΝM—²m$/D'Ξχ’ΰݜ‘Γ•;$ex·oUHsΰˆ¦ΙΎ‰B2ΊΧηΪaΒV―«ώ$«U~%ž•»/ΊKlΗφΘp‡ίξ)# +•hίΜ†¦@ς# ΩO˜ +σiε5‚BFt{%ώΌZ§¦{aΊ£„ˆ"•LWΗ·Žέͺ† ρŒΊvqσXΰq5SNΔvΥρ4ΔlT3#M*6‘=κW Σ_Ά­σΨ¦δpΏέγΠjEν“%V„Ελ7Ο¦²rΧ7/ΣȜΠ=υχ;‹a–ψuBl€"ΜκV?Ί«˜νφ£Ž°•ά Αλε@­}σe‚ieΣδΛc6ΈnΆ—ηLΉfΝf=έLEψ܈j8TΘ;ΪSS χ:b_ΰ%±ξΘSθpψ£C}ηξοW +ƒRΌ£¦ήiΧ|ž"&œ•Ά$ρΧΠB•α?―RαθΝ +‘ΦΊ]χOυΐ¦ I3όŸ(]/Η}°HΦX_™šcq»Š jSp©2ι½Ϋγ;κ_XPU*šQ<ΎŸΑ}ŽP†»w±˜MάνzS}ϋ–—MΊTe›mrύθΕΫѐ!νΉ 'Ύέ?qιΰTa#Ψ&#4€θ+`MΝ°tQ}:"' +Ό >ftP}ή\ι«"ΕeiqEΘ&±?Σ­Τ;—λάRβΓa6t”?dl™D(7βί_Χ«Ϊ’η[?ππ½`έπΐP7vύΘ0d½τ0 £BŽ~ο=a‹ŸΒν5X~ ρΛγ€0¨1ιeLG5ΆKx~ΰ«―°Μ΅ Œ1G,}O&EιUΦμ,V|{˜Kd£Qτzνd0mΏ³6M©Φ¦ψ½΅ΎΌ<ƒ~ΚΖ‰ž’›Ωόψόύϊ”N˜έ~\zK"*•1GπχdTζ’Ύ±3C³γ όΌ)π5Υ]³©ˆιΗ—vύρ•ͺ…ί¬ξ9ϊQ9q,ΙΥ}‰‹ζΟΓφ”ϋ†]πςRέŠgςΡΆ’œ›–΅°π‚>Fΐ§‡Δ,ͺδΞωl<¬6ˆcto"3‹ΛF©a%t1<νZΉcεκζόμ μkl\δ^―αj]Jъ―ΏδR㇍kΜ4[†©«LΪΨμέτ4φ5lα[[Σ‹x~ +gΠ0‰ͺ‹EζeΏGαΖΣΖΜΰ}j_Žΐ³;†?ΐ9#!Ί&όvm,°ΩΑK sΦό9Γ/2/‹—ΨX1εΔ¨;]Μtœ*ENV`C¦άY‡σΙ +-œfλ@Άi@Α΅_€2&CGβθ§G³ούΔ G_½΅9:ήχbU~§Ϋu2NθζiuΎˆ.7C\~]†ύΔρΑά#Jνχ¬ΝVΦAχφά²›Šx³΅Εη_ vυPDύΤk“e„;F΄'+j!•Θ0wwdΡjj^…ou γ‰ήΓq d‹!- ΉpΝ^d€Ÿw7KΝ‡₯ZζjluoΤ…6(c―΄οΗ3ϋ€qTmτςΉΈΡž‚©TYΞh¨—ŽšΡPrψމυαqΙNΐYe_™«j/$―…b‹bwKJj¬8Ώεψ―]QŒͺmκ+eΨ(τ.Ό“fŒZn† ΆΞμ»LΆd³NPG™#dί<φ‰ΌόΦXŒλ—αμτ›ζM•ŠuΪl]ŽRHΛ{*ˆ1‹)CΖϋΕcΩΏ—|Ε吏Ύ,Tή’΅ £RW”μΒκ Wφξ½τ Ώ₯tήΙt‘Άx.ΣίΦΚΣ‘FΗ8ψ*T1φ·Ξ< ”>ŒΦ 76έG:ΣΕGm— ƒΘm]¬*ή±Ροθ!β τ΅ύ…κ F¬Οāτ²š‡>κtL1«ΕyH]γZφΖ™F"η―ώοph¬h,θϋ }ς­œŸΓέΝJt ;’}‘nkφ|;H) o2τ˜Σ9"; vΑΎΝΕ.;)ΎMϊΟήν7¦§EΘxF"4ζnW2 6χŸhΆ\&›l­€y$H²4£*ΜOnΤ!'F724†Ρnτω¬Šbžω^δκ6ƒi#α3΅œjC>*»Qι+ΎφάzsζV«ƒw –β*ΎPkΔΌςΪ…3©’uΧkδΟ₯P>ΔΙ6Sm5x”Okα5)4ζΐπέ^Β‘ρ£‰€λϊ +2πByφ5;[άΖρ…θΎ†D‡Z•ίMΓVυΚμ wD`dKdV­#„ήωz„PaγoͺUβή KήV­FήΞΧλvHzλ«βΦΒ³ΔςΡ€ήh]‰.%§²Ο|ΰ\½ψœlpγ₯ +ŒΔgŠ°§σ.bΘή’d„ΥζGπ-ΓΙNτ¨δͺχ +2Πξό?ΈηBΫωΓΤ‚IevpŸeΜ(a ;ΪΓTνΈιn ƒ9wH ‘/œzή©V)kqν4/ΈΝ ±1 ʘΩmΟ ™eΤJh3bΫΫ+ό΄€τρΡς œΠ:•%ˆQ]ϊφŠŽe Όp°Ϊi]7|ZWυςΟuϊζΆ„‘uΝa;v7#ΰžζκ”νh-ΦΓΫaίΩΪNœ³«hέ§Vυγή]΅N΅“Σ φΆχ')ζ )ψζ~Mƒ ZW²Ÿώ‚!œβ£ΊύΓIκ²Saœ/‹?Šš› +‚Ϊ―Lʚ墈ΑRμ σgEΉ>!ΌίRD»†‡gΉ“ΊeY^Ώ,z=ΛΚj€~~άkΎϋχy'ΑP΄Π)ꏯ«ΞαKΠ—½s…@τ%±£ΰΥΕ·ηZY…ˆ(c‘G‹RιΝMσoې _XdaΌ¨fjWμΔSš:νχ\ΛJ΄Ί+dΥΪ¬8―8ΎΫkΥ9ˆΚοOd4r$‚ΓΦκτEt!B\`*]Νe©δA+™£xJf¦„x±›·μ0C}KρΑ Β•ϊΉ>ΥΌ鴘5v§Χ˜DΥ·Ÿ,€ͺƒtψΓIg܏ΤΠX †1ΔֹՏxSŒΖΦUί‚S;YMΖνΆwΟmo޳ηgτΕ€\‰ϋΛ§·‘ZΚe’W†ύψ€SΗ ΗP{Ϊ'ԝŸώX2ž6 +ιύ'fφ­ΊOϊ(ΜΚκEίpg9_;›ΫYΏΟJΗ.r$)ώβ˜?n™έ_{`D7Š_¦%dz!Ή΄©ϊ <žέŽπΗ$·>έ"ρ7ˆ$gkIQχ”η]λοoρi57+Ekˆrhv#ŸƒώIJ›$6ZΦλ@ΖAΰƒΘK% ŒU§qφΨ±U=[(&R ·ι[Κ΄έφΎ1 Ε‡M‡;FΈ†XύyΌ―·’=ψΨ1ώˆŒ>Ό\9I!’™Oάer³‹‰™³έ˜†"\=)κ²π`¬vΕN~ΰΒBk#Ν Q~/_ΐŽ©‘搆΅ΦlƒZΜΜOLKΫuͺ›«0ΕΈ:Yα…β°αEŸα!ΠzoŽ-ˆd*ŒΌaΪ }ΞΞφπR@E§g·v%έ-ŽΐΦB%PœίηνΜ€«hbΕ…κ˜pl-A>π0)ͺασ$x΄ͺ» ΠgμΌρ,`’Ξ›6Θ%=UγΛ<™γ3ˆή½γt:¨Gϋ°άGΦ<šωlΉ0lΔ]Ω`wΰΗ Γπˆ•S™(¦±*¬IΛΜ δ0Μ€—ΎΨΊΜ.vφρ°ž Τ)2άόrΆΞΒΕΜeuπΑΟ»Eοδ33-ΪfbΒφσέΕ…·οmδΏΔL3―ΧΑXΔT#΄/©X@μW²&ΏJX.5 hny½ΙΕ! πΈΘc[} OΌ6y9°DzΣ“χηΥb$ύBot‹#[’φl‰mςS>ZΆϋΰ ŽΒwa•°,£#ΩΜ„gΕ–LΞ5&ΠόsμΚN +Σ–žNΣaƒ<(έ +³Ηέ”m ΐΪΜώ$Ϊͺψy[Ϋϋ`ΑρŽαk_IžΥΆ^ΰnUɝϊ ΦͺρΏ₯ΓD‹ζοΥ δŸΖS5Β”‘egη0ωSy<2§)ΤqR­ΌZ©«πL%ΌžhΛ9}"κba>΄άzχs gmTŸ'Iϋ²d`Έ€Ψρ‰B>Ξ>—ΗΜΓβݏfΜι¬―·ΙΞ(­Y\l1˜©‘pΛ5μQH™MΝSvά°sq™,Κ5QΞtΌω8Vά`Ή[œΖψΫ·a¦/Υ…Yt»ωΊ56C!k'„S$3α˜ηηž–qώo«[ +' ίT 33Q’/Ϊ΅Π³Όol½δηhΦΆ+³HΤή°΄ΗœίΩΑbYkPφάp0XIχ\$[€Ο,xΆg_κ)Ÿi'ΡC‡œ δB#¨βΪΚ9« LΨΟ%Η. d¬1±|°¬£σΈŸbWQΖ3ΝJ!lξ΅ϊ$c3§ ΣΔYYoήΑχ–w½.h ―&)ž=4Ϊ8bœoΚb°αjΓlžψ ]fΫ'uG΅ΈΫϊ»ν§ΚaΟ)ώ€°Β„Η"BNžζΑ­Η £dΠ„ϊΜƒZDS)kQΎζ$„!έη οα’ψλΦ3zλQ€tηΓκJξG€ε‰±ΊζΣB”D6…¨•σώnĚh& σ<…,“ΓΨzΐF¬²Wγgσ₯ςΆϋSΟVy½ά㎎ΚzνˆY§”ϊγyρΕφ§ρ{°Υ-%>Iξ ίώE/Tmκ%ι’lƒ\ΠΡ&ν½έ6\2bS‘p… ΤΟ-•Tδ½>άƒRQ#Z4n–ί5’fSyΨ¨u”6ψ†j v€fΜοΆ +N₯žόχ‘nGxπŸ-ώτή/%h΄€WϋΔΨvτ“©5†‡/Ϋ₯œ]Zb5"MΛΧ’,­'α]ι“₯(ZIΏkτ[_|›ϊ“§υ ϊH=EyvΕdϊΈ’Γ—ϋΛ0‘nΕψ܍—>όfυ‡μΓ©[PG–'οβcXιη ‡vόːΪK(Ία2‹ϋ2.ZŸΦ― ηF>Ϊ{y0šZ’’FΏδ>'12œΙ&-tφ§kqZ+V»>†7¨Pˆ%αΟΕΖͺ:†ŒB­ηm^£ͺΑΝ―Š&‚ψ©fΟU•œε΅Λβδ/RΚGœρuΒ±>Σg!βΌH”½meίςGΈvφτj Μ™r΅ΝQ“σω‘™ξw™―£θ˜dsέ(¦uQ5Η„.‡^(_£D ιȞΜZAzn¨{wΦ|ηŒaB‹€‰œœ«“U–ΜγτAލΉΤ€6π_£Ζ₯Ή,x‰zyΟΒδ‹”§5΅¨6ξΪϋλ‘υ<λμΦ¦ϋΤ`ιRτlMλνά`{"—ΓŸ@·ΒΈ4Ή<_π8ƒY—·w™Ψ"Λz-yNU’,Θ&R‹rΜυœ'9+ΠCœQΪ ‘|θάN:6œ†• +vΕsΗSβqΩ0`ςω ΅½ΡΏ·Ÿ…Ϊ@ζVq!FBI\(›αΫlεα +WȜΊΏsbn«Ί|#1Έ‹Ά\cD°Μ\υaw,υ νœT@Y^ξ³>xΨ&‡CΙχꚰ­]<e«)’κσ'ΎΘ6zτΞΫ‘-RΐπJƒ~ “ώΌIͺΌ΄)—G£ͺρ4Mͺ‡:Έσι_ž"weW‚TwN HΕ’*υF•0ŠϊTσΙΫ˜ gŸΘZ)΄DΈ‰΄t'[Z" dE{N3h_m­ύNDΜk²|j‚¬zλλqŠ"h,8,½J\¦Η ηVχpNδN}#5τ;”6~>ŸβX|› 9ώΛ© ϊ^7ΙΡhTRp—ΐ“Zά!23TyI±‘Ιό,κgPπΊυ=φΪ/2XΘ­%>‘ D‘]˜,;Ϊv'“8ξ+ͺ‹ι]k––<»'ο_™Š”y BŸϋ*‚/§vC#!Ύnπp7gυνNΉΎ_M9c‰/Ω-ζ{ΓSvkt8’–,!±=n0dψX6€o”€ρ―|ͺπ‚ ¨ψ²mmh1Ϊ#χΦ;(ξ:U\.!ύΜ:sτiάF›"…B°3Hea˜ΠΏυΐ/σ‹FݐdnU―Δ +„ z0‘¬Ψca<‹.ό—©8Β]λ¨„Φˆr΅CZ•cΘΏxι {bΦ·k*zΎχŸΌ½©ψzh࡟RNνrpfa°‚0›P\κcŠF(μ=S€^±Γ{Ν©ΑDύ₯ΆkĊ·O6° \‰u2χ'§ψθPΣά˜}£ί푁ΧεŽΣb˜½>©ͺp»‘ΩΖ½Π.ΐ"­ cs`ΔΙ΅`vΉνlAH +_!«Ρ­ρ}εΠ1hΩ5†ΈB$"d}$z%4σ ]*Ιρ;™ΝύΞ#{³n#”ZΨΌ ΟBΦTφ0ρΖψ€Ο6 s3qeΣΰί­Π€ žJ‘ΗQkpοŠW υ¦ψήΦ©ƒN#Ž›o½$dΓhTσ$c•S―FΑ짍τ3| ζU-Η{ρπZ;ϊ[κœqk`™i΅Aυ§n%{}dΙ;DY7Ζ{ΆΌž!;)Œ¬xBa*²y*ϊ‹Ϊ₯QΓολyΐ6§ν>Θ―d€uξzy@£A­19?8Ε*ޘtρT¬ƒWΐhšΞY8—δŠ―οdήV`uΒ8=ψ`>·ό#WwL8YnΦ“Οe2WϊgΕ\zS΅ΏέΈN-%I\ dωγ=(πΜ»“nάq4κνς―‹ŽΤΑ3{Ϋ.SΣξσΫ΄ΐ\Ω>ŠΨ.†ζΑ6 ³ŠβM―Αlζ»0₯e|Rg΅ƒ‡ oγzq΅”&“Εš3ΗΆΊfͺ³WΞjœ¦β{ήHΝbf‚\ ™―¦|<©Ά•_jΚw%θ,Lδ§ eGzN=©wƒδλ5_5ΩνŒ,ͺέP ψ~ ܏ωχσ9ΊhYΟsM%G"Β–ΚTμΔ’ΊBy>›Sp­=E²‡2lΟξx ωb₯ΩY±ψ€πF°„ΝD³·‘C²7ϋceo ΛB0₯”P;¨6˜Ύ6B±xΫχš8ˆβ³;ΙΈ‹cφ{²ί ϊ‡|λέu†yh²\%†ΏP£ΊQ„Ξ~"CΥQ/.μ”Γι5C’1‡°•΅GX‡ό6GS˜JΓ₯k܏|UξΓUύ±Χ?$†ρ:΅·ΐΞΐQΒ₯9‰iqVP€ΏΠ” ζ-­χ ‰1«Ω¦Ω +I-βt…ΖEοΝpήΘxDœ>PˆΪBτ¨FwœΒ.u$σΒ"₯dν|(Hθ}³Δ¨œΕ«+Ya‰ΝώδχG QξηΒk5»AΏ;ηΝhι)Αžτί1u―J !φ‘μΐν[—=oςϊ}˜δ{Δ‘mέΖlZ΅Wΰ!Ηο>ˆό½7Έ“Eυ‹iύ~h±“Χ}ΊœZVͺ© Χ•L€'}H=ŽBœ@3ςΝτλc5†NžŒtίƒαF]8b2>iΫ)KΏΕΕέΊρe0hp£=ΓΑΧ™άϊUgΞ@αΎ…C΅+hT7\[*όΔZkpψLΉΆj\ͺ°”>.δ<’Ζώœ)x3,>`[ΪαΐμF’ύς¦la·d³ ½ΐ`Œs_­†ΑΑHT’Άώtδpwρ\ζφK§ώέq&1Ψa© +’’:ϊ™ΘG’d±l*—έ&ƒζ@ΛWώ!-RήΧΙπΪ½γ[ +: νTšΔ’™ΥΚΖiΊ%²κڞ:1ζχμ•zν>{VΧ³ΐ$Oβζ#‘ iY,jυgΈΘώ<ΉFg9ΌT/Φ λ–uTpνε6,ϊDKεή_u±93 άδεNm[…0LαΜΛόF"•VκΨλw}šΕΐw#Ζ΄ƒ7FχΨGfςšaή‡%AL¦―ΑΦ+4—g’αΊ½«™4aŸΉ»hy=V΅ί` iUjΙ»θΪ$iω…h9r{rέ%€)΅Ώ +0ΎHΣΊΤ!δνΐ.֍ —ΰ°<@j}μΣΉκΣB~Α»ςΰsΠ#ŸΞκ:iΩZ<ω]hγΦ`$[μφυ +SΨv +ΠΨκu,„1­ 1 /žjώΕP~‘τ(bΜƒΗ{…bR +f‘ΟΜ-ΏjΠ“έoΛΑDξ#‘• Χ9HP6šύKΊΤgœJ²^ ³…@b%ΔΗΌ+Nf€±!FykqsFWD= Ϋ΅¦σMυαΔƒΌ“NXbuξGΨμΦ<ο;βΒ 1BΊpΎξ>YλE?χ>―»ί_:@~Ε·‘ν«ΚR‹‹ΔOSO’κ[hΕdΰZ0ΞΤφߐš`έ퍣}μβ#(ΣτΛ‚B—οxηΕiT˜QTONž}e8χοΈ–.2ΝBιΈ―Ζ;3χƒ L‚ Γ΄gξn EY’”oϋ^Ώ:>δψ ,Υ‘Φ+I=ΦtŽΌCaQl¬v«ΣsˆHΛ Ÿκ<κ…]„Γ9!Žv]aΈ*ΦZL£hΪΌ(3Š©-Δhh*φκθΕ#Υ{³_χ*ΫqWθM#r€ΐεΉΗΫ(οΊ>`Nέ΄δ +G4žυ9ΝYχΙdδκaL`gfš'U½5ψ;§KFέ»˜‡,ρΈ3—ŸLξ&jρύΜΕζ¨Ιάwή c‚?Ώ΅ί|FΦ57­Š €ΙΩΝvrSnαš/ε9Ρ—ψα©„]£[7κθQ0Ϊ!G{ )w&>ΚΈEEΌz–Β)?ΥΉ‘ΗH•β„sθgΜ”Ϊ zœΉΜΩβdk₯g2VΎ°oV1Q`gΰ&…˜Zvγγχϋ?r₯χŸu΅ πq―.εΦA‰ͺ΅V―g>›°UΡΩB€κΆ4ςα[+\5Σa3Υ’ιοž_`±Iίn·:p9‰e‹j³„‘αqN*–7ΊŠ―χ|ψŠΌ5CΙeŸvΓE-ᫍΎ§Ια0*R£rI΅#ΞβδM@œ1=ΪΕΣΛW2i9hšHTq ‘„„9Ϊ>ΞΏ“ˆΕWτމ K2’N±+{ͺόΉHmωZŽΟZέGΨUiœ° fp?kΧ ‡[Ιφ”αΑhAlωχ οNQ1vc& 5[©‘ +»―βυ"oΘw ³ρΰϋGIŠΣEΙ!ƒgΥ²H»ωKρδ™žA»ΗπΡ%5SX¦*‰p#jοΐ$[² 3(ρε.ύ”j;…άα(ΔύJ?hŠΐτѐtbDA)ˆP™Y\₯υ„jϋυΔlOώ`χ:Z½™+ό-Ρ02JfqQFˆλfψFζΕο9*U€―ˆ#Ά²Ž:Ζws.F^ΑR†·WΰΑ–=B„uη i²H_MWȎŠήf˜§Vψ⌬έ5ϋ +|•²‰"Hύ5I©ΦΙ)a'­ξεηewΫI„'O€`ΘΆŽ>“ύwΞη{|Ξ‰[e/J8ήΦ™¨g·E¦^Š@ςp0³M'ˆ~5(=χEKΦλ΅Kx8χp`IΏΖωϊ‘I½KqCΟΏb½ρΎ/O’>ŠeΙ 2’δ7fHΕL/‡E)ΚŽί‚”€Π {EއlΪΛr‘όθ8!₯Έ©Doug%ασ~VgκΙ H«ο8π9Ψkāyΰ+bώΑ"γm<6ς +›²΅ψϊΜk.τ1ΤϋV‚©eυυ¦ˆσ¦ψ¦RTb?ͺε»ζ’Οέ—ΐl½v΅Ϊ₯ΘjXaΏΐ»B%§Κ,yJ¬mπ'Oϋ―>¦Zga2˜]&„sΤ§χo!ο† +RϘgˆφ]pΗ·2F½‹³$D>Ί_‘Ά«lυBΖλ½›sΏΊ?δuΚκmΊωY„ +§Ώϋ‚Τ›‹΅κΞύ‚Άέπ+Ρ]rWΚ΅―&œES1&χn= WK5.œ!υ<ίcŠwϋΈϊε“πRκ3ΪυΓνυJα° +εΰmf³pιcώό9Υ(J']ZPψXβJNΰ»C ²DΙ‘ά¬Λm;h,‰ ’>3Z7 a«n©0ε”‘¨QРۜ?οαώuρρ#Λ /υXK%lκ¨μž2œ•!ΌΎ1E¨±<μ%X’oeFέΧPE5&έοMΓ<ΣήρcκӁ–Δ' ΩξΙmQp‘–K£M#|̐΄ŸΎ9ώ=UΓLou0ŠOWšDeβθςύ‰υ…Υάεύ4λvt»s*ͺQp ^·Τcι‚".Ÿηi…ƒ˜[Θ~ίυ—CB%1«Δ7Ί Ÿ6χΦωšΠPG­ΫΉ/’Λ»ΆΛΙΛTVχ€ίίκ?ΜYΘKΓ•Ω…}βΫG¦cœnΟΐ}&φΞUΖX­ξωHΖΧΉNΧ†=Τ€USƒ8;ZynΨυݍZOIτ…ΈDž”ΣΚΤΫ§Κ|Άcγοψ!ŠJDυtΙ΄)-υRΝm`EΉ=!Mtϊ`δ%.KΕښNώΐjΚYγώZ ―ΒΌyιΙΏO‡‘ˆΗ&.ϊn5EhV?ίj‘³€0eŒ€δš [,b,ž <Ω}9H^κψ€Μδ\ΰk"α΅X¦WŽίνEΒ¦§%'Yr™xΠvƒf ˆ€Ϊηiλxβ*”Xe­u©¨γT:[ lΪΰψ]Κ³\ΑH£œHμ³qΐ—β’΅1‘ΠΆΉŸOΏ[ƒ0E+UnΪσk)k“ΜΓ¦” ς£Ν§ {τ³—ͺ⟢υJπο<φΠΚˆ2Ξ?ι€›Ÿήܟη?Έ¦£Œ/ ι4ψ™ήΒ;΄zΎΗŠtV§`¬=–—›4°―dΆˆ³³'σΌ4>;i“=Ή{JHΒΡoάW”Ψ^y,Λ@R ϊ!‰sΐ­sτd‘αο0ΨuΒΠύ|—|ƒ“FŽ΄œšhŒ-Ψ―iBKΠΊ±0yŽ&οΨΩ(“.™ j '°}ίQώυ•=cŒΡyn*g'ΰlFύΥΒ8π SΒC%8ϋDβ[”4FAqμx|fQϊwΆr•sϊ½ϋ°’U}ΠC…άw'Ž&J²x±y&ψ,σfI‡ΜMr-ŸυMi¨ΠOˆΤ|Θ œ"‚†±»οά­ΙϋH?Ω‹o¨5gγqL]šYγ7Y +ς‘50 <’}¬}^±ΣΓΨΞͺϋΤυt6š~μ‘–> Ο*m’Τ¦Θ%ΗtωžΉκ™o »y ‹λUMτC€™vύΕo™zτ„ΰ9 e&±–τ±κ ‘Xσƒ‡.―Εnݘ" rdtύ*θ9 #MΛΠΌ’‡‰aθBXι° ΉΑN[†•’hŸt±^3Ή€?Β³Ζsng]αΙkΊŠβΜοωΕΜ—Yϊκ‹ΑξΕ₯»ΆhΠΞ„ +ΟgBΩsυto "Τ]‘/ρμ‡ς΄Ζ{Β¬_ψόΒυ>χoϊ¨±’’ΈFφτ(lSαΑπ”ΧΑ@•4wΆHQgηޜθέ|Gfs·'šާο_όνG»Ό…mTd諏S/[ +Œ£"Φά;³€ρM€2~繜e'‹žF’5Δν{Kͺk?HΆ?…`M€Λψ"δ,gΣ—›Ι.ΆIy;ЁιςvEZiΜ―YšOUvζΑb^wbυf=₯Boc’+Φ K‰&$Ηνϋ»«³Ϋ?ϊ žp‡Μaβρ—VώςCΙ·;”ΌΗšh3Σ ΨXͺuf œΓ;[ANόΩ6Œhχtž7ΉΟΡX‘¨ιw‚‰ί*Υ"£ˆ‘W4€ μ1P’PΚ°bγF)cσ5 S7ΪW’ξ½b›ΰΘPWΏF\rζΨoQΘθοδήv«ό±›FO‚Μ/{>:’ίνY)i‚~±»Ή¦j«iŽ~2(Χ­εΘΕͺy€žΠίͺbμ,dΎ―ρ ΟWφά3;”ε‡λG£gΈ΅Σ濝5 GcZzΝ%V4· ή,G?U©pΉtιQύ-(ά@dΡ.rp‘Q*؟ά3+Ζ€,Œ^’₯€φSθγαRLφ\BbCyUφI)ΆQ=³ΟbΰΥmΪHΛ―αν HΏσIόϋy f&' ++ηBQΟ >Ε9ϊM~Iy˜ΔiΘ―χŽμj'ω¦Κ-γινyl²β„Αh³bΰPOΪΫΈΦmδ^&cE¨­ŠΦ/J ˆJ΄·Ύάeೂ„ΚΰύB,ιβŠ}[)œ7ˆΩ΅ŒXŸάˆ†tCjб~.^Q.ώ2.gφ)O Ο‚μλ°]‚ ZZ€Ή8mΨ!” +»κγ0ΖϋwΨ₯‹@\ΊšΫSˆWόΦ¦Ί,ΤUc_۝nP~Ά‰PdΓκ}lzαžOΏlx\_Ÿ&%ˆ«ΰžγΨσjF\ '„{Ρ_χŽašmKθΒώgLςκβe¨½Σ3΅7Gσ’ω%̞€‘ΣΒAw½γΏeΕ;ͺμOΰp‰fKψJ!\Ι$o†€Ο4ΚFβϋ‹7Λβt#ϋΙx;ΜΫ/ΐνΉq•†δΒΧOΓCΕ~H¨Kw€B%¬-S…ΒdMώD ‡γ5εPF§AŠΧ-–HΆ†΅}4Ϊ:Hkδτ•}&† "ΕΪnœ΅uρuŒŒΓ±υ|ϋ&‘½›6λβω€ +β{²γžγc²Λ…Ίn(Ήιt•»·ζˆˆyόνρ7δvώZ ŽΒeZ’¦‘TΗσ΅:ήν»-NΨ3θVΆOνΪ’™ΑΨ|°€πCΈltΏfvna τ&cŒΩο¬]/'ͺ‘L]2τjζΣάΣF-Ζ [ψJα–¬+ne›§7"q ιm£ω.Ά_(Χ{ +Wi=ΰUͺή3χΌ΅z-eL˜Ηγeωλœ?τN” Ί-δ–β—Ω΅vUϋΦBFZΌq]Pešύΐ£Αρ‰Wi°’† Œ^#}5šυΕΌΐŽβ₯˜ωΨ±y„Ί9c ²nRΎp³έPj+>ίάm7^A +C¦­πGŽϋΑ’C^N8%Έ]ΚΩͺΥό3n½¨E +ͺ'ϋΞ(Ψƒ#}q©φ…f΄›]NsgΐΫί”“ΊΘΌΜ΄sβ k3Z‡‚ άOϊκΪΧϊ#θtθ’•δƒ’.ΆŒ…Ό"Ϋ$ΣΨϋŒNΠΧΉŽΪ7ν §rΊδΏΡΘΧ '©,E/[XΙ τK\ užξγ«iπQjίͺ9¨@‰`+.άx»rΈα=¬b8oXˆ<΅YιΠ£‚qΐ³YόŠkβHμΥcÝΉρ €Su֏ƒ{c/j›:lƒ‘γψPi¦Τ,¦/vιiΑX8Νa .“‰±`δU8¦‚=Xw)Α„αIq†ΙγqΤeΚy’ ם§Ί­€η·¬©ς#eΟky*±–™’Σ<Η†bmςAH­ ­‡ηΖξ=ϋ<ιΏ…ω{›I΄$Q{ΓEƒήλ†-!(Β5>ˆ·†ωΟ`mv‡ŠQƒ©hΰ—Gω#ΒκͺЌkΝ όΛur₯zw'’Ιέƞ2Ε†_LΜό¨zIIΈ_/N˜JlSp‘h.ωϋϊJΒkθ¨ηŠ΄C“BFͺαόχ hςιQ”ΏU £ϋaDP*[nέθP$5~ΖΫ^³…ŽE¬W ψ›!svyΰύΈ¬Ÿ5CγY"ƒΖwœδ(„bu±΅Oσ+l1ΘΏ¨M]’pΫ'»`‚²:T&g0|UΌƒΣ¬· '΅|7ή Λ€"½sΥόθn,oL'–v½(Ae`©±H8ώ2‰€Πδ;c.Pήxtωί6Έr™KOFe2;=i†fˆ[» '0Fd"ςIΣo“b³Ύƒρ"0ΠΪ9ΰcΘΪ€φ€‹ηqΦ«£ŒTΨ-jup@H‘ZQ“I.£8—Nΰ:΄qˆΏWρύSx“Δήb^υΜΙχ_ b― ―—­μl=Kr:1$ζ‘έ7Ή·ΣΘι—5cαz/@Ϊ^j›DSΰύιEκένEJίH΄œμπαŒ#‹Γ +=;m)ιo»£Ξρ \šήΥ&2οπwœ™ό˜α$y98’&CPέoo©W$eΝΒ)ΒΦ0jνΰώ²ή‹;TΝPε‘•y&—`H•Ζ$™Χ™VΡͺ +MΩήH *©-ο"]½…YΕkπΰJζcM)0όnψ³$ˆ<˜˜ͺΠwώ;μœ*m)’ΊώώcŽͺD™ΒΌα=_#χΜu-„1Ўg!xoMB›ώ<τgˆ°ηZ¨?‘¬ϋ³Ρ{9ψVZԊRHdΊA…ΞήdFύ(ϊΩ¦<ͺQ_Ί£…<¬DΛΧ”Α;‚žvžΙuS³EŠτa[3‚ώ„οΦՊ§χjχToΩ‘LS~Uγ0Œ‘Κ4S%—…ΉMΛVΚ3GΙΠaΝΜ—`k[KX‚σ^}©.#}DSƒΓ$ώpυŒ¬n›~'£ΊO±NΨb?γ`φm ΝiiΎ ω*gΠp*PDϊoπCφb„>n’Χ€EvšΆ-`4hΎdθQ¬ +šˆL—…¬ωνLP¨ώ]X6οϋ#.ϊ»lκπx’*Ÿ„%Χ½ΖσξJ΄ΏH&£ν^hο8ιͺάm&ρΣΘ°ήb$±4šΜƒάpΘ—Τ$F€ƒωή`u)\›²uX|Ϊ ‘Ÿ Ϊ8νηΖΉ™³£*Λ₯˜ϋό4"`έΉ΅Β‰ θPΚΠ›€œΘ’š‚ο»1 ‘λKΎ~8Λy½vGr/kŒL‚j;Έ4‚PΓWKN+5K‰¬κ‚±§ ‹ϊ8(]4ΫG“σvΪ™U“‘N5€N…φzώ%ρmφ䌩ε΁:ALcψΕd‹'π)M6@Κ:‘ +/H/p&›+Ρ£+ˆ9Χ― Κ$nEn«μ%r΄NΣn―§t&κ0=xR΄ΔέC9ƒ/qΑjD€…3ΰ‰’\—σΝ/ΐ†HWe:ُΝςγΡΛ§7ύ―Rπߏ,Pš„N$Awf`~$Y?«ύAρX厱'6€&αnKQI^π|™NΔΰz€ώhJΖZ,ε9eΔώ‘°ΐ^(/_?ώ©>έ"D±Zew =ι›·Š¨ο"ΰΓ(|{[Β₯sE*ΉθQ/£yρΆίŒ,’;κ„ΣY]‚’ŸŒ7Ωt«y@’§]Fζ―eΤ—|œ +, r,}ZzΔΛmK?δ| ₯Ό€΅>‘o/$­φθ=J8΄†¦‹ˆ&¨,ˆΒ«ͺΡδ>dΙ―5xG7ο˜δ. ,υYz"rdjŽΞΜ  7F©›ύ’6 \eΐ€['™E @„oΨ§N.σ-BΓ-Ύk4Υ†‘ρDΗa#MήBCΛRψη3]Ά ΰkπšUm3 –ŽWnοNh»―}’žYΡEΎ>ͺ>«*1Έ‡hΔa`ήνsw ·ŠΤgςΟςαBžlΚZΫ³oΘίO0·”YͺώMΗ•φ|¨—nίNγΘΖL9‘λφ·!F?Σ<47Ι£ο{τεε0²@pΔ3–ΝΓwδxƒΓEΌ;­ΗX}vr~Ρ~Kœ,R<ΆoΙγxΖ£ξ«W¦ι —-\*΅Wλ»,0. ΆQD>ύ•κΛJC₯β΅/ΐ +l‰GΠ«βlΩύRtUΒώfί +L ₯¦Σίΐy-xwͺXρbp6ΑΪ%ςυ”IφR;ŽΣs@ ξ䐁XΗ4IΕΎΥhL’gφ8Ο+qŒΝ—v–8’¬ύ’―^·E :{ΗLYζLΘq,Fό½κ’Ι<%Π Δ+!θΑhxο'\§š ¦1Mδ’ΑS8œόξςώΝ‘ŸΪ@¦MfϋXϊΞlΑ½ήΪYαTβ_kΉ[ /εT$Ÿ Ό™NΠ3Ω-+›DPv%:‹ΞšlWYο(­]zίΥΜZX ?‚<<œ`zJόZ}πΖΊ‹z χ‘ΥΒλΖώΑΎC¬}˜Β5Y‰YΎAcš£gΘΚπ6C—κΩ8η̍dQoΞΤϊqœn7I†u؏c‘.ihU’ˆΫΚΚ’²φzΩέ•q›ŸKΐΠS%Έα«ςj‡XW—ϋΨS@ΰYψΊίεLί3‚Υ£^!ϊv²qν({c)8α[b–œAqΛΪ=lšΪ¨SΔωζ ΅hN/OH‹‚ξ#¨χ›εά;Bώg»˜Μ?NiβΗ|ŠWΫΛ{N†‘_―σ‹™–τ·[ӝ Θ—τ]X+Œ#ϋ4q#BŽωD+δνΧO'%/!ψϋh&V>&(Ί\fŸŒHωμφH$Σβδ7΄a±ΘVqώχA΄1ωα"^ڍŠ@E[Π;xΓ3؝x)#I€ζij°§π{(‘έ)Ρ {!}m»–μΧ ;T Iφۏ%}φ€NδBεQΣςŠ0T‘ψΤ…Ηω–ΐ;„m­ηpŽ`ŽpϊšΦIP™Ο9ΝΥU!“#ηw.hslhEnΒΕ¨T”Β²Έ`‚u›LAK،B@ςη¨Γz—5‘čKA©κΒ›PόP̈Ύ4¨q\ˆŒ±w,gΞ/ΆϊlζB‘‘&˜T»?'-ΨΑπZ \Un“oHΎχBξrΑάΓΙξMΧǎ‹s1…j7―KΧy΄\3ΐ"eΔΙρi’y2IQ4£U#‚’r3Μΰψzϋac(ΧΉC Z³]γ†ΆNαSB}E³Z1 FΩ#_°|fΩ„ϊ―~މAΨ#+lψR ΑPX3~‘”xκtqH–©<§όŠ"΅] +SΘψHd”@_›͟““) ₯νϊ›΄7Ζ’^ ΅½–ΐ₯ ˜螒šΣλ«£2xBτOrWμaXΈωkγaΥ>W†‚³gŽ©eFQ iΪ‹E 5@BoiΖΞ RmθaρuΖW2 +ζςW:μΐ²$Šχ+ΎμύwόΌFηΠ“ƒδl£?ϊΆPˆ +’ζ›4PΝM;>IFΥp¦Ζ½Ž΅·$5;7΄₯άBK–r[‡£ŠZ$P;Zκ[’ε2έΏ¨d^w―eΞJƒγš½™Cΐ§vρΫƒ¬f³Y|,T‹tπsr3ΏΡ—4P7ŒB§ά0¬]£Šξρ9<vlw˜΅α٘c+ZΜ(49{«O"{μˆΒα¨*έ*ΞۊBόŒK¦Υ₯ mC-+ƒ8sY«Νθ ϊlKπz‰9lσ!Χπ¨GoΑ€ρ¬]ωj"(y«ά™„ω­6\ΗϞΣ'bϋ½,ύd“žόΟ¬§νςώΏ3ϊ|zm4Ύψ”μτ jαΡb ω 4ŠΦο·η"’'ϋ!χΣhBD¨οBΨ’ΎH-να!είιϋoyΧψΉr}φG©ςΉ[<@΄O%`Λ /_6Ώ«ς¨ΗP§½ί¨ DηΈj_žρ"ŠB’VΣο9’η«ŽίΏmΐ|cG‹¬G 2‘@ύ>†^Ϋ»–αy΄‘>€Μ€₯φξAeSb6 ( +Q§u2N©ΏΈC₯ϋ•Σk‘ΗΥR~L­Bί…m››_•ΥήΐMπ „•yŸώ#Μ•}w₯a2Ÿt°ΐΜ +¬16―Vώ@Τ5j(<₯@«ίΖ>s Σ:άG}υNθΧ5Κά° βφέ#ΣJUΫ}‘zδZjƒΛXςLПŠεΔG*ϊC©’8Έ ³·ζεdO’Χ.ύ/-/―jIώΩ¬B‚»8ά‘²Ψ0HJ½IŽυW kdΣ–βZ_oO¬ΨΡͺh[ΑJuΌZ+Œω4hE'ΚλBˆtΒ]#J ΧU <˜mό›tΚ#7|ϋόi’ Η₯3KXV^ς¨,ΔξςfΜ@‘ˆ/αv»ύ‘ψw8ώΦ"NœŒ€V'ٍΖmΐΎΤ«a ‘DeH'uμΛ`ΕomΚδκΪtα-iχ‹2γ;²’­?₯¨ ω{εuΘe‰ͺŸ{₯Ύ>`?βΞpΉ―I8<†%Σ'κ('Ζuά6¬W9ͺλ•ΰ2XŠ—E―0_OΠ“ϋΧΐgμ;:26ƒx%_Ρ3B%¬hαiάΝK,e&ςC2ϋΫΘ)lOE8V–ΡP|Ψ W[ψ””XΞ…8ΓυBy z“kCRΆhl2«αw”" -ϋ;Κ–β›³]τ ά>“°@¦΄–ΆWμaΊ] –.ΕCΊ Η/ύρ‰vi6AP"zΔΐΕ‡ΈsŠΌvI“’ƒ΅ŽsC”C΅Ύ³ ΣΧS€]ζƒ<¦FαΓnυιˆΣΣ»ꞣ[Ϋ;‚q½ΕΠ°Π}/4τΜ’Δ J‘μψΜ€>όΐaχΫτqeΕ X™ήYΧuBYάΦse‰Sτˆέv£ς(π¦%Τ«AšY—39΅Iq‹ΛΖplGηŸ„ΪOεΑ±υ‘η4Ή»­&Ξg~νΒMΣ3Ρ©Κ~@–“i°r/ΜKΉwδC)ρ(ΗmςΎΒΗ‘¦&9η[}Ώ•£ΒQΫΛŠΏΠό°ƒ’­H€HΜ5Μ'§9 ¦Θe}’ύΐΎdtβ±v•ލ9&ίj‰¬k‘ˆΰ²ΪFVLΟm“ρρ%;Nπ3υΑqd…Ξ:šΈDYZ$σ߈ΰ©JΪ‹Ylw¬Δœ-ΑΉ¦ΎZ°+FcgΆ^¬±θυα ³{v‹‡+…Π”Lq²Ž Ώzα Α[ εδ{Ε,OYrt4ΤVVΨAΪy‡ƒ@‰8œ'"}"γd +ιΚ¬σRˆ3Ό‘―©aΫηm ώ{Z ±=q“KTϋ#„Ys―τάa§ˆΑάVΨeZ?;Ϊ€°―Ώxμα`Ωΰσk5πdTcƒς ƒ§β{έ­/βΘΏ‘Z'΅ό·š…Ω@[’8ƒ:Έ€ψRχγ|ƒ„ίl†KδOSζlΦ»?:bεIαθΊέ.πΕ-ΤUΉƒM]7ˆΉγp}ς©ΰ‘&—ΥΑ;ŸΌ^ΗKύ,«T󜲍ΤURμγ$―Ux0Φ]U4+¦³[ιΡƒ—²gΊPzΫNpF‚!)c<΄PΎϊ―R―ι εjMΛ~ƒΗ+…x6₯¦*BζΛƒίσ.+§αΑ•lͺ^Ι%εt5¬ΦΈ5γ9θN(ΉD»]όtΓ>‡·&Δ” ΌM}sSd=κ;’^;$p1ΈŒΠY—5#x+₯zf ž”θPV–»ϊ%vνSυΖώΏ€³ηQιumζœύsΚ¬7²_ίEμ?ιj_zΰrŒώ+μ΄ζ«όρg’`JΔή5J(@ ƒ0ͺ{―wςGkF8EόΞ,Υw’ƒδ`Š’μšž]₯l<›Ζ#>ξ€scSΊbP|m‰Ω²L«κoΒΞΟ…@€9μJΣέΑб[/¦ΎΝ.«|ΜbΠ Ԉ½ŒΈαC`ΩέΉ„Yb}­Ρ†Θ+Ί˜MF·4š£z¨²°―~Ώ{*Ό»^όΛ^!uΚm…€_νn‹ψΠΊ+Εκύ»έτkD0μAZ1σmLu=ΪC—0ϋ!΄#εP&α"΅’₯Ϊό© ‡mYiυW`~—-G7pnΚ1?M0V§bAb ~{K΅χ{ž¦oΖΛ‚δπpόo1¦ώόT™ΘEV‚2ε’©u`-΅š 7Γ½)ίdMλš]/#‘ƒˆJ!KG·šΎ΄\r)J ” q’ γŸLؐόΣ²H-μ_dαˆ‡Ν˜yPll¬•\„—Nˆ»ηVe`feR)Ωc_θ:™z{¨ΣΧFˆ φξTΫ °°`m>P±ΐ1Craί±K<7ί{ΝξΒ;#xjΙR`/¬VTpΑ‚(ȟΜH‘‘„vX•ψ• ΰΡΠΞ+ƒΫ1Y›ξζ δƒΠ)q^²©†E•δy|b"¨b1)€ΰΖν’Oκ^ςήρl4σ -γ>βλΘ;¦3½.ΘΑΆ―νΞ3{X&€C ψΰΏ‘ω½˜χ8Κ—$ΦΈB5ε;ε*LCΦ%WgE£4*P}jvTΊΦάŠΦ›Jί1k™/Aα-΅*ΔΤγ.Ή)=ed6ΑqŸΓ"‡&ƒ¦Eeψ΄”ι2­•4 ΏώΪθρχݟ|‚Ν*#tΦbμ€”Λ€—Τf›šζΠ‡˜ϋΟΖ€—ύΑΟέ/Gc£š`’ραI†  ~₯ΰΠ­Tcπrƒδ½Ÿhώ.v{o@ƒγΗ3υΛ8¦ Ϋμ=  (u=κg¬2ΉQΙΰΊΊ—Ι«Ψ€ηΧr—ή2‘TU'ΫΒ¦ τθ°bΌ­ηC}™τΘšœ―6|_ƒŒ\l9Α«5fL‰Q0‰fυ‹—TΩΣΜ„_²φξ8¦Ϊό¦\[πš^‰Ψ‘hNšβ¬mai +‡K³IθΉμ%w»pΊ’SΐOατŒζΎK7zΎΨ^?θEA s)uԈήEΗeΣκcεξN“Ίv)εΦΎKώož}ΉάΈΐNυ½ΘΣƒό›R’P9μ׍:&΅ώƒδi°g+Τc£ςηΚ=11²§<'€…kƒh\pWbμlΜv2=ό}LOΧpzzKŒω ©ν£(ρhυŸxυΔ‹Έ›ΏAU~V:Jπ„ΑTΈCήή―v•–F~Π”qGV,β:Έ©P.³ZaSJεώ$˜τœ¬Y0T[cͺžγ΅³ω΄?θ¦ΡG|υE—όs/Γδ<―θp …‰³%™œS?²m3ΎξˆƒCŠžf”"' + +‡‘ΙAš3ΘΊ‚6ΐO&{v¦ΡWκ‘gίz -αKιTW!f N•pΥΐΓXΈj€uwζAOω…εgν*Φέ]ϊA9ΛΨpΙΣe¦²ΖyβR`Nέο” DΘΌν|4όύσ#§‰šΓ=‡ZΘΥΒt0πκ7ψKqpώΜZωͺw‚ "ϊ4.HηeŒc«5tΰEα`G₯λF­ˆ’θψΒψžζ•K;Λ‡—αΊ-y‰[2υΨ’fζ;αγ‹ž:€+o1άŠ―ϋώK£(:Θ­Ζp衝@Όϋϋh“!πoΗ%΄DΓ₯vξΪ3Ό”LηN5d>zϋ€C4hΦ•γοΗήcΔhχ;—,Ÿm—iŒA΄ˆ0Ϋm‹ΒFiv˜ί.΅οS¬ΒS™x‹ΡωG͈<ΙΖH―™mRΖσDηγG˜ β§ΊΎHΞK/jm΅ ύέ`•λΛ€xUƒ«ξπT*ρΨ―~ Υ Dπv{`υθj)Τz$hqQ8Υ2S ΈΓ³πϊ[ΈPA‘ΖΘ_Σι*εμξ„:±P+6ρYXuκ&(σJβλZμX,¦qνSŽ bΏ§»ς)$‘άΥrΩ>nj\TςΎΌaν(ξ|^Λ +!xςόU΄­ΐ2fυλζ€Όξ.ΈμEεσ&£ώ»wŸγηϋ ”KŸ€žΧ1^%ΝμDT)“Dς˜M'Ζ_Pσߌχ³{›ύ“Ώφͺ•ˆξfX€@ύ=`γω€Axƒ&ΏΧ6ϊB½O­ΪΡ- eΈρ…u₯±ιηρRm½Y-λ +χΞΚ=£Y cΠΙ―”}e¨΄UηΧ0޽Sm=φ‘Υ{<¦š«%m‹βqΦ_<D͘Έ΅ζTλ—`Ok )3­tX·I°ΑvmO‰γp;·qlρCVAbΓΒTΜQΆώRγXΓ<Ω#!z6oĘ;μ¬Η@”=ZΨA#‡Έ„$sΜ ώtξ>…_ςivˆθqα< +A …ΐ"ΖΨ%Ν°^γόΙ<Φκ…«%WgE£:}°ΩύόCZοά< νά³'{φνήΉ”||ͺ˜Λ’βνMζ—ŒXΒΥ―έΚ`%Rβ s»…Ψ5nYc¬h,sπ  ‡#6¬Q=0X#Ώ4DH€%ιψ3 ΆcRΛKOλVμ NmύΠ kί+α\ΎI=ͺޏQzwν1¦+2ΨbBͺ½{η +’П‡ndπŽšϊ±FΘJφΪπ8™ψ€:wΟtίIύ(B5Ά})μΠ¨AuˆZun$5IwΥπΥv’ZΥπPsΒ(0 BΩΐτ7Η~΅dλΚ=·@’ŽKk9Ω*ΚEšΞnY»2Ωb`ΐ³ΞΪθυ œ‹¦ͺbΤΥ@½ͺΠ€[•%c.οΑWΩ€ ˜"Iε+—έœ9ωzΰ[pbφυιF¦Q±“Ž"ƒŠLΕΤyδ ¦oΐ6νj£’&$ž«qΉ 9ά¦Kο«ΏκS0y£Yib&ΘΊkΏ9ΙΓ/Υ·F žpŒ@θ)” χ e…9aVA˜k²&ηίέJ'΄ ]7\bJR­ηΥΈi&3β&Ζ₯‰ +ωoΝί%³«ρυ¦ε8ίŒΠIΓMb³αKα;‰φ’Ρ+[‘W₯!$,Q>¦iRCŸΙ@ˆψ.W1ΰυ₯m‡\-ͺ0*@«έ‘ύΉΪ³Rο«Mώ1Eι.·,…(œœ ¬UΡ1θLe ΞS›¬z —m¨Β'Ɓ‰9ΞC¦‘ΩΌΥ=‘7‹Ση„Ε•œί'ώ,ΗίΊFSœˆΧ{OqΦm^žqΥξ4·Έž\‚Š‘WΗ}’%ƒ,™ΌΕ#S’lPc»‰@€φ`Μ)›–Ό§<±ιξ΄‘ρ&ή4§ΐZώΜ‡O|Ζ™:α 3Γ›?„‘yζJ§Pž!ν·H„ΎDf}½IΝZΤ³p·―usΗγ·ιmθ2Cώe5bUύ"΄nΥςΖωΛ•E€Ϊ+NΑ|rδ; σxQκΏi“Κ5λEŒ­|½ϊXͺFRΰ“A„…\νι€π&Υ)©ΆW@qησ+?Ψ6l蛝 4Djΰu/(OƒΣ₯ΛJͺ|©πϊo[₯{(Π1―“ͺT]ΰ‘ο)ΓΪόΫδΌ9iLͺmΊCg <ώ Φρ:K½§’v”Eόρq,ο€qβt@J¨’ΤΓΕbœι`‹h c8Ρ•χι„αZ–ΆΆ-5Ϊ7Λ/ΐ?°z#2ͺ$Oγk€H§Ϋξ­ SƒΨ–JGΞp”„C2¬~,€Ϋ\· sH£QlΎƒž]G’°o€Ό·±O…φH4)υ­S½·‡—τD‹U<)[5'ΙW՟†*[μι‡νν4A)π£άdPοςyΖOξζ€ΐ‰v9•ˆ‘0XRΚ’JέbV§"βkόηB§•Ε΅X5 ('RβaΝBJ Umν“N΅#9 Z6μ―dPR2ΡΕψ+ +XUΠEr˜ν­tŠ•΄H™ιΊ5ΰˆΠ4;φήΛ9έΖ(~/»|σ‘·€vΤΛ β9»œ­6e¨{·‹ΧΨ[μχς&ΰZΌέa$tκY­ +ς"OΚaΛ·v‰†nφΒFfώέΗ©–ΞΧM~Ψ]π§ΩεQn ιƒ]υΪ썐«gύΈ&Ω>€>ΨΡ=V|-2VΎΫLΡϊ«x xZYΖΖΞgN[M{½'’#l·dέϊ1-₯υh~κ―ΛΊ‰ͺ3LtM‰3wۧW6¦ΔMο\;(|·Nr‡₯aΓ0Ξ ‡ΔξYΏΪΎΘΰϋ³αΈΜ5ζ6ύ2gaKπm‘KΧύZΏ>Έ ΓmD1Χ₯&‡*”¦™rΠTšγ έ₯g?'χFmž νΐͺ™m€£— ?d2*zΊΕΟβ³GΕ—ΖruMΒ›l΄λ]Žβ1kP_f―Γ& κΠΝQά`'w£’άνξγΨϋ!ΞÏ-X·¦ΟΫWξΠ*Λ—M5#•Υ›χδYΪW‡›—Ϋl?>έ²ιšϊ2ΥPeŸΈ5,£δτ|Ε$ω‘Œ± ŸέD·€KŽL–©ΉY'oοjΘtͺa―n[nvͺόˆSωj>φv7x+tI|φsΨΉ‘ΰn?οΨ%'±λx₯|»98^NΖ#Κ½}UςM»ͺπzHQδό ύΟ›ΐrρ†¦»9’Ήκ΄ΆΗΰoωί§tŽ|6ž1Χ―F\ζŒ{LOΌΜϋG½K›ΙςgΑͺγ[°4Α|Γ…w|©΄[H$‡ιΪ@oŽ2dΞά«ΐKτ˜vo­K³σ!―ϋ˜wΟPR}Αqψ ΕRθθ>}{”]9™Ο0ς„q;ΐ‘ ­ W\wMγ:™Ο/β㐏‰~wƒFplc³έŒ«πσu²°3^TΑ.°Cu‡ƒT€u)ΐ·ύ™”ϊv˜δ«‹ώιš<ΐ“Ϊ­+0θΦΗΑΊk»…)§waύξ J8σΰi²#ώ“WrύΌ‚Ά’βˆΞlΌ-φΡέΘͺ₯Fτ€f 2{d3@ηΛά͐α―SΧ…Β(•€_πA~λuPέ<¦ήVΤϊ§aυ³ήΐ\{’x„ϋώ¬T#ΐžΤ?:μ‹έοk^››wΕ₯3ηzβ>ƒ9_Ξ-Q‘Κπi±Pγκ ž,ŒΖ¦·”«tπU‰':­— 0ρ˜B‚‹[h|!VŠ ‹mk›·μ .αΣf[;γ Br&ύ‡rw;{<Π“ΧSήζ«Χšζ·’›”N› m½N–E₯Šςζ{ΙT.Ρ“‚–Wδ­θKιf–Μυεe<^7W GہΠQ;[Ηu†!b›Πιs2ΘA±IbΗ!¨"”kψψ#–ŸoZ,ΨFπ"c‡$‰9Ίͺ(“n0#ί ΥlN™ψAξvιΙ‡θQύO+Hoι)•2eΦο|Q¦+IΆΕ£B±­"›₯;χ³z Υ&·l«lh‚φ™ΰΤΚ=<†™.‰“³™: Λ:ΙΗ„xIƒζϋΒ8Χo€0μžQGo˜£Ώ”±‘uBΤ„JαωΣφ¦4$v*“zt +ίn˜‡-Νφ«F’ψ {5κ@χ0_ͺXœΧ?γ!ΰώεκFm΅ύαπ +€Ζd4ξ²FΒFΕΙγϋξہiC%ί Apžš"£ dισ"Ι‚pδΕx抑]” e†οš£s<-€YΗƒpύ~J퐖!Κ[ο†6C/τLTsP‡bΏ’lσc\Wβ™νκmζaΡ»s܊νd?ζ!λUΝ „αIŒπ”Ο9ϋr§UΌ>IΊΔΉ³"Ι‹ύκ»ͺty€σ6„ +ΰΙΫ± ,XΔIΛ”„~'³ΙπΛ£vž3ΣiήšU—ΩΊK΅PpλV·θ_ωγzΚ«zMWΝμ( +ώ+Γδ‰h ζŒΡDήJN +;κρ2Pۍ»Βφ»{κΈΡΓΆΧ‹ΤG¬Υ*L•Ψ +q  b,ͺΑ«©@zς ˆCtR΄`Wΐžœ“*ο·U―§Π0?ϋΫγτ!wΫ«5 ލ5Mτ²;j‹hς«ΞnθηrciΘλk` Οθ―•σβ^§ι[L¦fβΰ6v“Ϊ7[‘ƒD9zΜSMaψ`’ΪΡmU1α·ΕΒfpvœΏΊJ;žXsκΦgΪ!6ΞΆ£°ͺŠ8€[΄ΩΆ@ΠΪ`@ΤΎžΗ―«ΧΝτ\<βμΈR[Φ’†Ω0ρz/' [0Z"Šˆκ>ΰ&LqΆυσβSCΦjΫΒΎΧs’ͺš`ΆΎ“Ή9Π'{±³υ2Ί<ΆjͺOοWΑΥ±ζ Lœ Ϊuy&³ν†Κν—€vΌωίFLuΈ9πŸ¨0ά―7ͺ£œδˆ =Έ1·‘vvΖqϊΧ?ΓΚΚjΣ¦ρΚΞ'hωάς?ί^™Y©gH1—Εu˜D'ρ±CοωΒΎΏXΆϊcΕ#ρsΰ<‘²1²ύa›S[6Σ›Dj¬Ρώ@]=₯―Ϋd¨ζ`(oΘΛ·•λr@&ψvυόΪνώσklΒT“ŒMφςδΪWζ—ελΛ©`Α3„₯=s₯ΛΪ―T΄¦.5…UIɎPμ7vί’ŽΎ‰γ‘G:ΤKθ*Ι¨1œΔέΑ“)Oδ…Ν|6­NΣ=©εE°†T&±δςΉψ.%*$>lJTr˜π]ώˆ&ΙŽqΡΐm © JG^Ό3m—t°R˜ibφ\Μ­{6dežHΨqχά8@‹VŒ^9θnΨG  ©(O mv‹#«ΚΰΦΨ`γ…‘2ƒqo( ύΖ[2ςŸΏ½Λ)ˆρLj88k$'λY ֐GλšΧͺ“ΧMX°έ-†’ε<άΐždΒγ™Fύ n½Κ+ +o6‚ΗzΌ±”¦ŠΐSAͺτλ(CQ +χρ¦Ο)Ό’YjΉ4Γ6ΞΊͺwΆ¨MΓw²(%~D"}ƒ†κ·HθpN5pfΞAΈ9œŸvΜ™%e'@‡eΏΗΔcfΗΠ‘Š™9±Y‹‹€Θx«j£rΤ~˜–ƒb5Ψΰδψ/#U.ρ;²ΥΔ +XOkΓ½]ΙLΨΛ±” ‰Hψ ²ΤQfJ]F],ŽnΟ›ηP6U QΎgx:­Χ½²šrΆ©ΪhνIοĞYo₯d—!β1-@` +endstream +endobj +850 0 obj +<< /Filter /FlateDecode /Length1 1368 /Length2 30875 /Length3 0 /Length 31834 >> +stream +xڌφt₯]³5 ΗΆΫΆmΫΩ±mtΨq‡ΫΆΣ±:ΆΝŽνδλϋyžsΞ{ήγϋΖcο΅ͺfΥͺZ5ηum +e5sGS €£ƒ #3/@LA•…ΐΜΜΖΘΜΜ +GA‘nνfόŽBθβjνθΐϋΔ\€&nmβ&nq +ŽYw; €…“—…‹—™ΐΚΜΜσ_@G^€Έ‰‡΅9@ λθt…£stςv±Ά΄rϋ{Μ-Τf4.ϊ…Dμ.Φf&7+ ύίΝLμjŽfΦ@7ο•‚šίΚΝΝ‰—‰ΙΣΣ“ΡΔή•ΡΡΕR†ΰiνfPΊ]<€ζ€(šΨέ#@έΚΪυίv5G 7O ΰ―ΑΞΪ θΰϊ7ΒέΑθψ{8@MF δtψ7XώίzΐξΐΒΘςίιώύO"k‡›˜™9Ϊ;™8x[;X,¬ν€%IyF7/7z€‰ƒω?@;WΗΏρ&&Φv&¦ͺά )’0ωΫΰΪs5s±vrset΅Άϋ§E¦ό½e s1G{{ ƒ›+ά?υ‰[»Νώ^»7ΣΏ'kλΰθιΰϋŸ…΅ƒΉΕ?M˜»;1i8X;»eΔωk‚ϋ›%Π ΐΑΜΜΜΕΓ:€^fVL€WχvώΛΙςωoώΎNŽN‹ΏMύ­-€ΰ|]M<€7w Ώοιψί;8€Ή΅™ΐhiνχ?ٚήΎ‹΅@ω/χXΜ|ώ{eπ—^ζŽvήΧ|™t€5•₯θώέρϋDE½Ύ μΜVfΛ?$γϊ»πίi”M¬SΖ+γ`αΰωw΅―ιΏ*φψ¨#ΐΞ₯θψ—΅@υ\Ÿ™ƒΩμοΛgͺ+δΓΙςFς» Iw;»Ή©εq›Ψ[Ϋyπ—΄ξn ΰψW7T ψoΡ*Ν­ένo―Œ›Ι_!ˆ8XΪύχ5Z»JZ{Ν•­έΜ¬ώΝ–Ϋ5ώQ™΅PΩΡΥϊŸΗ +€αοhώ/ί_i™Ωώ}tΈώ₯δΏ\ΐΏΚωίGJ8˜9š#1VN€‰‹‹‰7άί!έq|YώjΡθυ/˜έώ†ώΆη°ptϋg’ΜΛχΏr›Ή»Έό•ΧΏ¦χΰΪKΛ@ Π nuΙь/Τ¦!΄λΉNί“αpJ`žβPλ' ƒοͺK·ϋ+t +MmΦ·m—G‘”±~”} κα5βί³φ&舎$•Ξ7Ώw£ͺ³‡p+3XΓΣEg"C„° κΒG~Ξ~šΑΆΰν Ώd)ςœέΉ‘” П=₯Ό‡*Φ'Β—UŽj9εΰί+ζb5bτƒK(ςM³qH‘άahΡ½ηΡr§ΏˆeΠΑωŸΗ²ϋκξ°Ζ½,ϊlV©³Ίφβ’γκβ‚? MΜRϊŠž€Κb/ϋ–o ―x΅ση"§m0 0ž°fΦZ«F;΄Τ{L¬φ°δ₯B©π‡Ρ’κΛ[1Œ\H•0k;’ΡέκΩl„'=’›]š}6ikί·&ηΝ±ρa†§π΄Žη±Φρύ>­R‘!!KOZ +½¨uήLΒ4Θ +·I°^-   +ύšG +χ;€IΏο·ΡW<–½"ff]nΘΠ ‚KŽ<Ώ›8κζkޝΤe;O4ƒ―NΕ/Σ_iΧΧEΈVlΖYαΤω™}ίσ·β©Tψ€Ν !LΧ;ΥXŒβεήΔς‰{+7#δΕμ2R‡Λ5cQϊίΌm^x +/5«5:8 +UHΞ£&OšƒGECΒyΝOφ++E 1O5_«=#EΗβvΥΉBσΏχ¨εφΊ*₯K‰Dƒ•<ν/ΐš’]¨‹zΨ·Ζ#k© ΌξOφ–Ό{WȈSP―* ɝςGψ­¬ˆΰΦ c}h bmθVz¦AΘBٌ£Θ:Z'3κkΥΌωΔΕWS. σΩXž›ά'ω=»OξkW­_Κ>ΨΰγˆI·ϋAI²Ό”q·-@υiΥΆ₯α)΄FPŸΉ0 ά>€%ƒξfcΖœ:—•SemΙΣa|-%εΨμωd ΏΗ· ΤΈκ7la<π‡U +θχκ€’o‹ Ejό9zηLɝσ’ΚWq©ΔψΒ=°₯XJΞ!Yž…“gΑΧΛΙ@½ί/€!‰±Σώcv‚ ²ΟΏH…1ΪŸz£ŠΜ&‘OΚΐ[J…Ίσ9#‚$‹\ΛΉ:Ψ &;"λVB9ΰΛ•ΐYG¬930Σ{ΆΈ$½ˆŸtΝT>Ž$ΉlιI*Ι‘οy£IξWϋμ¨Β?u<•?kΠŸMΔΒ\ξ“ώΕίyjΥΤϊ²’΅5fu½•Ί]}Σά\W—)4†k,tΗ•z†o4‘©—sΗν₯ΜΟΞ²Hx'ΰ6ύ, ²†ŒΘΗ™ήήζο"ηΞrΚφ–‘!iΥ$νVέ0;οθC΄τ`FD‘Ών|€ίΡqΔΔξA’—iτ$ΣQPwΥXϋŽ9|dΗΌMXy« ϋ’«Γ\5οαΛΉ“Ό€<sΙuΛ eΓxOΞVŸΣ‘cܐΓω3FΣG-5.ηk)¦+*•Σ GDθ½N%G>ΐͺΔ<―™VΓ)£LNnχvϊZΥƐ„½£Χ‹sΆœ‹~ΑƒΑ•›X¦d₯°L6­υ‰x’ΐM§ww P¨bΔρΩrOLΟ‹Xΰ€Νθ€-AκG¬œύβω5UΆ-4X(—ŒeFƒσ{ϊiWΪ2Χ­šR¨|?nzπώΝJύΜ³;騋<Ο\Q_¬-³hjMιqiZΈX‹Υ‹ψ„Ϋ™Dή|fίρ,ΰ€E“)fTT@Έώώυ‚„9±O'ίΡη…όβeζi$ˆb°ϊΧμ¦ηξΦ.«cΕΐ6 ΖθCHΖκ7²ϋIΎ¨ύ₯$6o3 ΜJͺ9Lω¨7ηΣ·ή΅-‘$bY(ŽΌ‡Ϊ­'e9ΓΩ|]mŽ”ΟsρQͺΔꉏ.™w+ΕΧ¦%ί%4Opy³E{LHλζ’^ΚQR…σ?φƒΒ­Z‹αΥ8EΥφ¦Μ«?!i_3Ξ σ“Ά“˜,²νΊ)”%Xσy‰4EhΤDε™Žψ+°.9Π+ΔrD +·Ÿ²νψj|χΒS“u¬hC:HB%’ΒDƒό εφU]qLDνTΗ¦>0Ί3ω ”σsT€7B_šOΣBέώ$ψэθ} pςΕT™WoΨύdβ–C #¬Δr–Ο” "yCœάΒζMD–Ύo†¬ °<#5€ϋ±©n_υΥα3#‹fWρRIΪ/+X„XэΧ%π?Φ€I€ΨP]·Ζ₯ΟΏ +ή*½REΓjΓ*ξξ”Œ―θVΞf<ι–‡˜ ςz†(Α1›ΦŒDΕΉ»WΙ«ETWώzπΙ{ŽΣ':}α…ΈZ%½Ά%Ήcέ$„ΖΣΩωβΑ<Už–Œ„-°9Ωπ€·•McM³pŽ΅Ϋy^ΰ£Μ?]Ώ‘.β’i±HzM_.œ€m,?νΔ6{iz?4―‘Ξf“ωΰ€•:yάφ!f†H\*ctεωNŽιΠ¦#V-Ζς:θVa.LφTΖ 2c«±-Wš7ΟWΨϋg!Ι.Xς• +~%Fρ.ͺ”og·Ψό³ͺν4±ε}›Ά+ŠΈ$?e=½ά΅·„‡ΏY*QLΒ8Lσ΄ΐj§{lS%iJ²ΨQΉ{po5$Ό"‘›t–­λI™g™η\7Φ•'”ΚxΏšράeMλ…W1Ήψ|Τuώa$Ή•XΚ"!ːPk3Œ’•έ­άΞώˆ|Uλ +½Αs3lψ\‰Ητ«πT©$}|Rk€VΝ`“©ΔΨ»"F₯6r,Y_%₯_’ΆbΓZλ&φΔPπhnT¨`RF‘ή¨Ψ!Η_ C§šΎtηRœω>ͺσ0„~‘γ τοͺ_!@(Ž~ZΡ‰P~½‡›’Ր(8ΏϋŒΉž— ι]D–Ž‘jhΥUžB–Yέq£(<ψJΝκΥ+£α€μyΟ³šrΘF¨Η=Υ‘ZX7ιoQΈ‘v \ΨNgr0rH—•ΟΗ‚‡‘N†k8Ξϋ=“Wέ¬Hθ+'ΩΌ½~mh›iΊΥοπ'λΒXΎΆΠfέ€H#΄}Θχ=ΝcοJ~{£γ¨€=ιžε'y Ί‹½ΘΧ₯k +Fπέ­ƒξ68mz§@ωΣΈλΓ΅c&†©°οο%x11βh_l3F­@έ–σ+’#flψS§0KWεŸζδ ³ϊ΅S ™”ςόNΏ ΠڐνΠόΪ2›ΈO^ΗS[θ,¦F|ΌΛ‰–qςε™8₯AΠhΕΑ’Šv;9”τvι1ς„›LQulΰθ"γͺΑπŠf₯K*ŠΡύΉΓFp? bϋΎ›ˆ"$ƒ<’Cjtš€Ρ–‘QΨΊ%Fbs@Φp<_¬1QέΜE™œhΔΦ5'p°₯oƒΈφGœ`ς‰έ/§αx•σ4½α-Ω/ΝΕ·L±|ΠjΈT†νΌn²υ6Β0Ό*C$¨™₯Ζω¦:0ΧUœΑβΚG’mΥ©Ϊ―sPψrψœρVάƒέμδΕX’΄“xipΞ?–ηŠƒ…’g|XΔΤθz +•Yˆ€c`j¬“πl•²© +kγ‘)vΤ7σΝδ0Dΐ1•C&{'E9.P­`”ΝΐϊΚ–I”tjυŽnϋ|-»{¬†―‘Αγϋ2( ΐ,ΙοT”σ? /$  20 :‡eσΉΈr'fΕ΅mηDo™’«2ψAjP,O²»ωT+€ =•py« +†Γoj.Ÿή&e©ςΕ>"ˆXˆςYl¬θ0aŽ ΔΝνYj<Έη¬‚πΩ!­cβ6(ϋ˜.˜ίΌ6RαŽϋΧf–jkσ§mΣjΦμω(SkΙψΧ2κn 4³’>™Ζ­#­Bϋ2f Βj”©ό_]‰—@nτΰlΙoϊϋšυlCw&Υ"$ήή]ϋΌ7σt9f$…^א©ίL§>—Ύο2Yψ³m§fžύ.Ω'•*OE­>¦²Pp…^ ΦΦψπlnmΣlŽ5„ZX¦°™!‘ξΉŠV’ώ‚›Œ#6zi‰ζ;‰h˜Ί ό±'Ϋ摏Τl°AGΎ +]ΌN%f%Μ‘Ξͺ†σAΨyρ»οF8©'6Γ€ͺ)ˆ#]$Π,*όΤΞQ ΄Iϊ6Κ€΅”lέxyΖAΗΧfݘΝΟΜƒ°ΖΈƒJΖ.ο#p§žδ°ι=a^Ήόά7ΉJ’φη•ΐ”“Ι‰!|πŠΥ™’-ΈqvλeζΎΣσ+B5·»χΉΛmκζ]Š;œ…Ώ™σGŸwΙΗ¦Αbφ+ώsο9’}5h[Ν‘5―τm^RΨΰ:uP)ψ”PR³ηΣy +K°ΚύdgAŠ7rχfΠ'$',SZWŸ"^γΒc₯„qΖgίgη·h¬7E’ϊ^[ ›NφَP—=†lβ3Γ<[θ‘υώ²΄πFB°ΕtΘ‡m›*γYζΓ‡UЈ=#Υ€Ϊ~ΏxηΔͺχ;1WV‚ΞΓ’Xcυύi³aK­”;‡‘T&τsσ*KΙ²SΜόηNμΔή5άϋχz–j‡H£7qA'(Ί#‹ +K+rΒΧ²!Pα+…γΓΉv)ΉG>ŽJ¨"Ο‘³Nˆη+‘26 ‘oΫ:¨ΦξUAύ›·έ³Wηsh:.(§„⏼ρ±kζDΠͺΕΰ›€$7T„3†Šz š½“Š’±«½i½…Rχ/΅cΉDάPΤAuΉrmςOΛ±€₯½=0―h|瑐BoκΤ―.ω +™^³f?+’^ς>β{θΣύ ž:±tέUΊΌSWΗƒΣECY,Ώn›νW½ψ# ΖξΟΒ`x~UηĘK‘'MΓ†kœψ—L9ή³oΖ&Χωh%£ˆQz΅SQ(Ζs=9…ύžx}η&Dψω>GΦ·ς&‘Ψhώ6γuΗcΒo…Q‡ώC^ΘGWZ-Πι‡q?™h‘|‘jν„tΛ}ξ’SΖΡ’ێΉ/ˆδg)G ίΐ―δŒΖMeύΖ©—›ΏΖ\+„:<~wκΏ§MΔ\ϊPί-S4΅£[οκΩ +Νq$…]-Υ€νς™½³όc9ir—ΰή$™7›Ξ?€ύκ½ΩVQ5φJΫ6jωψΕ.€ΝjПΐ^&1YΉ|›aΨ7γ,tlΘϊ©qq€πTœ˜8Γ%xN3\t—.퐐ΦQV-ΒEzκ+ΚΟΔκφ‘04rβzXτfΛ#£}ŸhQXž5 Ύχ5ύρF‡ΎZ’-q^šŠ2Κψg—€wf€”Vv] ©wM‡¨†ΰςΚf‘rnUδDς(ςW{' ΪšΐCͺbώ!9§GƒžΊκw²uœω΄/φ©Λc‘LiͺΗ󧁗@IlsM)ω%zΊc,WΜ*©Ο’C¦Άrη¦ˆ±GP«˜!δͺ—]’*ŸPρr7aΆ' ZFfξ‰Βа  Ώ–•ρBW€ΰΧrΑΨWΐ.pηBS\vΡ/­£CΩΆTΜ%j{X}Tdτήημgab²²?dι|—άγ<ΩΤυΌV$Ι§WύcΌ'Γ…$pBΑλΐΗͺB›}ϊ―ΩΊζταZVά.>?uv9Œγ±%Œ2½ϊbCΐ5rό’©΄›G"B₯„‘5–P8eΰυ‰ΣC7ίyΗ4ήΐΖKxhe˜ύ±ζ›υI}W0aΨ4Φ|†oΖά$³,ΐ'V‹Β”·ΛΖi$"Ή‘Ώ½kg½zΤ\[’GύΖt^ΰέ1·!+n―έΐŒ΄‚v/ΞκruDdhN„“—βŠί'­φS™‰;ͺx=θήϊΈϊ…}σ/”_lRC*ζ ²¨X΄ο*νa»Š_ΊVƒgΐύ±€ΧhήΊ’τ.›₯ΏΡˆETMp‹evΎ†τώ‚μυΫ­Η†ev8SδΨε3 +³¦wΆζwBΦ£Ÿ^ŽΨφΆ)l:Es+!! ξη{¦Jh“ήΖΏFŸ6/Δ·΅u!3ΊGŠzΨ ΕΞ Q5žπ琾FΠ؊₯Α¬FΛυΣΝCΣr­!¨uκZr@Œ'TΥσ fTL~˜!†"‰Kυ€ŽO1q8%ΆKŸˆΉŒΎ1Sd°?ƒ ”ΣG·έ-ςΔGT–;νd°ξς¦V±Ϊ‘rτdH…UΘ ΦΰΖ+vγΜά„Δλʌ΄2Wv-х΁ΦCƒδš$Ύvύ―SXενΤ\>›ΤUαWΒλ_ˆ:Ι1Β…mέK²n™ΈL­΄ΣΣWӞ4 Ι)΅¬§jo‘ζ¨qwΕG<)Ϋhπj7αB1υLϋΆL]Ι’Τ{P[ΛβρASω{Ύo(’e–<ŽPΡyξ;^4Κδeπγ„³'{zιΰCͺζΦw½‹"«”EŽά I§pϊmηΩeή\:ήν”ΪΰΈϋE‘„ωΔup|ήΜ[_` Ε(žΦχ’­©λ˜ϊӏamΘΣ{>S ψQˆ=€rqβ$zηκ.fJ·‹»UΔ~ €#θ}δΪβF}Ί-_tΚRίw;¬k~\|Ώ'ό“*±–Ί§τ“t^±›Ž?αjΣ¦πΙΔδQ4ΪΘꭑԏˆΦŸΟ1Q—ŠΞƒj1ΆE\7}+-γ{{ΆZ'^Λ²’ΘLGΞ1ΏέhχfTΧ6ΰjAΕRΔ‹J Jόΐ˜†0οI1K­ς[+g\%I`ϊ3&Λς1λJΤ‰x ƒ‚φξ.\ͺτΌΐ\“Lb£›©λ•¨τ0LPθž—ό‰e«žΜ}Έ„΄N`r ξzύ`μ‡H€όVԜδ–ίΈ³•4`šϋs‚@ΗΞ8N=aΡ°š>χΙΞ#¦MBί|Τl1ލ +žjΎp•ΞΩΑβ”αΖD4ˆ{γ3ΓΡN’ΰB§!E~xP†‘sGŒ0ΰϊλg>]ϋoν£fi9V1>τΡ–G|κ»ΙΠ eWš2ήϋXDΛA•£$› ”’ ΅Uhξκθ6xŠŠΗC΅ +ͺn£Δκu|ouΙ]ν‚γί6m’σ–T΅Ό6O%]εΡG qΌgοKˆV~9ƒ˜‚ύF#zŸB€>4ΪI2V†'qΣ¬½ΖyDήs~&g]΄•ω>λόO”o7–S8ΓΧKΩφŒ=‰ςΚείΑ¦Σ- E !sd|tζu’― έμUέO›jΨvψΛ jd9†<ωΠqζ>΅­Ÿ•†ηΨΐΈχϋΖ)μ>(ω6“ž›Fνlζ4YαΖzHokSnn“ψ*…_ή%Ύ§$PH­8ŸrΑπ ‘8'Ο™σ”ΣΖDΑΙςZι^Ό³y–iΛ`ψ…Μ„β\«Fo.;ηγβ8%mΈ2…#]Ύ²Β€g―š’[Rη Γ‚© <žΓ/1.•Ή‘wͺ# τST‚QDqΒ1\‘κΊ-’ŸQΤt˜ζR%4sCG xn,όŠπd*Νς¨+,ir…‚f‡ά"rφ“NάU¬«rHΦxzτV‘€σόβ‚œM§¨Ηrαέβφ¨’Z~#΄L`xΑWΏΠˆβƒ α{˜žKg+^”N#šΑ—ͺ­ΞϋS/o!Τίxfρ²ΥϊbΨ›ΟG†π€‰0Gζ5'ι1Ϋ`£rΟ\C~ +I2Γαk)¬2 kA±œΊnVB*Zοu©m³Βtο–ΙŒ·Β…™sύAΡwΏpφ$ƒ1αvLιo\“π§ _|)ήˆΥ”\οSηA‘γΣRdε`φx¬L―/ ‚ΉΰΗ?y„{ ΫE·|ΒOύIe4‚7v°vHRMKή|^Œ;ί:’я™ŸΏο•ό +Yψϊβlά\ΉοΆ “Γz4 }W,@(ρr 8„Ÿ@GX[…†–Ϋ―TΧ…ξE—]sM‹KΕϊτμIπνIœΨ.‹•HOGAZ@G-—χζλΐΘQ₯~έ\ž—hΘ_ξΥπ?©όΜ^ΧaΊI D`ίξφιŸ‡4c U,Ÿ₯Β™Λχz«ΑθOs³™Αέ·ΞƒήH4‘ηddPh2ΒΉϊŒΝŽΆδkR $Œθz˜H8–š{Μ”o G»<»ΓA1=μTαBv‡,P[@ΜΔcε"ho‰©ž]?₯cΗv₯Ψάž–LΜDκ,e|<±Ώ·Iά~6&ύ5ΪzefhͺhΛUw +—š­Δ}zM»K7₯5Ϊ,Ψ­ΐŽΓ)tλ1_ œΏ7»xYΘλ΄κβδŒ~[Ÿ-ASK8A½H²­πx2ΥΚΧm­oκ{;–l%½‡’£V +ψ¬ ezVΰ‡ΎΜλΒu½Πυ¨} ωφΊ9ŸD AλΣ:kυzΌ―€;ykhb+ΞΆPψ +α―ΡΙ(q$wύ_ yΧ°ΛΖZS*Žθ"8Qς*9ZlST¨χΓΪέ δ΅PpγeFlhWρκ=pϊΖ¬Rшν"p+XΆ½ڊ· +Χ΄#’ Ξ]δS†μ6,Ÿ³mΕάΈλΉνKλ6­7Ι’ΈΈ=!,έ'Μf’rƒΕ‰ΝΛB_…ˆx’muiΌ»u,Ξ&νψ‰FK+Ι‹ΑIrΩ΅/eψ2–›# f ϊ‹‰cu]/}ίχw Ηεt2†ΉΩUk @Fs‘m'Ο2Ά1ƒX_Λq’Ζξz­ 3Ε?Βψ~θ‡’nμ ΏΞ©Σ‚_¬ M!F‘¬ψZ³š‹`b·Ζ–Κσ~½/2«ΟώΘQΡΪ΅ΦνBΟWω ;πF]’‰δ΄ΰ/‹ΡCO"J­B‡/ϋ*|LMP?€hώ[|ΐCΧΐΘ#JkΖ₯4’νYΓ`(U‡Ω=1ΪTšTsŒΐΠR―εϊM6Βk™yψδfaKzjζΨ~ωΛ7Ϊ”ο:z‘Dα7 βρ&m½˜β#v‰€•P0}Y']°ZχŸTΥ£—οsήίsΑύ^‚v37˜νŒCt\~¨± -MΜ䝓{ΉΊ%σ3b‚6υΡα|«d–U5ςύ#sUΤ ΄j·Ωύ“I}ͺ§ΜM‘T‡_^’Y{WΜ]ΗΟι°ίζ€—ϊΨλVCψ–„―0nήέ›P\F{½­Β€&}zΖ™Hx6Ωκa‘€·`ΧζjΉ?nΐ6o_) ό΅ΨvΥ΄Λ₯Š8Svi―KΘΟ!­ΞrωαΠOλϊCR=Β„pM‘ mΰ2 [U;°}Š€5pϋa'cψNθq`-_NWρŽΤΝΗ₯fσMά!_Ο遞l~Ιa†α}•ΔŸΆ@Κνs?Ϊ6?\Ί.υ%»„Α0ψ±jQnˆΘ{›ηΊφ±|‡Jϋ\š ©R‘ NΌ GIΝ«,δ~Z^6—R{§ή‚7uK"…ΘΈπΫ}Δ%\«|ν½~δSΞkdœ‡Σλj;Ρn€…ή!C>ΘΒ«5Σ‡œX,„υ‘tΫ-”FϊΈ²š•Χ‘τ‡ΌŸ­φ§ΌΊQΒBΌ+Υ{iŽ + ²ω.θŽ ›ƒΩ`χΑtœΩy…3ά3ψ­»5žβ4¦Ρsδ;6>₯†ŽΞ¨ή†Έ/=»ΦΖOΪqf‰οucd8tπUQΑ@¨m5’<aQcϊΖΫlω„!qεΛΚ{³“Θœ}ͺO²ϋ€Ολ,„oseMEϊ,²δΠϊ{ν²ιBŽέΉ48Ύ,)†Φ=œn- δ†ΑΩͺ ΌΊΈϋ›Vηd˜ξίΘ ΅F₯θeŸΉώψAXP>¨1Šζ–₯©²–LΣΐΎΊ0fψ™Ώ~7*Γ¬ +D /vr괞Δρ+†ΚLZbfCΖ]žΧ©ΦΦΨW$Άφ.έUBβGˆsψ; λVϊΗ™Υmο99uΨΙ¦wΌ‘MΣKJθ šY—$qž‰bœB|Ώ8+ΉzΡΐyςY˜#žΰtx Ž}”§wαΕΣ{½K6ξοLS¬³ΌψSٍ̺ΑΈGΨ©ίC;½$& ‘42Τ VϊωΣ‹πmφdΐθLΎfζj[ZΨO¦n~zi +ΪχTSz\›|ΐ ψ½υύΦ°ΊrVŸ‰ ₯’άž"'LΗο¬­7,~ψhž<ΧqφτΣhZ8 +²Xm‡WA„6Ρ8Δ¦Ω€μ¦q*€RuVΜPfΞ-ήz•„ΏmgUΓΥQ+ +o™J—‡…’§GτέχΜ8‘‘πnElέԟϋV\CJμA οtNBλ*ηΚNϊ“Šfb+;ƒT^ N²w¦ή}1A”Ύ­?©­Ic‰B5κ£<7f%’#%Zτ6Ϊ€(tc’d›(:0 Ξol=AqNΌ¨•§όε^ύ§|Ρζ ‘=»ϊRάι^o“}ŽlUjQ ne=ςΆα „,rβΘ x–©¦ ¬Ν;§ϋψ„;ΨΫΌ.W|'PnCύsͺ²/?Ί b”' α—‰ ―L€kΘ‘α‘»Ι’#Τϋα§ιl δIσ.ΑΪPρHG¨­4¨b»“ž†δ*/ε^ƈ«8šβχ Βak1Ϋ…ŸίΆU +g?Υ6Eλθ₯²jόs/)sO"Υέ‚‰0ζτE‘ˆΊψ–Ε&Η ιB/eΡXiAγΥΌς‹οΉΕ8ρΑ0-χ|^k ' +οΖhΙŽy™I‘Ού|£EISŽΑJΑ„O;”ΠούΚ(‚\³νBέΊAϊ™^[ήΓ„±NΑ •)=]˜Τe·e;?|‡ƒφ§σΌοσ@L™dœ‰εΌΊΝUΆ/C«_2“.ο¨γ|Άε"ǎ:eΆε¨`IϋCOfΊΞC―ŸΖ@©{ΩNΘwχ+κΙΣΦ[+Ί‰ωE²OgδGBΓ Χ +2• γv_ωΆώ°:θYtƒ>'Γ%«™?/Γ;j·ΝYΓ{†bPdς +oύ5σ·β;Κ“-›Xo'Δ-J―Έ­pπ΅nΖ»Δ)D7GĈWŠo,VBsGS Υ‚Ω d†ΒΠ^Η€‡βrΔ“—ˆJEӞ‹‘λE^ι±Δ$yδ^6O"™…b_p,²&΄£ρ?ψΫ,³mΫoΫ€:Lvβ‚ίΑVκ&O6²Σ |Ό³Ε½ΖΪ3άjaπ"i8΄λšθ΅¦¨Ζ³Dς}YJ¬Sρl +±9°y8i­‘Ξ( Π0Εa#ξ“R`;šΘ‘Μ­πvΙ½a££ύtΩQͺήΫό]γ{RΥ|ψΝ‰t>ΔY6]Δ‡“AXšZ? +ΐιώώM1δχR ”X 1Ο1J¦RJ=Ϋ]Β"αγβ™ίΦ¬iΧL_όL’­VϋpŸΚΡΥί±όXG`aƒiΒc +.y–q…cαΠk’θγ ήόVΐΨ'ͺ€θ )΅ΐ–χ)W8,υλΟVφΓ¨9Θ‡xπ» Δΐ@ n»z~xε[fθ΄ŽPI¨μšy‘kΡ1CRνμθžΩΛ wτ΅₯ξn£“Κ΄)ž<›PEΜΟ—™τG›²vθ²qQ_^`”ζ½ι ©¨{͞šu]uΊξ6‚"WA4–-43οί' Ύδεμ8ϊVy|•ΡtJ>ξF<"ΪΩ&"Qθ―Σεiם©¨ι~}Γ}ωΖT9•ŽΐΓ5Μ!PΜ?†,H؎I\¨ΥΓHΤ%΅―G·~bΗKδ–βmο^s‘σΝ~0’/#ZΦ½Ϋΰ»α峞€uΚΰ—λύv:Œ!Q ²Ίϊz^‘Ό½Μ*-p˜4md#]S½<ΗRό!Έφ?QX (s”ι‹G‰6I£Η©³7ϊ?Ιύlηt l<;ινΏ0išΤ V7f»V G,β¬t§–›ˆ|Θ>μr•"Hέ.@¬¨HBΣΛΪo‡Μ/N£QžY1wΤ₯ύ.ΐRΑ\©q1Ϋ 7KσX!Ψτάx(Ŏͺy™*ήrQLΪ9έG—6ΐwΜΖ†›tƚC†‡2¬“ΞBΡ*X/½LθΨΧΩ^œ L‘ŒLΣx^xζφ]0ι-Α£:Ι’QΛ3”]fΣ0ρλR2[Gγο.;—“>XΏr6ΏŠKιΤZ„ ”Χς!F%δ»ς:Ϊ)΄π\BΉl‰Ρ•¨P8ρr9υ»2 =π™λ£„Φ#Ύ`σ:hρJ_S₯i[>ΗG@ΩZ7[™*ΩM.4OΪc‡ΫŸj§WΒ§ρ‘Ο™SΈγ΅˜ιψ΄3Nζ»ή€q΄UIκEέCΔή=6.|Ρ"—Ωδ‹ŸΟMǜ™ιšΎŽ^Ζ‰‡ςV†Ν ©^Βγ›Jt퍏έίΞuυΛΞAεr +n5}¦!ν/„–\)6“θ₯|κoΰ{ιL;1ϋαCπ8[γ­Λ–ΦΎ°œΝAμ“mš΄ΠC zn™oΜ??JVŒ˜BΨ­Ί’ρ<‰E{)ΓeCPKδkLωφήGοΩ\+^ΩUΦνΌ+ΓΓyN8U=bIω„W έHMμΛ¨ΣL3_ƒšͺΫ<<‹”kfLω`I|ψΫg[Κ(εΕ£&H›‘΅xλγ|Ϋα;ΧɜiVB(WbΕΗφέw›ΰΡDΊ°°/Gή”ψsίƒΘ°hΊXјrΪ%αβW.ά€d9φ›ΪF$3$‘ψ‚xšζΗ64nš ŸIπp/wdoΔ­VYΟEϊτ5l₯άRΐ‰FΖ’!>†§z‚JgŽΗ+dΟEΘς¬’–ž³Œ0ΘFNΣΌθ3iά`•μΏΐ‡Xφ‰όΠ|XΞG„ζ¨?βxηΞ΄EΐΜŠnMM<(>; ΪΎPσΨ΅ΆOuΏV±GͺM™μπžc‰#³`νβ†’Ν`—²#ιδΨ.S"ι +tΩvιCyΊYv‰Δwe‚[δx£5’Ύƒ6΅ fLΤφ›92ϋτ―;>οRΙ6³¨ Ž.#ξnQ@_)b7ξ[a0±–ΐΰΥ@ηήpa­)’wU’›oeG[\ng„>;O£’|’,˜ύαyw9ƒTopΐ…ΰ%ό΅lμ‹…+‰Φ› ΰEhWΘ Μ°­ύnQRά΄_οƒΎΞ‹Ήί±ήηςy”sœώwΐV?*Ϋεz‡"Ω‚>T‚δΪ§;ΙIM<Υtˆ›²¦ƒzλ"}iŠ[B_σ,ΑcωΔΝϊΧKgϊ…Ο¦Qθ{XφΟώyΒ\$D8Β,GΗ¦Ο{GϋΧk +ς―ΙJόL’t‘q±Ξιf}±nœ·ƒ1“k»5 Oœ;Β \Υ~tΧζ"ε8r1VρxŒtΦύAέπzŽΙ=ΈΓμΛψρjΆ<?mΙΟυήό@ ΐΜυ’=δ§>„4μ”ΙŠ€Sι=>WΧ] uA5!θχƒMΌn Wηlο&…dζαwι‘)” n½Ό’Lϊ&©ͺ14°υ7€Έ„υ‚Aq€$ab“sƒΡSΝ©ώhWςeιω ;κΛ…lιεmΌ.ӊ(ˆh[!±κ:Β»οΝRχΕ>ž΅3S; ύ` λ`₯Υϊ*ŽM$ZβΎ½Ÿύ Yˆvγύc퐽–Š ΅΅2‹ΠΣ ZH―γ’²•*η uGA₯1Qό¦‘VEΒ‚3WςΣακξωLΏB RΌΦ²ξΆΚ6Σ„i V›•jΐ-–’Š1΄)™t<ΜΉ:Χσ²2`ŸP*ΛR³p Ζ±FN~=ΰušΐΉ 6Fρΐ’γljŸ64Υ—ΗnrΘ“DευηX’0ή€.‹Ξ·žόΌšKιRtχڊΆNΎ*P£L£ΧCΒίށΏ4,‘k Ύ‚ϊήd’0Qυl˜ε«UaυY+ʝ8μπΨ+πέ/oΐmρO(˜œΗΦ·Άό΅ ™χAΣυΖκ ;TD€i· Σ©I ƒQ žbέΨ³Ž+]ΙœΛ“₯Όf—ΆοΒΐDC\WΓ«p]Α‡Z<5«ΩX%Ά”™Θi’νΘx/ώ’<{JυžΈ0μ“Š–>iŽ§Ξ‘#D΄§ƒ—.3Γd?% A2\}v©% Lή,qΜ‹ΝΧs֏oq‡ΥϊjξZέl`i'8ό΅©4[Ξ½zΚ 7¬‘ΫΦ’ΜΖ±wχŽA cβ` ³VΟ Ο,nY•ζ8Wjηbλ1šεατΑP(νΨ&τ^ίϊƒsηϋ>Σl I3·ΐ+\ΗUΡδSάΌšaΗρxŒωΌ:ΏbΝΚ8Ž$χ—³|rk»Rψη½HΪ~Ÿq-o/M{ήΈ`Θρ[εΌοpŒ „ΫΟ-YΠί³½UσΝη›UHy{ΤΣχΟΥZ`SIό¨2ŠdΤJv&„QŒ}*ŽqΏiϊ‹„? {οˆύi#d&΄l]ʊx†Ψί™ΛΆ‹3ήφG΄ ³fU ΌΘόN»ΎΜEΧ.Q͎ΐ«TΠp΄§‘φσώΞ†œ8ξ}‡¨Ηαρη͎ο―έϋχΎ„Θv* +yΐŽι-—"Ένδ<§g“*‚uyhΏ>ΗΙ–γΥaΝ4Π°a«D’ ’ς“dω1σIΒό~£›ώδΦFvοᏭ_’yΘ %¬QαƒΖJ€θγLΏ΄Lq„AlˆZO – ³Z¬šOο,/Ϋαέ!q)Ι#ŠZ¨g"‰BW—œB~+ C«jExΦ ΩΥUxRΎI©Κ;[₯―¬§V€υ•BiΰύD2sΠYS3 gν¦‚ι[NΔώχNωΐΰ^Άφ “ί‰Μ€ωΖν-aŸ•4²·‡MνG|…8œŠ6ΆŽ) Xjι)^^¦"[βB5Έ°9²ήJa/|`κ„yB-„f€sp§{εΈ¦Α ΰί鐜K­΅6°κδ_Έ™&:Ι@ϊΊ3nxJ…jb7‚Ϊi*ˆ―ZηBΧΔόΡƒ.²Θdά½Ι6”ΰOΒrΟVœpΊ•˜½φYxΣΆ„ΑQΆ­±²x«TFωρMm«ΙAާ_ƒ·‹Χ¦”kE(…ېf6Ό”ϋ +k²₯jΫ+ƒ‹³θτήϋΟΗ!oήέΖIΖΣK,*Όpžt}ύ<΄-‘­ωΕOŸη§ι*χMγA€­d"jΈκ]K]r„ϊΝ°u4o•A‹|··r­JSPΓpo_‰ŒΧd’ΌIw—hΊρύδi„““ˆΊM–°]ξή&Ο|ƒuhb²qtƒόεΰ!*‘#PάδU..ΚήΒ©7ϊω[¬υoΛ;Κͺ–PdFΪ8Ž7]ΎŒ²uζ¦Σ‹,Ά!dtsŸ;֐ŸΡ­7‹FΞ΄€4Χζ@ΪuΏ σN΄CΘώΦoC`~“+ͺΈ;ΟIύ6Œ< ™¦5¬οcŽhΫ)XΧΔe|Π~Xη΄«’ΰŸ eί3 ’}P&Œ²‰k@˜†/…{Χ"‘~OK¦o[].ΐH^K»bφΧvoŠAΛKσω>MŒ+γπ‘#¨OYŽΟ'—ΤΧW„M>DneΣ΅ϊcI"fΦΙλYζΙ†gβ<,Ϋ%’[ΔTΒ;©wπ€*3Œ²†G‘+‚)€ΙЈΪD©°–;œ¨­^ΘqMχ‘ΟΓ]t”Μ4ςμ±UY―4Œγ’FeΙoίUμϋ •1v™8ω aRHΨΟκχΐ <πx?ͺB\¦Ύ”Α>#ΦX=―ώμyΕO€aπ +W’kWˆ’¬abžPej,…ICύϋšt3cμΊy/ί)»{‚ύύKΟ,žΓ“x¦κΐ=$Žλ•a§Ε4γjͺ~8?€―‡ϋ»σ{πŸ)‘%m;v.ΣΊΟ¨ >"}΄ςjOAΓ<Ψκ‰Ά 2Ζ³3 +f„‹5hΪΈU™Š‰κ·ώ-d‚“*WLξ4κRζ|m'mx«jΟvΞpb4!X2Υƒυ6€Ή5Ω3ΨIG{’’ξ1]n8/šβižWœς+Φώ’\τ%κ…JEέ+=„ ω&Έxγ₯ ¬¬Pεζ/j,&Μ”Ά]q‘Έ΅±i:}#Z ΝC>ΆO5ά{‡›Ν|±ν/κ˜ε\ρPΑ+­Y(σ’Q[Α½ιErͺ$™#Όy Ύq²r&δψ’ΧΪκ­Α°cϊΡ<,R’' ©$φܐϋ^4ˆΠixΒh²g‡MŽ΅sσ?τζΉ FU(bˆλσθd?|'ΚΟ‡ρΛ©ˆ‘¨ύήΘPΕΒ+‹Ν1H­Π‡CΊλ,―΄‡£Ρφ†f¬λs/Α 0ΝΫέ?Ww@˰鳃]ΘC«7€6¨QKw–[ϋτ΅5T'―Ί —E,@|Βί΄«­ς\ΰž-—ό°π0κ3β„ξ$ηϋUšU’ΝL‡ΊΡ‡ΎNΑ?―ž{u.Ρ7σ³KIRκzœ»ύιΒŠΧ¬ώ+ϋb…Ο‡Χξ2™Ρ©ξOςΩ`ρŸGCρ lbΖζAcΡ”#ΦNΧ'CdLCO;:πh ΗΜΓ§=Ε’SΖz>€rG¬ ε#έ.ρ•₯Z΅_- +²JDΓΧ;5&Νkν +5Ιλpp§k.‡‘έΞ€DS6εεξ|<ηοuξ¨4ˆ J)φAΤdx†OO;°Δx SΤgΠώFVαυKέΡυQ–ύ―{,ο 7ˆDΘς₯ή€Ξ"Ω ϊΌow΅+[†cr―³ε€―^Θfn–°σFFi“‡φϊ7z;;ϋΌΨE’½΅y!ρf˜‰ pΛΒ²–zΌ(™έΕ’&h_ τxL%Μ|<_$t%§UMwΚӊ<‘•mζ±ώΌ2θEZν| ΛΈ6‡‰…!=²ΰk™―…nQ?ΨPϊϊŸ7a2ΒΧ(o(ρ” F»›fzΰƜΥ((€ Œ‘Mέ΄D[žcˆ%ž}μЈη}ς‘›,_Μs‚ iΊΧyH^ΰƒg'μgw;=°JBΗEΞ…VW,—`p­l Τ]σx5UXbΨγΈφƒJχα β|ΖβlχŒ­N‚p/QΈCt­Ι,ψΩ«FΥS·R½ν†xΧγη 3θ΄M¨ήγψη|ͺΡ/Λ *0ϊΗ•˜T5'bοl†ΐ =j’―ύ¦"IόΕήS„ψΆ^μΛ „RD΅Š”ι…τζθ ‰P8 μσ‹O +ζ%ΐ,―uqc‰€(ΧΝ +^žπΘ$Nky­¬ˆΔ'σ„e$žo˜§/…^œ0αh"ΩτΖmΏAΓ!Z˜Sδ¬ν9Qw˜ŽΝŸΖΣχη;ςάΙ³Žœ(πFύ©ΔΟRα•Ÿ#ih%ξkŠπDΒ‡ξ.ͺ|r ©ϋΡA`œΆXή»e·‘νϋ”₯ £ϋaΓw©ΖμΖhΕΪ‹γ΄vE> ‹Ϋώ|ύŸ±%™Ι&έjYRŠCόd²kωδIι–ξfΨ­ν° ©„mξνw‚ ½Nω2ͺ{Ž +ž‹QΧσC›1ό@­ςοΰ<#ά…φ% AΤωK δE/ *3.‘«ηΧOq›θΓWΪσž*2 +ύ2?χζie3£jΦY±0΄Ίuβ@ۚ–‚#b¨ŠkΐΉQΩφΡ"©².Ή«΄:>Ϊοχ‚πΙΨάΨ™ηΓΖό{Θ†v?„Ω#Ζq9A’ƒ‚;•`ΐί\ŒΠ‹ϋRŸ8§ΫΔ=λ꽃1@@c υ6‡ΰ΅|&…ώ玹Z§A=MV}σŒ|νε[αI/Θνš½X•κρV˜T…Οe†Jάό‘BOmυhό‹ηο ΒFA‡½<)φρϊX­ͺ₯Μ΄ςJyό‚™v=-‡…υ§υD€…ΐ\@ŠΥ@{σjŸˆs†ύb‰NϋKΓdSYά¦T‘[psΏ Mυ"ήd-sfp49Œμ™‘Ψ-©™‘NJΟI ‹E)€^ΪdNΌΩ3s34ž& 0O΅ °Ζ± +j-sΘO…ΝlΛ―KœŒKSΗY»οέφJ­οoWqtα”AΦ€Ρφ*N|θJP~l΅τ0‚5z™―Ζn<…—΄6 n{z†axΙ-HΟ£Ϊ1΅c½C莦*,ήʍ«Q @¨ZV[Ϊ’ +o‘TLί”$ŠΠβΧf J₯ΑG8%ΨiΆΨF#C{$m”±&«Ÿ―>Ilͺ1cgRPΙΨγf€mπΥΔWhdN9-ΦλjKe/q₯eΠsΊt^ό;-qk¦ `γ%αr—τv/yω.ΐε{•@#‹w‚Φ™ΨδΜͺמK> :<cφ£μςj‹k%L—‚ο$Đύe(€k-‚MžpΚνͺ―ο «šˆ7n ι[uΤσι-]£%!oΛΉτX”C6pJΚdκξνU΅ˆΏόυ\P™hC1Z@07c±’τί± (X½[ρ0Sλgr§οKd€…v¦'BRΰ§§ό֎~―»oΊ†zΦ‰ΧKL)ͺHŸŒΓ&άΗ'ΡCζ8.}'"δ‘bkβE ͺ₯¦έ«@k‘BM½CZρ έ@Ώε,1d,+ΊQzeΡϋ9±bOμΪ„τΐΩv&~BͺM­y,fξΥqΈ¬²‘.΅δ ΤGήbηGnv^? μ ‘y2|h’OSι|Εά„uΗ‚Q("e]ςl'`ζ]‹Q)Σ€Š;›tCziφ€ {MoΈ;}1ζ‘jC-_¨AίΗ8ΌΐθhΈΉ 9,ΫέφgΣUήL?θώ‚&τΕUΧ]‹…~E+nB +8κ\)1l£›'DWΦΙψ5E˜n< kA7ΪeδzΒa7ΥeRa…&š$δnzΙγ‚‹;±ƒΉC3Œι’>~@dds)Ώ^ᨏi‹=Ε’(ΊsŸ#θTν’¦y½υΥt(°ρtT΄½@w ΧΝxM |ΆnοK)υάϊΏ·>AR(θ;nnμΒRTEτmν]Σ$–ΩjέwΜ†$τ›jΙΈ GN+ΐPI•‰t:€ƒ+θ‚.ϊφΈΨ†ήupF$!:oνΉδuˆ]?5ν¦–Q rΩψC²’YκΦ΅Aρ)Θ¦—sΒΧϊySSΒίγE±@°%>OŽΊ¦ˆ‰KBrαc…@葦!-iCΡV³Z/ΪτσΙΡvf Βω$y'xηύJbTe1Ekvo­ ΦθXkk +±jο―΄ΈΈ Τυαe,hΡ΄΅λ‹ΛΧ+'g^%.š wKŒ΄4³nŸ’‘XEqQD <š›$„\±›ύ ½†n¦qžL•―"­‘8YϋЬ΅›nΦΤ +Ώ˜'°ωΞ}ΫΒ|XͺC/0ϋFQvϊ*Maϊڊ‹ΙbeHκUφ ™NvΞω>ˆOΑF‰™ `ϊ›pr³˜•ϊ›šžΙ8Ψ―Λ«ΤVl2σα\’hl~•X­ +¬PΊοξΈΕΟ“·­¬)U>f>:ύ`ΐ3hb,Tχ;IΰΙΙχΥ£ξETΏ±Κ«8₯iH:ΦΉwœˆ©–]‘ƒΆΉΥΦc{RΕ«„δŸCΦ;#έά€<:“&Aΐ­<u\Ύ$X9ΕψP£",ό)Ž#ӌς-Νϋ{ΨKnψόώ1ID"όωΉZ?¬dγοχ›Άώψ0μ¬Z…pWΌεiΧΘ] ³™‚0š{q90ΙWΖ2B“; +τΉ +ͺNhFΞ +ψL0še,lӝX|ωy½’Ω}aφπυξ(-5ώq=v6‡Lσ†ΟχšWύΫ‹– Tύ~&šUΨ₯™zpTژΰαƒcΓ1€|ςtdš`_&Yώ;‘ιœ5?Ιβσ>„§uΖπ3μφ†ΐΘvΦ +ΨμΆΏRβ‰φΆνŒ6Š#Ρίxw” «Hυhε.ͺu,λŒ/Τ?,Uf-8™IhF·χ§ΰσ_&\ ΅ΫB4ΊG‰βΣZf–l΅…iΕngpσήζ₯|ώl΄r3ά¨(p_hψŸΆ¬woRρφΆΫέ9ςΊk`δŠTΡ?θΐΘ›VΆύ£κIόŠςs]ΙκΏ,±HdΆƒaΙ‹Kœ§Ρ˜›MζZ gΑŽi+({σxB[)Ψ{η,²'Z! ύ/[¨Μ‘˜Μό W=œΪGηεΙ"SΈŽZŒƒ ώ/ ΧYΡ–Ί΅:Yu·βȍ/!6”3₯`?Ωd^ܜv¬0ΘEŸ& Q‚Έ—ˆ!Π§©Δ‰`‡fB”ͺβκR!Z}f™„΅ξπ,Ō³4š`7dž‚ύ§qΟΈΫ\‡•g~,αό|1&+ςέ†Ε Α΅™˜pτ—`=de±f‘’†?tV€ύΌυ1ΔΞ%…δs%ο± Xδ7'§"ŠΛQeSzνb ΅“7J€ˆ[‘††Τ ›VΌ8 (b›Φτ•ΜΜGς*ΈΥ^-²Έ9½0_³“Ω¬ZuuOΰϋˆ_IΈΓl{ϋΛ#}Q»$€M€μ–`Κvšρn(ΤΜ·~›ΓRpΰ#LR©B‹έφΙ‹A6Ώ$Φ;FΛPŠηΝ€Φj:*1=S[©Α€kό?jςRχi…q§SΕ ^ΚάΕΩͺ±eήzΧΞ’ e΄ΜŸΛIω ­ Φ\ζ.γHΑƒ_†aψαΨ(ΞvΤΣm»“}ΝΓC xγ1ž0³Θsδ²φ§s>ϊ©ΦIJ1Μϊ³Mώ~w!οO[³P<:κ Ο©S…aΧΆ€ΠΤωkh +Ώe”k+ο ρΣ} Λtp ΨVG AΊ@de…qε™δŽb‘ͺSOκ5ˆOŸŽLω0Gόά;ΗA`>ɝY §αέZ¨ιΞ;(ƒΈΡF°§CF.L:4U+Bv$‘}΄‘‹xΡ―-•{Φ›p£nλξΛ"α~ΙWJ™Β1*‘Ώ_·aΉΤ‰7ANπώ€ΆόhΘc,όr9ƒύ’²ΧN>BK}B;ν5)yoό8§& Υ-ΗΤμ.r^”ς=>+LGΫ1“— +ΡΩMH­ƒ…IτX6v ]ώΓ²n&#o•z\šλΟ‘σΒ§cΐ+§6Λs|mƒΪƒ +»KI_M° +ΙζLΛεjρK–’{φAElG“ςί&ΒiVδα§8οΈ^Λ|dtόϋαγ7τʜI,/»:Šω|ζ­¦ϊrΡ$2„χ‘m€e_±—x–Ξ„,j²M§‡ρ‘; +Κ΄έ9‡―π^7ΌvΘ.ϊƒžŠ‘^ΔiΕεYg°†±‘₯νm†Ž„@5Κ@£ιΩ»pFri™³9… χv¨-KιΎ\Υc’κt Ζ3§!’䉂ΰ½Φ ΤΤL_όŒΨ/„ba”_`bώ…Θ>°ˆŽυ1|±Έ BΪΩϋ}mΊϊaκΕgΤΖYΌ3΅^‘ᬱΟdX€/Β<]¬Rζ…fŠάJSt?ΆΔ+RzuΏ½\Ψ)HbD$ŒyŠφGDPGƒ>Θ.ΎBΝvΑYίDΞ9EΚϊe¨™¬¨­y`hJH ‘ΫDΩ¨τΫ¨| ΎϊžΖ)u·ηpΖ?OhδΙ„ΫͺϊG’[+ŠzΨΙψνΫEήπμEΡfΔΣκή>Aΰ3η4~³tQΦαBWK­”Τ2»»Λο"‹αοtοf‡ωgκ‹X3ήTIωθ¬EKŒύaε72ηmαT ›&•ܐdρ·“…ρπW,ΊZ{λ ₯τXτ{—ΣΈΪD^γg,ΞΚ†Ώ  kxΏ]ν¦=žNλ% sjσ0€ΝQΌŸψw#ολύ΅f¨μΪϋϊωΡο#2έ\wΑ΅4ZAοV”­Ž„šwΑ½f=ψ˜ΗX^bε^ΒiβT{¦]^Αο\κ}QeΉD*°€Κ$=³ΚTl.«³XΠPΊΝi}¦Ρφ­* ΏΙSqΌ³κα;ΆΊ‚”/„Ξτ:c“$9Xˆσr­6υΗξtΐμ6Υ‰y§ΆΤαΙiWάͺXΆ‘gjδΓ ΑΔ¬¨?W›†[!#K«¬[Γ0?ψΫUώΓ1GӘE…oκΒQδ3)}μ)€Ÿξψ–ͺ₯+ ΎE•\'Ž_ f[³Ϊ…ͺzc(sΙΆ^ ŽΕ'a¦ςsψayωq2E—ˆ@5Ν‘—8f+hίԁBί,a{ˆL™}Ε5Sd+₯rω·ΓiΙ‡ͺ‘nΣQzF±­LZ.P=…Έ:%Ž,Έr 8ισΡ’¬·EE‘6ύAOΜσ„Aƒ…Ξxδ•»²;΄0ŽKΝυ%χ +1 Λ΄S“ΩJοψ<ݏ8§Ζά/ΏΪ(hŒΉu6£Β-τεQκςΓKΐ™ϋfΐπЍΠs·Uvρ8dψi~·ωω½ΘDhς,q5Β³98΄„b {ΩΣM.χ cΝτ/« ŒάΟF?.άhCΤ\ε»84αΎ>>·8 )C’Ow܊δγΰ§Ο΅TΧΙbΉ¦ΛΛ Μ£#Αƒz aΨηΏή|ˆΜh΅[Ά »‡>eΟ€&?€1ψΩ‹šŸΰ€[ƒ$ΐʚ%€ΏAL?0€ŽX§κ¨Bΰy‡@ιϊΦοΙlΡ~IΤΜέc?Ϊ/kΈ\`ΰ‘›Ρ Σ¨Y‘§U/ίλσΎό18SέΚ‘rŸŸΨΜΘ›ΫR1¬―/Ήa"ϋ`>8*η'ΰ‹xΥ]¨llΠ―/ή€§¬…S.³WΐωΟzΕ%…JgΦΤ`±UΆ‹“ά>΅‰d’_Σ6‡κ0Ξ$„κ—βείΑ@φDˆ-AIΣΉΥ-\[@?«Ή΅ίMY{4U3’½ͺΏ$ΎθΦGΧί¦:\NΧwŠ@›ΐ3w »•š‘o +ˆQ¦n½4Ι(NΝ?LFOΞ’uο+,σ\ βχL“₯ ar}<Ί΅ο)ΈΒZ«δεƒ#2„`—eΊ%κ+»ͺM`<Ν£―)ϊψ„ΎοάaTΟψW& +!7¬φώ‡o-sq™ΓڏRψuq#*ύD*Έ‘gI s“ γ:€tμζ-ξμ(tγ)Δx©ΏϋL ­!XΞLΣ+ςBΟρ ωθT.(ΥΪ yIήRυυ«3/Α~ S .W•ΕάCH*θ€y#jΩΕΣoGς™-O“>4gnΝRΘ•kLE"σέsΙ“GΘψι^γpΛ7ΠpƒjΠ ΪQ> +0…ειmeciYˆ¬ώπύ―V»Πœ£‘ž½”$9X@|p€κD·ώœžήa±Dَ˜Σξ ΉΠAςΉžB>-9¦n‡+š‘S΅j sΟ΄ «·Ω6σПE΄€εJ <Ό=FIλoˆ-˜+ω¨ύvω˜5²_z‘θ|€QHρί#R΅ŒλνϋΖΆ…V[―KύΔu‘APΊσŠΒtc*ωΧaaΨ${H:σ]Ύα VΒέξ)Xˆ’Ϊsχ:ήΫ½•5!Ε ,A‰¬Ά~F°œδTuΣ_hqΠρ¬9Ss*Ϋ’nΓ!©œi ΪΑbά]WλMG•! -΅ ^­ kJμW€ΖbΨZ–’$2»Κό‘υΈϊkόŒaΛΩΧ©ηεwşόv€Zώ l/ΗqmΰΞ!μB½tŸͺ‘S7oϊcK{ps:Ρq§θbU#ϋ²ΈmΣ@–¦“fκu΅1‘η㽉₯°+ϊŒeΖΚCmq\T“Ή€oKK¬›ŽR"ͺ‘ˆ¨ίΨδ²_z £ )ώ@ΨΌΆ‘3›Ύs¨τYΣ΄“·0yιφ‚εό“΅qϋ‘ξ»H;ΞΒΛ³ΰΚύ9£4K‘Ό?zα£…ŽŒ;™·wΑ=ο«›0m&ƒόΫYΌXI)Ÿ 7ЊίΒ3πpιJ ;Ύ˜“ξνb~Μρ !ΆCΙ­ *–τΏ6m‚X *ΰsΞ%νψύN*ΏE νLΉ Σϋ‘"qsvH{5!Ω‘4σt…Δ‰2Ϋ8ρ2ΝϋtB₯©†ΐ( Ή―0)he„¨b[τΗΓ},nά(ŽD%ΌsP$zu}­»\ώƒξb²ΓoΪMψ»rΠ‚δΜ(₯*ν‘Υ|"άΕiJ²ƒXΖ΄$ϊ·‚M―άC&ξ‹»%7PΖό–€P0?έΥ„Ά Gδ•χž/ηκDKu.ΆθφτΛΕ&T(+<ΤTt~ΈΡI#¬ŸN£Nκu8aOR4ώ{Δρ£βK»Q·άμ_…zΉΊ8«^”˜ Ω P† °ŽY%υKυλi’8C»_?£§΅V!*A„λg<Χ΅¬κ&"{ +³ΰ£)V}―υyVυΛg€δIβBDϋ(g ’­ω- +igO—ν?xφήΞ°/ρ΅Q튡{U‘’μ + ;nΫt +ΪΛwvFΆΊη‘ΫΏsΦΛσͺMeh=₯b—œ‚δk‘ΝΤ!A˜QΝ6φz&B£΄\•ŝΠHΦ―>€₯:$“(3υG˜ώ:7ςΘΦύδ‚šΈq"'@0>Χμ3έ»―nšΣ=vλ’<vxOqΧ*ΗVΙ*‘«2=ώ{ψ%όπ{³,—ZyFŸΡ†ξͺΏ– I§k>„Υ΅―"m‘=e +—«·ƒnH~w›Št’U3}=šO]lΖ1š·ψΦΫΧ˜ν© +ΪρETΦ+­1X‡σΜν΅JE S½°\½;@ڝ3@ ½Ξ¦p2'`*•ΘδϋP{“avO‚Β»‘Χ)Α―Χ•uΖj6žmνVΧσ{`8ξ>„η’6„Οͺ= θdμβ/YΤ2ροΗb.+νΧDω¨]KΏ›@EQp!έ]Ξ(g›OφQ_‚₯±ΐn‰(BCσρσ%rΈ,“Α%q€‰š€B ³0AZtΤ•βΨ¦#+6#gE­EχΌd ‹°…]?–j3ΏY‹‡zwͺηΊΚΩ +W 膑η2H6I_+a +UuΞΙΫ,š_Ÿ7λ‘^V/M ―3―CΦo­`k%žό§άž–HZEWπ»έ3ž~#MΎΰ₯sϊB†]u4DΑ'–ψΎ”₯ΓNG¨m€¨£Φ…&Ρ@Yuˆ€lΐφbk/»ΈM΅λM7pv`―υ”ad9%΅§ŸτγI5g­gRhοΐθψXΕ•Ωhη^°N2rhώ.'Έ]Έ "Ks!JΎŸšB +‡vwλΞLzΟσχs(ΡFς©Αlε_Vi;?αERΑ90_Nά}υ³ίk:μ–2Ž’ P "ΎΊLΣlWvΊ!iξ’π0£ͺ‘£—μΝ𦀍‰£a-Α+N― (§&‹—[ιh Λ(NοGCΓη7(₯P9δ‰qΧCSΊyXKŸξ ٜΙΫ£ƒ/"0Ώύ€ςͺ:­"Μηλc0ρۈX;δ²pΦeγγOΪ γΨF·άΡ―iŠ­ΎžψT”Z€T1r’t₯ΟΑ·D“5菂°Ot!―ξΐ8AVΔIˆη2ABOM:bτg―Šx%:ϋb{‘Δκ ζύΞF·7²DJ,ρΐQ §}τ“ΕΗI³½`η +-΄kkc•LΟ1vιΪ«! ͺf!ώRU…"$ΊΧΠ0ψ§fύG*.¬ ]} +£Ψ$ΩV*Υυ¨lΰ6q³„ΓΠΙ\‹v ΰψȜ΅A§š½ΤCQ=ρlq±‚Δδr§ι=ΎŒΠΪ€εμn“Τ°/:Χk1.’  ²VΣφydψΰbΑzx1Bκ’λfρΥό&ΪA%O:/4+¦3A‘υ»KΓ'~_―kΎssΟAφ€«DŽ–‘T-βο’½„ έ·‚NΛzS€Έ’ކ0ΟΤFγt[JΣLcHFŸ2ΛF-ΚΞ$$”β|Κ5!&–¬c_ΖM3­~ΰ°EΌΰƘ „Λ€ΟͺFdqk(ΗΘK+›’]#³Έ?.h°ŠžšQL ‡έ,Tιͺ€‘l3­(ΐ·eŽ‘-HΕjίo¨ώv7 dœ<Q9ς,'υΗT±XXE7"t$j$ 4ΊΈΩλ;£|ΉΐƐΆ΄³/=€΄];Xσgl+ϋΣΰ yΒm5δύe{wg,abjα…»oZηΓ¨•“ΈΔσWrδ…iK$'ΪΜoωnΰžώΕC›†‹DΔπΦ¨ ½$txoρašΖώ8ΤΕŒ]pŠY-λuœΕD^εu΄τ+!ϊp“ψρθƒς =ΦΎNMΏsw΅Ž¨Σjυ%_ΛT\/›ΞOΆΉ—,gΎνpuΆu±ΗNυΥϋΰAt΅`r5Ά¨tj­ΧT°V+ŒΒA‘ΐ*Μtν•Ι‡Β£ώΪ\ﻘ蜈}ΟBq$ΰΩ£΅5. * 2U―hτ°§6Ί°>h/’1pΞ*8™on‚Ίχ8T@jςνŸ]ο™ͺΈnu :ΖΔΪ¦Β?*ƒ6 θζ]Ξ!’ικΆΎeί·uΛ²»I Ώ»Ωΰ'_t΄jΙc.sƒΫβ*Z¦ŸέVxP˜½˜[ί:qŒ°}VLw’Ό™σqžn#Χ +Τ@BωAH[̍ΆκTVΉN²ΐ#;.^>υεΟ‰­ ­όψ·ŒIΊ‘iθ:1l΅ΪͺœͺΌ>2d―΄£aTΑ‡Ά}Ώ·!Ÿc*Ž•GΖνL(Χ?k»ξΑŒ³kΊ. :Ώ»I’Θ³ΈΩΩ9G‘‰:†Kΰά[dύΒ8uΨdRΓ‹α=λ'؟Y„‡5­<ΪέR§δ   ₯ΠΦKό…WΈ0.1¨)žΧφ†m +6vΞK(Λ›+ρ qΈΠp “ΨΈt]ήͺlΰqοι3N‚?#“|Nχ§«vίk,4ϋ{œ΅%Ίe|Ώ‚*9Ώβ©y‘-nόH\³Ι΅ΙhsςsO°Ζ§A­xM,¨ωέ…©+UCωLvd’ξ +Ÿνs‘.αMTο”“εΏ—xŸ5bŒφx1ΐΩxVωΊuΌ<ƒ2΄­Ί&”)[λ2ςKhSΨ3oΪ4Yή>π€­ςΝ/w­ƒcΧ +†άΐρ6ΊΣ'v&£ΠRŒΞΩβ³`2&„oƒ#Ζ₯&τΧΘ*΄JJΧ‡β5”ˆ‘Y§VΨsޘ1[[ϋΊα…{/ϊͺψTΨCD9ӍΤSWφηPο(·Λ Rΐ«ϋ.\>26ˆ»X™”KώurgοaΉC’ΖαH d"TXŸί+Dά0ψveRj4JAΊu?˜žΑ»‡Π;Λ’Z7ξδ‰Ηόφγ-vjΈ¬*Z|μ!“Η-ωςUτ\zS uΙyΈIυ²‚9~ wΓh—Z3;½Λ"ΏKΦdXŽKx.­ψo8< u¨l½kڊΏƒ»υpΏε!ψϋΓzΌ”ΟwJΧlŸΚu£)Η+κˆ^uυ+W›&αD­―θζSΦΒ—VŠΡk{Ψσj6νGΰg“Ν$βˆtB4qΰ%Aτ `a|wγώ4<π»hrΦD6u£WSΕhΎHψ:g¦ξΈ‹£ΰΩMžhΫΪΗ^ν”zcœιΗ‰Jχ '¨ khG£`ΈW°mxΫnŠ–•wΗkσυ !a΄ΊMςÊ€]ΤՎΈrό²1’nι§ο"ΰΐx«Bβ“!ϊ7¦²Δ°‘=΄ϋΡθ›fzT=o‹(™?[v‰$6₯ψsωβAΓύF@†aJ‚œl<›Μq͘oQm ͺ€œ7,ώ€¬[X’‹7.CŸάYͺΘͺIΊ~±€+έYύ5ύλ|oΓǐΑω#γc{‡+aΠ)―]§/ |κΐKρ/ώT°v°ό„h9΅>\*v+ΊΚΕΚFM9£`Δ† NΝΉKutχTvP qnΥ 1»Rƒ&‰μS―ξlξRωѝ/ζFw ΓbmuKHh(G\ε΄-Ϋ»ζ΅±vέΕψ+ΐ0ͺŒΈrŽ9)αˆQƒM;υ.’ΐ}ζεF’pάߐž*Ϋ(@ν ŒτSg +zx4v†\ΒPθšΧΎ{QS<}(Z ͺήώ¬•Υ°ήΓPΫ­ΔHθaοέ`Β‹π5Ί-CΗͺ\„αΠ$”°Ly―ΐ!1%E₯ΈGΘΤ†κ˜Φ5Pͺu­ηθΩ£oŠδ˜±Α£B½]›—?,mP[ί±7~½ν‘^/λŽΩΏux™Ϊθ“΅Ž-Ε=Ήu~‹{Μ¦ΈC0'½³hxω1ˆ?Γ΅ Œ½wtYφώ4ŸΙ5ΰ{©­Θ!jStb« 'β a­h#žk„QΣQΟ…€PlP‹Θ΅»Η¨‹λυ"D•©Ά§‹ϊύͺy+εΡϊ€έ†;jRΘΣθI°;΄ ηψϋϋ/†O•γ=ηAa… Ψ<ΌD%ΙЎhrp‘Ϊ +ΗN cΡJC)£›ή[ΕψxcœκγβJW`‰Λ°‚Χν¦pFeέuDY58<Κ@np6™²Uδα'ፒΑΨ’~Υ^l5,ΟπύΌ}τœ/ΈΓv΅r›εβWg†Hρ κSμδ­Ε³Ω+ηοήΚΟZΊ†όΔΎΣ›to‹ͺfˆ rκ³σο$Ω^Κ Ε€dŒ+`˜IZ₯έɈྠ+φάΝ?ψ~uc¦}fa© +l+6ΠaΖ†N&Ά:ς΄¬<‡|ŒάjMYΥ΄x•‘œjΔΑΥ©%W ω°ΪΈ¦&΅ϊΦ€ΆV+τΒZe:KΐΦ«8>ςτZmS“ΣΊΰφkŒ¨"Οc‘•­©°@ΫL;ΝLΡρ5―θj΄M 2S'λ"ΞΕbξόiι”Χ)·HVƒoTJw0`τoKI66V-|ӎi΅ &βhˆpa†²E}ΊΪ3ΏœXηEκκ|*=ψ<@‘Ί/Η­jYή_5Ÿ„D#°υΫ“€K:/Υ Ό₯{ KίΚ’π•ύrτφGAΕƒ!F ^ΓΊ\7ρMσ`¦~JϊFςω˜bty2ωΓ΅ΣΜkΧΒκdΣΖ\ ~6; ⚰$L~ρ·ΎώR^½xΓ³60vψη ξ²ΛΖ¨Ύ†Ώ«LΉDΪ€]χ؈–q£{θΔ,€Η­¦¦+izuŽφL–Oςˆαϊ…PΏ“y#Γά,K\h’Λ"ΥΦbΧ·Νͺ'ΊZ΅Žnώ’©ζίΈ­Χζ‘sQRZΘ^ŽC±|Ρ«μ©ή”‘›ή~„8VΠΣ‹@λ:J@ωXΪΉ8ιϊzβψΊξΧ LͺOtͺTFδ«ΕbσΟb2f₯Έ;\Ώ1e·J«η4Μo>ςrŸ.vαρ^;ΠuΎφΦκyΰΔλ…#ΰsΔ8޽θ―φ›ΙΦ4•θ#Α†ΗbŽ–₯ +ΰε<Ϊςk¬):]”ι[₯ϋqh?Ρ’¬>D“VσαφδKεO~–\M€¦Bςl(΄ξȜqN6ΨφΒaL«ϋεΐAiλFRτΞ3νž’€0΅λ2ή#βτ–v…ωόΪmTid―Νqή½j· κΎ―±Θσ­vPΰPΗμxœiΫ¦ωڟ―+Σ!‹‡7ΟJiD`u-aξwn†™β§š€š·ˆΡwΏ’‡Τ¨ΰΉϋ3’ qp΄NέaM.ΛΌέΖιXAΰν ?Γ<™§b^Θi»0ΙΪ[Γ”žWmΚGG²`AiΜc½`@zJηΧfv8‘£€ΤFί¨2ς’ίΦCΓaΞτ$₯NbΪ•ƒsjS)2ψ]{b₯)Jω‚ΟΏ; 7Θ¦_S)―v_ʊ…8Œp5=>Ϋοφ‹ΠΔlg¦ϊXΘ<Μ€Φ}ZΊΪΠν‡E¬ΝCk1ΕωV­9/]%Ώόy~ηΑ!Ε~j΅a2#4zš¦¬oώ—Ό.`Bβ.€μFωΰ51p“όΨvΒΐπ½e$H‘ύV+{‹ύuΈQυI8„ϊΐE™UzbyεXΡu½6σή*ToΌ‚, +o*μ »WψŽμf ΚγT$»y²…s­τσ˜5)T!‡Ϊ;Ηΐλi½P iητ˜β²―F_₯ρHΜ‘ 44[ϋσ›:Š2”αk[ € ψνn”ZŽSD<~ΛκΧkρoώ€wΦ.LP΅έMž7ώ7ο-6ΚFνݍ^’Ϊ ŸΦΡ`±ό‹ή­ψΠΎG“θΫνq μ=ZzŽ •»Ξ¦’|s³ΩT)Ο0ΞzΦΐ³Qr•p]’άšξΑdΑf™«ˆΌ΄°h­ώΠλ’::fャX°)›‡ͺ”QΞ½;Ψ»4)? +ιPΡ[-4΅kŽγΑ;nΫ–ό˜}9…Ά9VH#¨WΘ` e£šZΘΤΔ₯_‰‘!§wΏθϊLˆ /α@iΏ¬η“Y΅D]WGr~…eΤU‘ΨΊ‡2Ά>υk²†³ o;ŸfPκ *΅±‘ψ΄ΚI kι¦Ϋ)”΅x§5Ωgc ©\½h‹?ζ_Λ _£Κ'εΆH»Ξϋ)‡Ή-έ²EVoPφύͺιcd—…φ—sΈ}ͺΏ Ϋa^JκΩ§Τ%RώΠM€wΧm”%―‰χ|jM.­―ύ]t™]f¦PrN:Εΐ$ε!ρ™Ψα­ξ)-;9a~¦Ϋ›xΣW‘ΜΗ4f:fC²ŠΙκEί7/\†I’-pX{OλΠξ髈³vPρ&Ξ2šF?}ζXVq΅Έ»λ1ώ ξ| ό5fΥτ8꽓1HαZ‰f$‘€bό­χς%΅—†k‹υ»>"ήš92i‘ŠŠ―αΌ}dΕΦΫ¬sΨ΅ZOΫ-T₯$>0S‘Ψ’ +ŒjF³v‡γτna«:Hμ:ξ&+ήǐ1:–²ΜγΓ BŸ‰PΟγ|Ύ›GξβΉNΌ²ώ"d4ΨO‘£¦– °šRjΝ£f—CίzΥkΪκρι%Ξk†ηWš`,Ξ`Ϊ…°q͊Qεέ"/ ₯¦Δύ LdεUμ9s=cI©uq(lθΈΥύ–‚AΔ-nŒρΔHšΧΓΘ χ~ο7 ς”.—ήAϊᦋ†AΓ”φGp,ΈtL[ΣΡλ‹4ΒΧ%l‹.„Ξz)—ώέ—χkNX*4’~]μ|sfj?ά…€7w{εk‘f;|οq>₯ξΧκ~ΑπH”!£ύ“ΪqŠκΨπεΌ‘IΒek)Ώˆb"C1§‘Ίt―‡”"Cvc7RrΠ|ζX£ΉnG΅#ϋWΕ¬Εt~:΅.t[ZُτEŸΝ―œήδξβH ͺν½m5;Sο}[”ςVι³5wΤΖ«π™½—#ιOΧyt}<}!',λΙ0μLx«žΪΡ·ο—]ΉςΆgyIqe°ΔΙFλΏ«K'³ΡyΆ_ ΏΫ˜Άbq‰z­Α΄˜±μž7όz‹NχwŠ>ς, 9ι/²·Œ’κΖ4dΑ矐 …ήͺŠΜp1άπGK WœkˆUœΚ{k'ΩΡ’" ‘ΰ:UΉ€{έΣ4ьžhg‡΄ύ ρ8°χƒ:ςVεΕjΚIΙRή@ώ*΅9ƒ /±Υςrψ γ*Π½HΡz—'˜ Ζͺ¨υΨ5(Μ*&Ϋξα‘u¦θλVBοd/wκB(Kβ VG1=2jΦWǘΟ\VΕ©ΧΌ‘paŸ¬\ί°`—«0ŠcΖ3μΏW/NΝlΏΝ +R½ς%B¬5q;ψ”ςμ4±—jΚ_‘J£σBΆkμ[c %Fρ‰  +ΌΈ5Ns ΞΛγϋ(u=z"užϋ²ΑΔ&΄Ώ…ΖoεΝί,ί0”ΧΙ,›5¨³:§vΙVπl ο’ Ο$P' ·_΄Σw{9˜SqΫ¦»Ur%Z'ΓΐηMAγΒ†&΅ v Μ“σ]‰ηφ…&©!Ή%+ΊΩα +‰•];Fœ™τφΌŒŒoη°M€ŸΠd-‘ψΥCnpYα*O—UοΚD%t)ΏΑ»υΔV%³½Έ˜4FDvQΦΦε"΄cNυŽT΄‚.7ΠλΰSμSsZB*βψυΜ(%‘―τŒCŒ4²,hΤ–ΛΆZPξEδΟΥƒξ[΅φSΈο5¨ί|²,ΧuDQ”AησBψe0Χ1•‡UΫ?°€jξ5q.½―I~4T“.^_ †] +°’SFδŽy!5, ωΑΡ"bƒΨkο^GnΥκF6+g[\Βν€ίžσ,}Q2ζςΝ«ΗZΗy«Ρ« zE# K#΄fšΝƒbŠΗ¦Šm”4>―˜D„Β!DWΘ©Μ‰PŽ*6«OΛΠ£+_Ό»nΓH!AόλnΎ£ΓYζρι†Δχ‹‰¦U§€jK#;UhGΗοp’/½;dΨsa +ΖωsΉV—ω‚ψNCN$Δ§ϋκlνΞA*χcHεθB½΅ϋ~»g%”žΆΤΤ›S2ϜΒΔ2'QΏέF”κEι! ξφzΪςίΪθdΣ˜§+„ ™aUΏ{ΉνΑφ΄Ιڜ eιLuK~;»!Ω‘Μ`ΡC⿏ִ•θ6Kƒ’ϋœΓ‡λœΧf€q<QξoM§x©Σ%$ΖH·aή\ΌVΒƒΛΖΆΗακαΈώfΟχΚ_rw,bϊ†w Βέ–ΝΌRA±Α 5‘"]`A£τνA4”Άδ"ͺ[.ςΘNŠ΄2Rγi‡—6Bκ­sσMt ΐZw _wΕ)΅ w! "XικEΕBX+oVψϋ!CΪ<Ε/Qd§£ύΑ€~H»Τš„†aγΦ#Ά™Ζ©¨ξϊr2₯YοΚ‹Π…ž‘ϋ=›vρ~_'Ξ}Ά.|9œΛ7“cκ΅χό«θ†ϋ­ΚN$lAͺ{‘5¦`·OΡΆ―S€ΥHš·η›Wΐΰ*ι-o€»Π@L‘T}]†")Rl x 'ަ¬ϊ>Wά§οOxΰƒa`uŒζΉδo0”ΛΜς²Ν9©›ΓΣΑΕp±–ƒ£ΞKΜF~e–^Γ‚2ˆo++½Ξ°6(fRΈ|"Η} +φx Аν<ύ^`]~JΥcεp]f«ΓΖ…#κ2؟(,J dΫ$@Ια<ŽρcuMδ5viE&7ξhόφQ’،†χTθy;κŠ ψY­?’ΡεJžsμ1y…ά$aΓ^«rŽώe”ΣGΰ)JΣηf^i™ο„΄΄ °SΉ„• ωtP’ςˆΞ$ŠτΓ$σšž©›^Ÿ-8² :—iOgςž{¬r'Ο&Y2Μύ€έΏqΓ½%Δe.δŸχθΆG·rΪ3²~ ϊHRΰΜP΄U„’vΞ³eΏ£Λ' »ŒŽ8)r ώ,ϋ:PΪWEP7Ήε$ΣP¦‘œ'A–rΕΥΩ‘2KŠ„^Χ*τΊ1Ό}Ϊ‘xQ;bϊ9BDΝΦ29hJŸβ –a2?ΐό롏κΩμWρ ”΅qώdεξΚaΚV] ’wρ+¨^ ΌΣ υ³ΟσΨΞd©%<Έmsc¦ίe:z­>QauϋUΫ)K–eAΜfB΅y VŠΰ=5γ΅ δ^5e˜AρŸ»©γ„Žώ¦ •Y-0Ёz/Ar2*Μ1«ϋCπR}ΣΉKΜΤ$±ςqυαd’Ζ«RξfψŽΓ!@3Έ-fδ+Ι4γ‚Ε5σJα$Ε”μ t2ττ~Μ€έ}Α Αωάxγuξω#αIŠύ–ΎΚ"š7Σ+GΕ5MΧ•U~ˆ ‡ Ή9}‘ +ΏΊ– Ϊ l\o£ή4Ν? Β~§P™~V»m`aqjM‹Qέ–ΧΑμϋΩρ3Ό^DΩ Ή—Τ‘ψ|z%&ΝΰΥd)ώψŸGwΫΌ¦<”£ΉΫmΧθγ†氚Wένω27p. ŠήγωΫj‘Aυ#Ίε±nΓZ±·χ@aή̟ް ³gCψε+ΓvVόΠ³ngρΫbβ—νΩρ©α™τκ:fΰ·DσΥkγΓβJvΕ€x‡)Έ[ό°Τ‰8?+ˆΘc3αšΤE_δ·©RŽ!Φφ™4~λif‹η&fΠ.C­Εgή·TΑίΞ_Ε»U<›XkΫ4ΨΩ/WZΑ„’}ΨyXΥΣΓγίζOβξΊ…Ό¨W>¬5ΈcΈΰ€{λΌiΰ½Hο+?\$vkλ7jΓ °5μ‹ΔΤ–σ%mY’q» Aμόό{Iσζ£/ e”ΟkΓΌp½—‚΄fl*•Λ7ότΛΔI"_κά™έhΛσb_δLJΌΰ±zΧQwƒ­Z!)‰ͺ‘΅XV‹Σ»'"›ϊsBαEδέEn‘χ’ΘJ₯ΫοHψ΅ +ƒ +ΨE¦β ]NTΖ"ΉbT|NΪΨsθ$2Aa»Zan³“ς4›7ΖaγOΈc’³ιφO,DΖά³ώ +₯@yP ψΏΝ|'ώ°Ύ_ΔπισpξUϋŸWώR4?Yux«υΰΦμΘαTM§·ŸJnφύp ŒJ + +[ΒΎ«zr…Ÿ­―Eυqœg%fδΌκΫiΞ5 ΥΧ"ό;p^\¦Α Ϊ%'θ՚―m:7†~γ$χ50σξδp‘§{ˆ.σ‘@’γ‰ώ1½(―!]uš‡ΦΞΕΌŒ’Μ°Χ!ΎφΖN [β₯Q¦΄• +IΏ}wG²Ψ/ζΓ"čtέ…θ:ΕVtk.ŒBGg „QŠOί;>yΡ^Ά!9Ό©ΎFΐˆ}#ΛμΣΫ‰k[&Γ™I*m[“†“}/ψδ4λΛyc6v—γC£ρͺ΅λk»XΎκξI‘ΝN;κ(Z¬=w ¨‹…(7Hχ —R›œΔ,Aό΄θOaΓλ’`΄ œΐΕ"΄Μ}ΫΑΘEOkΆΤ{Ɛ7€Λϊ¦nΉι8Ω*Υ €Δ†€Ο;μ8l tqτΌ°UΫΑΙώšΌ81Το©,ξNψΈοΥ(ym ΗΥ +endstream +endobj +851 0 obj +<< /Filter /FlateDecode /Length1 1373 /Length2 27162 /Length3 0 /Length 28127 >> +stream +xڌ·”dέΆ5˜vf₯Θ¬΄mΫΆmΫΆ*JΫΆmΫ6*mv}χήχΏ~―{Œξcœ8{­Ή΄χœηDΚ+ΡΫšˆΪΩ:Σ0Πs„d”4ΨττL΄ττŒ0$$ΚΞΦ&1Шš8:YΨΩrώίBŽ&ΞmΒΞq2vΆIk€•““žΐHOΟρ_@;GN€°«…1@† igkβC"dgοαhafξό·Μέȍ( lΤ +ؘ8ZΨd œΝMlώV42°(ΩY˜8{όδάζΞΞφœttnnn΄6N΄vŽfΌΤ7 gs€’‰“‰£«‰1ΰŸ²6&žŒ† lnατo»’©³›£ ΰ―ΑΪΒΘΔΦιo„‹­±‰#ΰoq€’„4@ΞήΔφί`ι¨Ω-ΓIχŸθYΨώ+ΨΐΘΘΞΖήΐΦΓΒΦ `jam•¦uvw¦Ψ4°v²ϋoΰj`am`ψπ―Ξ ’ +ƒΏώg<'#G {g'Z' λF€ϋ'Νί]±5²³±1±uv‚ω§?a G£ΏΫξAχο“΅²΅s³υϊΟΒΤΒΦΨτŸ!Œ]μιTl-\L$„ωk‚ωo›™‰3€…žƒ••™`β0q72§ϋ'½²‡½ΙΏœ ˜Nΰγeog0ύ;„‰…©Ιί//'W€³£‹‰Χέρ?W0 c #g€‘‰™…-Μgk61ύχϊοα;ZΈ΄θr@Οηάιό₯—±­΅ΗΓuΎt²šrβRTžψψνά^4 ,γί ;#ΐ禑7°ψOτ+akjΰψw··ιΏ:vύΘ# +ΐΜ%kχ—΅&ς&Ή6= ½Ρί Γoͺ+δαdω"ωnHΤΕΪϊ_nςωn kώ’ΦΕω―dμώΚΐφCΥLώ-Zc ›ν•p6ψ+[³Ώd¦a`¦₯gώ·έΒIΤΒέΔXήΒΩΘόί”ω·]ε©Y[ؚΘΫ9YόσlωEOΏ|υedυχωατ——r™ό•Ο¬+bkdgόΞYXŽŽ0τιΔΘΒπbψ+Hcχ1@Gkkηό7πwF€©#Μ?ΗJ?μ0#·‘‹£γ_ύ‹ Χϊ_‚61q71‚Y[Ά3β +Ά¬ξ|©ΐq£9š‚ΨΩ뎈Σ +gq&]Ιυ2—†ό-6ο ¨o\…9‘*ΏZ:·μCyυ2νέ—°"žγ $2t xC“7φzίgf‚ol‡m6Γ–q€t―€χC_ L‹,Qmω#„­Ÿι€ϊΈπ³ΫιD“γ`(~B$LΑqŽΘξΎ‰ $ήsΧΠFŒEvΤJsQ–P™ω‰²ˆ}μŒΖzυ8ЊMίΪΝJeΊΛ—¨a­ +EΑBυ”$̞HΑB‹Ο”lq"dy*KŽ#Β*² %έ΅©_dΐΕ%ŽΔ’·”ΰZNX„'8Κύœiμθœ›d$ε&όπτ€Θ%α™BθtTN–31e³¨"7:σ(ο>k<ΪYIvφΏ0РΊTΉΑœψΣO.”³­εp#Ž +χΉ{—gίΑc1­C PΝ"<§Ή΄!]η­ΐΊe]Έd~¬-„υ SˆΘϊVύxε·ΨνΕ pΐ² ;) χB-Ό† HδDkOFοM\ζ.|b€΅ŒΚeΟpέ>{GΣ°Yd©Η/ όzBΧn“ω°ρlz<.Z¬Vi—~SϋK—nσ~φN;_:Σ׎2”‹Η¨7œφψ a·b4HΧφQΉΏp=¬κ m#˜ςψ] €[λNΈΨ7φΟζ³l:­…tζՎKΟ.O‡έ9|jt ˆδ’ζ‰ΐυtΟσχ½/h`Γ4šwYρ_Θ‰θγΧΘ%|HB”EQˆ[~@Λ₯ ΛΙΤ_dη΅.iwί|%δΘ(ρ­ ; ―tΔφ—!YNC›Ž3αJB―v€­h ΓS΅‚šψΓ±‘»’8ͺE?(ΔΗ^ ’qψzAϋΉV  bγŠ-[Aκσ«Ω#‘ϊ[ΘΘ=yι}±ΓψAΩζ?³«:ykǝΑ+nͺ'Τ]y–;²&)ӌ Zρ‘nGφχ »ΏςΝ©wͺBg<β‡I~ζΏΆΉν½Μω3ϊΑ8ί²:Ψό”γ”΄„ΨJΘ +4zԜφk^ fο”w-€’ΉQΥ"Η:5`Ωf15I‘^ŒΩ¨λ`Τs7;1 +Υ-•^ςήΦƒf4Ίμ +`ΘΫ$³yΕ\ Z‚ΖSƒΙk°ψJΙχ”m˜Žγ ΘmΖλ Έγ€1z.LXΓυm²ζn»ϋΘn֍d_¬O¦Ήjθ†ή†ΠށΌ~Ή΅oLΞXΎζϊm€DΜτΦυXβƒΜœκΎϋΈΣχ=TSŒTF^“–dΐoτGqrΓ–Υή|υ·&–©§η_€Τ8,κΦqΈΖk³€]€Γέψ$bOu(XτρΤέν=ƒιmΟs{0|V‚$ͺ ‚skSί'’π]:©\π¨&jjJrρώTω[‹ˆ·‰—·V;ƒΙΐ`tί_nNa»ΈI»<<"± ­Aœ…ypω” +γU»Ό–jo! (ς₯ΕKΘ{±ό­+eT\{“.ΗLž?r¦nΧΆD‰ΐε E7XΓ{–DOƒΚΞΔ ΜΚΝM<˜ή―AύϊΩΕYΦp!lϋ(κΜω|ΣΉ3’$Β*†…Γ— ²αBt‚x,ΎΕ}„f–&αq»αΩPΘχo'rk&…HΏCHχuS6Ϋυ]±ΐ‚){Τ)Η`† UΔzφ΅H›¬^Ϊm-ξηΆOψυŽaJιm6_ΥB§œœ^@]nνXLtφx&ΖΣͺπˆ±?9O'ΰΞΟ8ί‡#εP!gƒ=“—„Λκ¬"dάgψΙηύ6P€W±’έˆSJnθΜ„LӍ—*χ¦Ϊq+v³¦Α&gBBSUΫΞ%SލƒbUͺ4‚·΄2$—Ί€° £³)₯SΩkφC +°ψιcjSFΡήΏΦ7Μ ½GkEλτ•χެ“Q~₯0¦eΝgt¬Δ ‡έTύŒ$˜ΔΟ;s7ΰ‡ΛΆ΅σΚ^\@>4xv}W·ˆ« ±"ΞE΅€η$JΪ{“ωγτ#‹Šχηρo™IuΦ*‘IB€xρ¨3RA#ͺZ\'†kςφμΊpE8yΝR₯‰θ9pX°q΄kŠ0/κΉΓϊߘ›gΝiΥ Έ ₯e󧹁i§7@ ±ώΚ[θ(UN9`ζvΈY‰ͺ?χί P-WFˆγΎ’1@I`LzRβ-2~m1W)₯U’Έ+Ιά‹Mγ<φQ…―t"$΅cΣΖΠ;fΘ¨b +›+xΞvƒp±=RŸΖΊm2‹ρ7aΆ{sΌq!ΦΩ‹&«2YV fs­Θ±€S± aό=€VΫΓΓz£4‰±'ϊ‡Δoݘΰ4£‘R”p†ηΗ§4AύΗNr₯‡aa¨Ϊ.‘Ε›§S·ΤΣ° ] …<ΖWξ¨?z&ΉΙˆTώLΝδgΪ^ŠΊ3*ρ)S‹΄GαΊYρ‚zΑ+hΧσΦςŒ£Ψ±VΤϊυΑ½―Ϊνψ"60“1χbu˜p†³Ά^hmΡ·oP]YNdόdqμBNρo?FςΘkUΖυNY(^αιcŒ —ί²l!`œΎ$#ΏΌ™Ώx ι–vΤ·)ˆO%–Rχi`ν04¬|³HDΰγ9ί§‘ωAŠgHI/sζjRB’™LΩs–­ίΌƒ>d3hΞ¨ιΛD_mAπΠ2Θ4žΔάζ.\ τ(cΎ ρΤΑ«έͺx4x?΅Ή Φ†&γΰ)gδN8ŸΟ‘ϋRZΊ+<¬βNrΥυKΐ†S#Υρ:λ )ΌR ]Œ_ν2Ύ!ΝXΡόηΡχ†B(4¦yYg /ΤαuΌUΗmž Α +=΄y~ĝVVτΫΕΟbWπ?,c Ϊ…ςˆtΨ«°l’Υ©ύξ˜EΈET.³ oΌ§t€cdξ&Mπ)LgQ|ƒ0 Σg‰γ£=θ^h.¦P6–xΔΉ¦ΥΡ}X΄Y;τ«€~3ΝδύiνthE―‰+ LλfAηh{ΌX‘¨΅ΩφU«?ΜΧΒtT$ΒΘ«ώ:ցG―Τώ°έκ`4A"$½=EΏhŽ ™ΥΆξψτΏΊxΚπZp$>ŽΉϋQ΅•ΙΞ7±|Š-©‘j+PΚυ‹ŠΕƒ²1zLœ•‘Χ3?κΞ“Σͺζ ³•ΧT qΘ¨Δ’ΒΪ…ϋ oΥι(LpΗΊš›έQ„Οy¬|…ώQO"βs0%"ΥžB˜€ΒΥ9=#¨χFJlNy³ŽΊϊ^ΰFΕΙRάv&%t˜βND H£,ΛΨ~λε‚ήew±i2he˜£–½’8ΓζΥτδ΄ͺSM…`aοsΥ)Ίݐ‡‰}Πb8Υ@s3Ž%†^³e7πmΧΙ +’ Ψ‹‘že9|=N/ήhΈ1ΗσΊΓόό·tP*;eo~ΐjQ(m ΏRς·ξ ξTΪ wWvJξSζ\›±‘3 zί ‚Š;Αgοΐ»Šk?SΞ“··Q7Ι|hxΏ7*½(~Ši΅ƒ˜5=RΤ׌OύιΝλαΡΕ|՝dž9~ίΫΏ_pλ8μbOΐ’Ο²uD]kθ-ς»yŠ 9μBΩkž%Ϊήχ—•³κ†Ϋb8Κ£\τVΡ©)f‚:VS“ΜγΞΞ;DΌˆ\!p₯?RlilΨv΄όxbE`”8°! ήΝcMϋʟE8–~™―Ÿ;³ 5³JΐJζύ{iυ •Χ¨aΫ\Ν£†υfƒ—™r™%0|Ξ\u½OΔz‰Ήϊ"9ϋίE,½y₯π‘‚$Γ«₯ο:ηš„Ϋh«λ\ΧOΏάl$°Ϊ©(f,yHΓyYt’β!/ +ΡΘ('"₯Ε ΒΣπzrp ΅μͺ™ZιHΪƒ½P.mw!Q‰Σ),“ω@‡%γ¦MΣ +Όύh!κ'Υ·«k``τπb ₯γΰ,-Γ…Κ€ už^ΝΓI‹ˆχL*sm βΌΨ±bTΚΣυAϋό£ŸέS/#󽏷΄«σΓ#AΙβάD’ {KτΫέ^α1™ΊΩBFΆτφδΛ(χ7κέŽ™ZΓŠT7ώβmWώχΉΚ`•œ#ήιM$ΝΛuΎ3LΆΡ0Ι―5PE>6yδ Ξ#ζΟτ&Eπ(h™WΦΐΒ5όEO Μ †E]0rΎΎύ&Y,I ·Π돚ΨyΊ&₯γǝCSπ,¨»Δj˜K$ίγψG²Tοέ–dY"”k_ΈQ#ZBӝx@(εΪkήAμœ/έ·α0O?]1Ά%―/*„°ΩωDζΔ€n‘ΎΙ«>:±σ-Θ|VώγΦ3—IπΖ"ν°φΝ8PΨΤ-œU/!%(‹+βι}6jϋZŠla‘όf½”Υ‡θ“8έ֞ΰU αΫλΪΆQ¦]&ΜΚo+!V™ώͺΩƐΎjO,Ϊώώ·(ύG‘·œ―~+S,Δ6CiRήΒ@pŸθλχ ]ΰΞ†ΓZƒΨ±UΠΉtϊKU9qΞ¨έhKΙΌΎHΈ6«9·ψ”PΥΐ TažεΧxΰη~‰‰rο‘ιΰAΔ–±•`ΑAοΧΫ°φχ!ΰ’$ΔDψ‡βšT“sΙΝ“6*Œ¬ ,Ά˜@λ8Ν<™ωϋ’υ6ζ>εDωLϋΪΜl™ι2ΕήΪ”AHΣΛpΘ ͺŽδψξh†£δžΚ·ιŠΐ „)·ӟ`%υΣ‘{p>ΚΆiΥΩΔκ#ς~ΐέ‰ ΩμAτ6:³4‘ &aJŒΡv%mΦω.Zήτ`–Ϊέ›άU οYαάZυΰ@ι—όπnwƒμ7v™=›HώΠλ=δΔ)ΜxNϋΌv^ζšrΪtψD|•ύš+ΙΏnΧΤΓ‘Ι#υ•ίzφχΖSK ˆ Gψ± Ψύ•ˆ•NφhT +;H`.",{ί +δƒT=R5QQ‘Ξΐ–Q\χz#}>―_ΧΘQπœγΪ±Ξ‹ωRΒ-τN7 °jΌ#ƒ«`—Μ›1B6ΌΦΫͺ%ξλiV.υέλT…?*IAυν―aq©ξχƒuϋΨ–r4²,•―ΰΐͺu’bέY]Ι=—_%Ι‘”~RοƒaΪ#V‡΄΅αχπ¬ Υ”ŒΌΏ~iι N₯]½+†UFδͺW;We>vέj9ώ–ΒU hΣcn^Rœ‡υ*H$$˜%aBήB§ΕςفY‘§ +žΨN>HzΆX ΐχπ] +jγ–akς_ ΅Nκ³8#ΔδtδΔ9ι“νΣ5Θ4Iαa ρKΉœΉ΄ςΠ—iH™±΄€δ‡υSζ£]saU Μ°žC4)@‹’YΖΤPίά™|X#τ.\Ύ jefΗe:Β[’ΏvΩ|vΤ GΑΓΑdΝ nΖƎ³°ddήδ/σ)€ί•Τέ %υyCƒΌεq·Χ‘>vl°Πƒ›Š9ΖC+]­=“‰8D¦@ω…ΕMΏΥΟχVXPύ*R;ΜςI+¦.^ƒEλΞhπ4όΛ—9žˆ@)°.˜Ί+&ώfYο1ͺmeΑ¨³,Εγ›• x&Ήε[ΑVλώ9―c'k"”ι[†g6ΐΑ΄Υψ<Ά^ΌΙα ςwœB¨l₯¨B†pLUΚM ₯Eχ ΟY HΠ¦(ϋ%e}κ. LŽ Hh―VΙTž[‹x`έΩΒ(DIR]r‹Q|’ȁ#ˆ6³³ο +šcϋVƒ2Όικς}˜4μ³PβΟ9σί0n‚χP?†*ŒΣgV½_½²€F•XςμΆ»t_dρΙ—œΨ˜„ΟgoΠLΜTιXΧχŒέU±η}3BCύβž–Ή +™δχ„κΉνœž¨ΌyΤυ·3Κζί΄ DΌkllά΄%ςp<λ;5›6YΫ+θ―4\ £(AfΜ,RθbίΘΒWĎ:ΊΎ6RŸΟ€i:χœ*C-ΌΪ…š*8€ σ{4 ―#‘—T@ͺάΒMόE8’Ήρ +M=δΉ) ‡}Hv»ϊ1† £†$π3Οxρ€NϋP—·Πˆs 3^ιaͺR„όμλμΟYτ2q¬³Ο-7>Œ8ψ ΌxΒ£EƒΧ”6εf1αΆLqd?€΅ΐ² Ϊ§ +1NΎϊΦR„ΆΐΏΈ‘dρE½+·rŒ]J U0`ΩξΑ³X-θƒ’YΗ½οr°όΈ―ϊbΙζΆ‹5ΰxj™Χ6•vββZΞk-­ζV„+/λu$ΆγΒψύ•tυmΟ΅η9'Ρ`^TΘs)Ρ+Ω‹³u―ψ‘Ω›y¬ŽAՌƒ”UMƒ$:Έ¬ΘπΰY 5/<Κ ‚±oVuηU«qηΓψΜϋvΕz½u;,•z£`ώh MᐐU"z.RΓœΏrtκ&½°G!„ΊάY°άο Γ ’’ή#%)ρ¬RKw1Νk;ΈΒ.™υ+u±D +ϊΨΚ5MΧ+ dŽ4ώ6Ι³B>ˆ Ί¨…θ­βωQ6Rχ⁻ΐΉ~oŠσnI•ϋmŒξ°‚{Fϊ—Ώn[QΚ(ωQτ σ9!μΝδΒ5pΤ‹4šŸV΄j”Ύq‹dΣΑΕήG[2šΠ‘c`WΜ…$ηΊI3鄝™ 1{lymd°ρkΝjƒX5`»±ΊΖ2Y »(&G;]ψ$­»©ς*.M巘yώd©ύkIΞΔEYΉΗ©7}¨ς"bνiύ: œ˜MΉΈŸdΒΤ•‚C pΈΈηφ@OάEβ((ͺπ:Έšΐ*Q£Y嚺™/tβHεBλΚdιBbGtΪƒ—-œάzέΚ–SΜΙjΙ­M7Έ&JϋζΕΰϋ!΅±\ο›ΠgjΏ¨ΎTΔGCϋγΐkϋ΄π„K·MJ¨˜­–§*Β'Bj(²±qθuˆ―Ύ₯Έƒ€/~ϋu~΅4όŒΪƒ,‹¦Q>ͺ§œ<ΦOMτηΣXΖ'…u„r!RHœΎθŸ_."ΪB°θPuQXfΘ©f·LμpδU&4ΊIέ©ΰ6„Ζ+ωmC›K±ς6Σ΄;4}Βσ ΉγUΆΥ7z”GŠ’*υΗ+ DͺΉ Π`[Θicužπ"Έ8`Π>ƒT‚yωΆšͺxpp₯Uό™_’›|ώβ–ͺ^6‰~Ωƒ­@΄j ›5‘υάΦ$ΜxOzΛ^•t—°©\”ΎΊ[TΥύΔ* +ύθ‹Έ~½‘±-Sj€τ8xϋΞ‚ž£Vž•WΨΣτΊt?Θ<…„ƒ γΰš·ŽόL"Ηϋ’0N!μFΒ!Ε!‡:ΰLˆίΜͺŒ.8¬t%α3(εœσκ~iC₯ύΨO#$#~ΜHj²9ϋb›κΨ γ]wκ#Φβ":*ΪCžq=sxβEf1Άλ±=MJ˜3­ˆλ‘ˆΟ9²ΛΌQΐ°θ―ό䦬Ζ1Q­o¬{νSι΅ΔΡ[¨§Ÿ.cΊ)Np)SιιΕΏ΅ΐ1ξYM(έϋ°Δ…Tέ2˜Ό‰Qc.ly€ΫuŸ;‡ΐHαŸ?bͺV>Ÿ§­~Γτ—ΎZŸνCTT\4Ÿ|vΊητξ™Π3« ’ΫtWSόI(­MŸk˜Κb‰~”$lx›_―­χΓ™¬Ρ’3Ώ€£+λ]sΝΒEL€SΑξ^LσI?+oPhe«c€½ΈΩ­ +θ‘π(ιΨΕωρšϋ΅£κΚ^έ("ΡnŸh•ΏjP:λ’§°­WΔςJdO ξφzζΑk¦_Ο›x+π£Δ˜|―_EΛΓ(#§;(ΟssNΘ&Ί·Ϋχ†E`=#:=χ'5±Vh£|ŽΜ‘6 +MG–η5xΝ΄Fš˜eh£šΛή{θΑ³3τ΅Ο|%•βτΖM–όΫ΅λ%Y$·€iύy.ΔΓώ<Νm!1ΜEβˆZmC[μπ€ε“B:–Ο%΄Ό9°ιοdš΄ +ΘNξ"<3–§ +zηφ\Η:Ά(#KΎš³Ei0Χ93XέχmΓήΰiŸ¬UΥηΩ½1Ό‹Ω…₯U 5(c=βύςš†3΄’w|­&- §yvρy§F…Έ₯5›Œ5«9œ©+²Rμ¬9Ÿϊ'ͺ/B`«¨y-Zπ!g˜§|ςτ»·ߞR¨ΏΟ₯±«Ε Η’e?Ρ|Ο=Šγ\/CwL9άχΘ’»ΦN¦:°»nι›άeδuFI+©uΖΨ(|ΒC,2|0άK"χk>8ώ™4ΏόASiκ‚ϊ±~ϊΣ"άVf«?'υ»υŠϋ’ĝΛξ Κ~gR‹*²ζZ +X&,fBπΥ*―rΟ«b¦ΕP 7Š₯«^ƒKΪοOΈ_π4ˆ‘l΅7Ρ^τŸ„2v)6²‡ΕΟhυ’Ο₯Γuw-π3LήώΕ1#βν>γZ3wθ6VπΥ·&.£’ζdξ*ΉΑŒ`BC¦2O=-bέߜ-9@“ωΡ#O₯ΰς₯ΏKσt (_κ¨ΉιTM3’Nήpή€‡ύ61j-―ψ„ CYφό½δ3 +Ι>ΧE.hζ +QT°jFOΆϋqœΞE=`K₯©† ½0dMdCΑw9Q‰’°9EΫRσ]†ŽIΈέτ#E]¦ςΛξl\ΝϋX­9!vEeͺKΪΨΰζ5dmΰΏκ§aλ lΔ@!ΰ—Naπϋ\ς„Fξ'.Ϋ²ΩΜ7Ϋ•ψΎ5±yrΟ}²Θi]‡_­ε$Έ€V +Ό.:Ϋ·­%cΤ’»{τ= ΏfΘK=ΫψΎ ΦνJ™_Ž1μTgšκ~?ž»Q +Žρz~Άε‘\»ΗI€|ϊφoυίh]ς D2½b7σΈŽκwBρ<ΜΤΘξŒύ³.Ι*‹Ÿ•“ G°y1 Γ ŒςΡ%9Ύ΄­Ναέ¨–šμƒαέaπΡΛŸNWͺži±[­9­'‚‘|4ΉδY|.«MβGiΘ;&… +‰ςn“lM ή_ƒ-ηL‘©ΐvKψŸR _Ε‚vϋΘ ο›Χ`>άu…Μf­’!|ΡηΑέ§o±nb3{€a%x#Τ;¦Ύ³Ω’σ…ηΙ‰ό.-y›όJςJTVΤaŠ…A„+J Ό'ž" ͺtY’>άϊb˜Œ™Μσ@ΖΓαΑ ^φ ₯?5 ͺw[Œ—ϊϝΉ‘V©&θ@oΊšωš°η–t‚Wšͺ]œϊs+ΦAvŸ”I/1…•šz\.¬;μ†ζy‹σΨkΙτΒό†ώΟΎ_ηh³₯œDΐ4Ο;L§ΨΉ±ΣΊs΅Oπρ8o―ζX«mδ’[$Μή¬ @ ‰`)‹›>ί—¨gœ€±―§HΌ‘²D­ϊ1ΏζKΥΟΰ›8>ƒ3£7Z™Dεξ*ΰ tjbεΟρεΓ€—€½6δm€£]]ll^˜%λ]γ‰ί { \Λ–Ζ6…iE‘²=ΑΛάΞΝΘ§Vͺα©|šϊ[€Nmνx§G)}§H A“ž©“§χ·‡αυcΔΪ±cΏw’υ”–­Γ„7ΤήOΗΈΩA—BSΝ‘0ΔN`Ό–έΪΓAƒ‘(3(Ξ9σζ φξyn΅ΐ|zό΅ 3yχRθρΌ0žΜ").ΈΘ΅ψ’δ‘ε%+GŸ“žΏψ)Ϋv―ΤKw’Ÿ•:¨ΨζaόV.Ω‹  ηΣh„CΉTΥυ(xτ‰_ΐ"όw"iΕήNϊϊΤN€`l2σvP&κf{]ξIX]eΊ‹έF›\ΛjV„½vΫΉ%©ΉΎν²ί~ο{CdE3΅‹΄QΠ6n©KΰτΗ‡Μg4±”(ώ³uLZξb‘ί†Μ aΙθτΤϋkIIΪ>{P”d:LXm‹ŒΒΣ O›ΚbέR^„ΐ7™ άƒίΰeWΓZ:‹³m«ω`ψ*C(tλu'’δ>?~wlf° ͺκp,θΖ“ά/±΅*–Φ¬R’Mλ lo<ˆ2™&@ϋ`jœ„2Β4Yυ|L"G#;G¬©θ"duϋfή"ρΛΖvΎ‚ͺ#–β0βΉΣν–‹H%ώyL±² +°J}ηo―OθW4šΟ’%‘gG`8)™ΤιŽ€₯ +OS‰H˜Ξ’ΐEL"ΒqΈtώ₯γvΡ₯M0`Žλψz ψΌ‡‡ +Σnεƒ0Z³εdE²er·w€iΚ9‡‚&Κ φBr䆁)ήCH%]ƒQXš#Βϊ%Oh|ηUΠΜd΄9Η=έΏ: |"ύ3«τ*‘C. .›λ΅rv«Ε HE "œ,'Iέ‹^‘T&­Άlό'q6ΌΧžνqšF±b$D=ΐDΊ[σšMΕ2β‰Oση0ηΖΕΠ +νΔΫΧ–6(½ιΠ uΩ2T]βΉυŠO’'Kρχ΄]aWnρ­ +€hίDτM ©‹t:ρn _ sδΪοκΉ‘‚ŸŠΖ 7γΞ..ΊQϊ„‰Ζ*•‰ώεžBΔοž“?^<‹o‚Ε#Pή―…τbd€Td_ίuΒyέdl>C¨2₯³΅ιq%Φ^©κVνB†£Ω,‚{Uϊ%6Ζ’υβ&ηΟ!―SŸY1κM8QΕ&ΊρWΧΗC㈠vnΕ.ͺΌΑ¬_e§]°·,R~ δMVlΥ²Y§θžQτ”!#ιk˜RLnlηΆ–ψE<ϋ΅&χ:¨ˆΈΉbj©wž―Ε+cί'b'Ϋbk‹B_jSύ‘΄^/Zέϋξh«γ‚ΥήٞΚΚ‘­ŸΑ IΰϊzζΌȊ Ώ©+Ÿ9f6;¨Š.υχωmψq¦#k>Έ\qX¨05b΅D&€ύq«jcDη$3“ eΒη(Ώ4 "όΐp낃ΫΕBVΤ;’ΝƒžάΆΜΑΆ―p#δԎ#:‘έΛΎ:»˜Ήεyαπ5e;ΰηγ›pώKόθ*΅ρeΗ±†ž@ΐ§·σμΨ΅ί«4rξQνw’Λπ4 ]{ΙδZΚ£pΘκP˜ω Δ l[˜ _ΗχΫ&ψι” +™€ +I]Θ|ΐό Ÿ­ —o­­Ν«ηαty d‰ }™0μύ½ρs©N™•ς‘5PWΈLΞ>:­&€ΎhωξrήΪP8Mϋ ŒwΦΔXχ–9½Θ©δŽ*w%8G …+NEk•Άπ£Fςέc ώρΩ™0O¦P#u΄£Υ–dn†’Ι€ LΫΉ£/=ΌίΙ’Ÿ‘‚!φ +U —‰δjx: Γ’ϋρi.Jοΰ‘ΐO‡κγn–‘EΕX•ΎΗφΡ_΄RΛ₯ζ [©ώfβOΆ]7―p\a+XoKRr<{|ΔjŽΞPV#8τ[kTŠΩ‘8Ρ ιΚ ΧΊΝD‡ι’Y ΛΝ½«ύΟkU΅&^Θ€qRψŽJdΏ}˜9'Eι2˜ m–Z+τ§Ϊ†¬ζυζ3¦lΰ]:¬5Ձ΄ϋZ„žό'=ΠΦό²]΄…Ξ.ͺΊ›žz­Κk*r@ΕΊΔ˜ΏLL΄ +@<Δ&τυ·DFxΪ&3Yζ­΅ωη±»t θάyzΰν`ӚT7‡ΖJπ²¬φ:”} ΥF},ΧΒΌ`ˆΡ©LαN!ΠK™^ŒsmyΗσΫU˜°bέ―ŽκC“u΄ΥξA‘α›Η€„΅œ8{ί!g‚š`.ψΪoKίΎ5,†–AΡχ]Ž‹QfνθΕύΖ©VΠΫΉ?›Šeσ-‹w‡ΟA FPΎ~gυGΫ O߈~)‰%XΧ­³yΟR8Ÿ²λ&όeƒΦ—nYΧΔβ*πΩΪ8]—YνβrVH,HƒšξΞuΆGηJzŒΤΟKΦ%&I‰p’Μ»’Wτ8ϋ†–ΣΑο…xιϊSvώ!’4Δ‡φΈ…“<€’^und©ߌΚΗ“jc”αήΧ•Dΰάσ>ˆh‡ΒοΜ1ρ—]„q„,—!»5·€λOϜ!Π 3/²vΣ‹ 1Br XΫΜ̝ɖΌ€_#ƒtu2€»B ͺΉΖΡϋ‘'=«Ιy—EPφ²»…±xθ—ΙπφΒ 3ωTρΛίa6L–τ#H%JΑjή,ς’²ZΈaX‘Ν’±¦PΏγ©X&λΠ&Ef{#0βX! ~n`γ˜lΆ=@/ٍΨτ £"…ϊ&+ͺKU]ςώ–hΧ‚_σK%Άl&)ιρΚι5ζΖhU D\ͺβΔLΚ,Š‚*‹aKζ«+4)Ν3'ۏQ:vΪ­pρ6μ‘ΩgφŠςΜ%8i#‰gΥJ'؟ωœίχι%δβm₯Ρ ζ‰{ Χ1ΑxΑΤ'K½€bη’;ȄšQgz₯έq]‘Π‡/bOl€t©ͺ +²;%–―"άσΥΠF.ΏκvΛ/;?~‹™₯h1ϊ<Z< §…1<[† ΧliΟ¨)τΛΆ-™¨ͺ +WD”yUBΙsΔ…aτ„"NTYΖΎν‘―OžΊAWj\Ψ΅ΛQΟΕ?Ϋ7φl%¬ΟΚ'Ν7ΆAΦ\l’Š…ADΰ‰_σŽο.¨-ώx€ΡύΕ΅,LŸ*±tΥ'`:iΘΛLs}ώδ&ȟO‰F„²7[΄^*ο,άJN’ΊžxΤpxjŽ­<Λ\:½_ΈΈ?²ΆΞCgΞύsWο 54έU(T“ΐίͺ,DrleΪD’ᆱ9j˚i>3₯&8ΐ!Fn/&6ύˆγΠ¦ΡΌk“£Ζ|Ώz}gΡƒ!(΅~ ²IθqlӏJ>gr§Μ³Ε‘cˆν,ΰμ‰>w2㦱ϊσλŽoΏyYζŒΝm[΄0>cνZά-)ˆΨΟ*ΞγNδ4©ψ’ι…ηL_νχψα8΅Qβ‹ΑƒΈ1c,Ψέ@ΞϊΡΔ2@Ν¦Γ³Šρ·/΄%ƒ„c8qκŒ5{-λγΞQΈΈ;ς‘…v6Œ{΅ΰ°()ŽŠ; ½ύΒdί±j’ΝάbΥσΟλD6Σ|Όϋq‘:‰Σ±.MψΆύŒŽΈΔΟΆtA^•ν8‘»H1Έσi½KφπSΈ§±Ξ?(ϊM{Χn„XΉαˆ;ΐaavΰ~d\Bx« ο˜-yS°ίΏ&gΎkiXytξeΒάίE;ΰˆ" +Ξ2φ³βyΪΞqσ-IΙU+Ίς+ˁυ^R΅Α¬€©Ύμί!]Εqι7fa›F ΕφϋgύΊΛ†V³ύσn²}¨έK4"Έƒͺυ΄XΝN#O.γMxˆ Yαα BJΥGΑΑu`ΞGdƒμx*ΕcλάΊG +Š_­ڌVΫ_zζ€²IsG$–ŸWlβ4‹3$bs/ –Vδ•cπhΛΙΨ砘–»–ƒη©>! [©΅ΐοόάσοkπ«ϊXdνυ—όFXΥό†Υ”Ή>²λ GϋΝVπΘA£“Ώ£xlθ&/ί;πh‰?€9CταuΉ²π\ΏbΌΙA·VΛWŸ^ Ί‹vfuΗu·»˜―Ξ»΄>™΄‘“ΆΧ΄),\ž]¨ζ—ΌzΉή4!6ŠΙΘU‘Υ/nx€°šηn—₯Gt΄ύ›ƒe§Άς૊šg]-6œhΕleFšηπlΙ―*œe‡3NΗͺ.uœIκUbη56Œ;D±b9 %ΩΪ€³‘ί?¨ρ¬ͺXηe©:΅†seg$Ab +υ…ι·Η±6n\_ Sξ’mŽ*™οίΐv?ΧIFͺJSϋ2ε“2t@{±g«Ηu_UΧiΒ!+Œ„} vœτΠγ«”ΣDξͺ’‘5[ ’7Z̚c+š9ov3W u0=gυd2σ(ύa „E…ΟWΜ‘Ε΄…Λyw₯;J}·ιιŸ‡¨hτ}H±Ώ›nžoΆ©W€άτd± {½iϊ₯2ΜLλ0{·j˜NΦΑ‘h†Tυ.όξb·Ν€φ1εoΕΛ’°Τ“]kN΅oχΘ«΄“Ή|Ή]•υR‘ΫVβ#.ŸΊ4#ig°2αρ³)аmͺ¦ΌXœχgτΠεL’άr΄”³‚·.Dxb„'*(T‘žœRΫ5Ξϋ³Πσkύ’`Ό +xœ<έ΄ZŠl›θ|·ι”ΆΜ―ΐ€Ω«O_AΌ¦…6 +Yδs&‘„|υ΄Ο¬8:Ž~wUHΒ6ΧUξό"–―7AN!ΪqυcδνΝtdΫ¬ΊΡ +Ά₯vΉkΕ΅ˆ9Qq]Α}ρ¬hk%ΡΦΙ‚eΜ<γ¦ΚΞε|RQΧͺϋŸQ_Ό}2άΟRl΅ +Xt7θ—Ί9|Ψγxˆ{Jž€h0 ΓDΟ•1³Y₯£¦Α0›O“BΖo„ ’f8ΞΰηeŠz­:―{pBυhH ʊΤkΛKωh5…ϋ<`o"ΦύτŠ ˆξΤΑ*—5§+ύŠΥIeœϊκ ό«”vβζ†1ΚΟPέIαͺ-yΠz¨4bp”Σ€‹„9hΕr₯=―©ΦM3eœ? »ωΌ`ΠΥαH|ͺ T{Ξ%Xk(­P…_UΕ醔¦nŒΦΌξN&HΕΚ#Ά[K?ΞwVkΈGχ}΄Έ½κilκύ»ϋΥ£—1m›λΤ›ΎCŽT,ύ]hμ€5?gŸ‚Ϊ…ͺΡΙuA3§eί ’‡ΣΕ"wBt +z„€ΊS +m2¨ψ{Υ#—AoΟΩ©ΚΘ}Θ(ΛCXIϋ#Ρv‚α›3=’Χζ-ψ:¦ήŠW*Πr²5ή¨$PE;Β™Τ– F‘ΑŸ€lW/Θa‹ˆ½Œ=G5—Γ…Ύ}ZΔ†}wΫΐ«aω$WQΑΘ1•ΑΑλb=ŸΎu™%‘ΝΥςΞ') KθΥήοΠψŠ)ž‹$_β2ŸšιΞ€` M€E‹ yφ64ϋ…"σ= ½nPβSΖŸjŸήόΥμδdΪ &’ŒˆΧ+Y—U‰*€Δ?ψ'qͺi„΄Ύ0Ο!ΝέJO_'[Oˆ ά’šϋm`)J:λ=ρΚHž9tW>&ΜΌ§rφΉ³›G"'€λ~Ώιx0š +tΗδy}"]:ΓΓΉ3°d—P~-8ζdS„j6θ}ax 0(XΊ0Δ‘;3sη―ΩΗ2Ξ¨ |K; ŒχΕ“†ηUνš pδ‚Τ_†λξ· Ώ#ͺ‰Y λMΚ"Ξ±ΥΛ88JΗΠμ?|Ώ£λ+ηΫΕW¨–Ώ{ΰΕήυ}ιΌ`ΔL§‰ε“λQ ™ίΰ²f:Ϊ1Χ›{bɍΒSqT&Ϊ?eΠižqw²}C£&ΰyΡΖ&y°ΒΣl³S‡³5o”ͺ$yΗd Wsω:›žυφωλ°.ϋ\UΤοS ύ<ξ|€ ΏgΞ½ς(κQˆΧΩ{ΟŠ—ΫvUD]zAig΅{»Λ+·E«yγw/²Z/ͺ1\τΈ’Y όΎr€k‘k«ρtƒJ’@o~+ΏeS1]”K\σ“Oτ͍[₯7r:ؾیα». _‰β˜Ζa·Οz ½ΠΞ‘$σ½΄φ―¨πŒ­QιΞ›ΒΙ‘ΔΩ―ΣκΟ²μΜ2±8£ΟέΓJμN9Ϊ–“7ύ[βέ ε_3uκψ Ω`°ΩλŠaΌΧχNSH„l½/’σˆLΩ̌νƒάsΘ¨ ΉŽΗš`B\_8,$’ΪT^DT˜τoA2‡~}m'F[€ς"ΌΠw2Ύ„Eζ“ŽζΒ7™ΏψYQω5–©7€8dO»ρa* Ρ}xΉ±ŽŠ‚8ψN£Ή~·σ ΰ KRuσ³οKVξ¦9ΥσŽ„})±.kβεKθ”~GχςQAc‚ΌΎή4$./’pAaMŸνώ³Eφ+xad%VnΕ(VϊΝ?@Φo§¨•ΕΌ—¬`j™Θη7Ώ=RSΌΛ\UH/ΊΌ­h·b³zΫ>ωQl]ΈΒΫL,‚p!z39Ξ©ώyΛΓό<γ™vkXΧώΑΝΖ'EΌm<¬M<§ΛEβ]7Pd ν;Ψ“MŠχkϋ# +[U<Œ³λ§Ιˆcϋn$­'ή†mΨJ–V¦­*σλυUΑΥn¬”ΐ I‹½-„CΥ‰KΪ­΅‘*σά‚_œRŒLŠμΏ+·Λ`κ„€?˜8?¨2ΆšΝ­Ί„AΘ$ͺ˜ϊΈΑ]NφΠi,§Ιr8»™=Εα½—“ɝYς[Ρ³l?ΪΝ?‚”Λn2PΐF<υ½b‘Ώi[K!U¨ή*ύA0pVͺ…yOΨ(r‘K’Υ”qg,υΓ1Ψ)³]ΆŽx™ΟVέηυβ9[DλsΩΝ‘TϊRεŠΈŸ NzΣ‰\6ΫΙζΥ&f―^HΣSθώX±*&1γjJ‰cPζ“ΓŒΪΞΜ{1Ζ*°u#yΛ|v1dOΑKσ»ΊγwΧκ›αpν,PdJηύ² W^α›Ίpdx”5•œΩ’L{llŒΡ׌ž₯Ξ ~πΓ1א«#TΔ=‹Β³θΩfwhώIΆG· +wΣΩΥ^„ΔLγ+Λu +ˆΈnžυΏK―θ]ό?6›ž"Θζ8ΨXP­ Η#€Ά#‰γΗ$¨MΜ{Cy[Ν'7·™Ζ¨#όΘM2ΪΝN½Ϋωψώ2…$όβϊ™‹¬ε y”T%2Ω²α©ψ~u¬Κb“…γΜΆΣΞ —VΛϊκΈς±“ŽT—ΑhβƒΠΖ€ΚΏΣχDεJι±wΒξ AΖiΡ-©vqO&τΎ“Νxl±‰ό—BΚΡΉh3K yΘ~»F>½Ρ&_ΛΩ;<―– €lπΑπxΠPώ]9πέϋ”«WeΝΣ,]/Ή¬λA’ήΉέ –³ΫΪ±ΡƜ€jxΌό…―W$p€¦CΗ^02œάΆ…)Da`τμ9SŽ6wΜ7Fi¨Ίc?R»δμ +Τ©χ +xFcT›ΩdΑηί΅.ΤMŒ–*ψY-E2©τ“}Œp‘ΧJ·†‡ιύ%†μ²11{%UOz@‚Χν|#ΉΑˆušνπ»X\W<ΫcWqOξη;~3ΐ{ΞHzχΡγHpρρ©XTε †#­κΜΏ3&4ϋΌfYZFڏݲΏmΫͺ–Ά£"©κσ›·“:.Iχ€±•‘›©ι|!χΉiŽ"Ύόr½œ‰•ΦQͺΥ§ Yω$F4·Q4’μ*Jφ•³EΌη€rίWΒΨl«€’©­-C4j%:Ό” +|LΖiΜ"yGΪ<2– nήI]HPLό Έθ; iϋ5nrώΞωμXν:―aœΒ?°H<θyΌΪžϊ°Ι6 ”Hu輙ׁlryzΌtΝS±GD±Œν.Ι§σΏYLZňͺ 99(ΟΆ­l$ας QμvE€₯S|Ιι”±ͺŠ₯ζ–‘W–XY•kΏN3’*ί•š±*^?3Ύ^/±½i€κ•KR¦xTͺΘ—κc°…•°εε$δ+γ,ςέEδσͺ½γ€ŒpΑβ†^αšω½νΨ?CjΜ—%ι0O 69δ³ΈtΠ=«†οh˜b€ίΔM†βfž9Μ4ZHΧ+ΨΖMμΈgH/0ΚΓDΙˆ†΄ΧBΩUΩα„ΥϋΌA`ά’Lv¨7ΊΤ ³Y)γΩζ¬χ_”ω0φάnš>ηr)κi£ε8q›αϊΔl―}t'F€°Λ˜$ή γnŒh–£cλυΊ€mΤPχίUmvχβΔ›ΣεUϊ6KχKΠ`šΰπOYχjna¦›Ϋ"Œ~Ώ|±ά΅Ϊ"7́G+iŸθΤ*}’e)ώnSβ*ͺI«—ΖXθ4χ‡Τ—v‘/#bsξlεCŒΎψ³δόΪL±ˆθΪ™­Β3μV‘ξYΆ·SΑψjšΝ•g€πYqͺπ5mZ<ξ—@Zޟ +‰c +΅Ι>;E»MVν{ˆˆέΦ‘•ΌΧΛπ~ζ³n­²΅RΉΥmμεπsΆρφ:+HΘ€}ΙΩIζ6yΙ€νί'†%hΑΔV6!ΕπΔ_7–Iΰ“Ώθ|™]p‘XΝQ4ύ aav7Δ C‰τ’τno>]ν;hqƒί‘-&³,f•ξτe”‘qfYo}Wtσ,U3Œ”Ά1ύH²­₯"ά9qKY˜ΊIց€LpV²"Σα²VKB’ˆdkψΙ|4θ<œΝ§Nw?ΪΑλ²)(σF³N5+φΑS½A²£Υ[D‡oΣόD̝ύόa£°«άΐoί_ζcβΗ*€‘^“2Σ¨΄ρžy2 ‘€{’b>ηπψϊώ7`­žΫ΄υτη‰Ϊa’΄)Ζφ‰ μnΨψ£|ΌΗ ή φ p=χgtΌUG^†Άƒ7ZΤ{©}@¨τtό +ΒΊ°-ιβ(5ΒEΊEšwR\Ο¨šΒx<α!QΞλ9N½ψξ5Ja«IΣΆduΣ”ƒ€JϋUR"―σBΪ§)`μυc/j} *pQ•V‰‘εΌ€:Ψ'ν—‰`ΐ8SuΓέ,οP ϊΚά+ο¦4h DuM4ΞBŒδ‘n†3ω{ΚgrΘΐ‹ρΖ2φtPΟΥΦ₯Ά9Θl<ζQ¨ γ‡y/•V…ΊƒrΠ @(­ \ λη½lqη‰Ιύ»}ω– ,U‡\Ÿ?Α>‘€Ÿ‹ϊ:MΈθšΚCŒ?R΄–Κmχ’ +₯V~[)Τ΄ fκM4¨Ύ€«o΄4―P9›dPYHw8Ψ-ε<΅Jή6ω·²)Z ΄0}οΦθ™IΩ b¦^=)WͺΧ8­§ώ:ΝΎΊf?ŒZ$Ή:@αλξΟx!9Μ`lipΌ ž υΪ3ŠΘt#žrŠΈΞVξάp–]0T υGOn!d’9U§°cNŒ –ΓκnQΓq|c3^>J:όd܍ŸΖ+ Œ-\Π’ό%±]πKي­ήWΒtΪΕ&—>πή²Χσ?αό\F$ΰ“x:γˆD[3ΐΆQbγΎΣeB<«― Fέ’ύC_ηΕμn%­h•V+6ifΒ1Ωκ<ˆΔXŽeBη+ΕZN` 2jwaQ·±cXή@α °²‘nH±zΡ±ΔΰόSλν­Θ7vό7@uΛ3˜-“)ό1τΛΌ?Χβt‘εH R$†Ύθ‘›ωβή½iΔ§cΆ²Ζ˜˜Ο&½./€p]ΠλαΞfΏ.e5AχP(EΟ3$γ% ^ϋ"MΦBfcΆΏVRhύ υ]λ ’΄΅Η χω IσgSm~5ΔΓΝΌF;©^«Θ“ωW=7Μ™ ˜€σ₯X9_=η +Lψ:δUE0@Ύψ?uΛύφpReI@^BυQΆ9ϋpaαΈbdΫΰFƒqΆχχ}Ί Τνμρ‡··ΰC}"‹`•ΏŠ]O6 AWCV‘ې¬WǘΟ\t6PΘυέΤοφ:»“ χΨάώΊ·{UΊ}ΉEαψόγ`¨§}5τ‘ωΆΕm½‘ŒwέtšΨ`€ωnμiΟl)†w«\ΞM«+‚§α}·ˆά…πϊ@½_Έ_Π­+Z3Ϋ#βƒδ0©ϋ΅5δGz¨:9ΰ&αγ/œ―ΆGψΥπf ’tEΦ-ž X΅P~”YKSQ­±Mw)μLΔӎχΔ9‹OΕf‘98qFΦX0΅TˆŽ3zδN]ˆwBV·ΑΧ5gΎΕ3w΅°h`Žf ,Ζсs½1A3g2ν=y#λ +†σΊΟ¨ρ1>Ό¦ΟΗ +_xδZάN˜Ϊ‘“Ξυϋ4Ύ€wI9ρ*V=εΌ\€€~χ ς±δNpΎ.-T°{œόΞ‚γί:Ž„±βχ±{η’ z©Β­ΔJvf%ΑRεVςxh±ΨFJ¦Α‘°jΐQΈ"„dΏHΜΟ6‚ +XΏJέƒώ^¨+b:λ GΕ€YKκ:œύdίΚ;Ψχ³©γPΞ \βSn#8τ^Tή“  …οqΎV²άmaΌΰΜα‡8ώŠϊωWΣ~{ —ζza=Ω :NρH»|tr"Γ-}σͺŒΛ™ φΑ±ΘF”ηθHθS~ΘΒή¦Χ³WmJE°Ε0Π‘ψ +ώ˜zB„Ϊt50œ‘?hZΰ ΤID§(·Œ8]ρπ]Ζ„w‚22ΕΓ`σΰ(\^ό}ΡΙM₯@ΌΌ›-„ΕyβκYΦ³ zύν¦ΔΡ +γUΜi +0‘ ;W^8†dΊ΄)2Ÿ’™ Π‚!ΣΆ€’…·Ϋυ§lΈνŒ~ϊϊ!Τ–tfbŽ›ˆ‡Γ‘^₯ΏΚΝ$PV1*ŽβIΔΰ*ιۍ ψϊˆŒ€Ι¬‚’ε}]•dΙH}ΰ*ωΚΛ/φwU'z^ΗΪSό΅Γής9υF2g0JήΔͺj@o£bH0θŽV Ϊl™ 2qΈ±ευσZ–?’ΆΪζh{ΜcήΕ]λY‘τpλF=KΎrCV›CβJpz¬ΆΑo.Δ"«νΪ)Ώ![TˆΈU(Τ―WzΗ}Ib˜\]2dZsν+aή/f}rϊξ<ΡΡΑFΦmΙNfχνs4°η0Xυ8½ž<.­ Υ­—ε ίη¬ͺ+ΝFb½6/Dϊ Π6«ŒM€ΚΦζΧΝ{6x9fBΞ„ `ΡΟl€ήο™ŽB +%ہ‡΄s’U%`Υg‹ŠGͺύπUΐ«±$l£#+ΐv"-€fλ=T(NΟωϋΉχ§Ρ"$ώH•bΡ½NŠX‚ΫV±ψ:€Z4|ah$|R Η‰ιBψb_ΛΥ―OOίζ~ 6ΐιŽΜΘM/ζ‹ΖΌxαGDξzŸG»χέ«ΜΈLς‹NJεcYϊΤ³%·ΐή9»Δ,μ«V|tPKFFx…šΎΘ…Q4ξ-¦Ε^t,v–5Oά\Ηδ@έ#Ιξχφγ΅YΙrE˜ΑM隢πYVΫ(V5Οfξb—¨ƒ,>b©|*,…χψΝΡ²q§b±Ή<μWZ@L™šlϝ$kωη™ξ5ΞƍRP`1pό€@-·σ¨qz5qˆτ% b'Z&ΨμgΤkeFL8―HŸ’fωΐuρ̘6ΈVΆυhœŽœςι±£ο¬ͺIœΒˆΗL mυfΊβˆσ|:gω‚qί=œ~»ς1― ]€υ”₯γΫ!Πua)Ζ3ΑΓδ!ΪΨι°ύsάΔSJφ0›έ»σΤυ­ςFx8‡‘ŠέπQeςvyWzp³-wτσυ·λήœV„X°8rQϊ1,€2*h$΄₯ͺžΤ‘–#‡„ ΏAhΏž")―Gj¨n_bTƒ&Έν7ΐ.Iβ!UߐιΟ#ςζΤπD₯εDv£›ρ?αμϊΐΦΜΓŽ^ϋiRY}D΄Δͺ4jυo¦»ΦΑτιPg<Ψτ^œut6e―I<τ#ό‘΅’gͺDΔω:'†<^ιa2.z‰Jy‰ +²žκਫ਼tΕfε™ˆ(ΉοξΖ^<ŸΐŠσέH|JήtFhΉΖΩ…πmBΌΪω-kތ„·βΑΖΌ}¬8EƍΝχ‚Š:Qδ $}5ρ L+†s »ΝΒΪω‡«Φ—›ŸγΏ-aδ€Œε!~φΛΛΥρ!”0œh›‡`@}²$™=ω NI_ρε0υΊΰζφ2eUU—rJδ,έ©PηcΫ^σ4.θΟ +'τN:/Ϊ’°‰–dFα«§`‹=ύ”ZͺM)&^ΛoQκς †=/·OF6‡6@θ:ΧΔR«—τ{:tb·κnyΚΜu¨ŸΥoώr.GθΨ‘Œjj•ήτ£8dΊ¨ό—œή5Œ6wxΧ’(by4ΛΫρΌΞaθ!/⫊³ΚPύξ%΅c嘽VhR±V4 θ΄qonΌΔT/Όb»fΡθΞ₯U τQRΪ㽬΅,ΏZύqη ,AΫh-ψ&³½ΖΔψ7θ…'jή΅χφΐŒ^Χγ ’'-•γ@ 2› MΠK*Š*¬ϊϊ·«}δv5 ]]”Υ€Τ%< Λ[ΨϋRΔϊ’¨|v•ΐ;֝€elY εήΫ™S^Ž7€iΧ΅œΔ’ϋτκι»&h~)*2M•ΓΖU_uόΆPˆ‹8e§“Ž?ei†Ά1‘—~ίIA;Χ‹4Ζ*ΠίR“Œωy#€…Β³Ϊ8pCΪG]Ε΄Ά‹ͺn¬JZφž'¨š!–™ΦΝQ$. βδ” ϊ›hΌ εd‘ρlœΡ4$ž€ΠΚΌ|dw±uό›eΈq„–σˆC’ώ—.›@Ζω2P̞˜Ύbρ™ χέΛΖ&₯œ.- UDxdΫF\½Σ΅Β΄±aKΜ“•§©ΡHMžΠΫΔZ‘Vπ{ρθγέ ©emΫτΨ‡WyOͺδάZg{R9Ί!TδύςρUwX₯lΉ Γ؊+,f¬β’Lk`&¨«ŒΖ/|FΌ€Šlgϋh{Ψ"ΌQΝτΞ + λk—ΰUUF0μΟCίΧεε°;€‘τηςλΤAIΑZvcˆΈ3ω»΄>WΥύΆ[ιq.‘U ξΥ8@ΈΘW>9~ Tς±‡ŸfDqŒ:πκυΎςh–n΄]*ph9OaTπΝ›ΤΈθ`(εΤͺ^΄>©i©h)­£Ι{šθ•έα"c―Πg=OρφϊΜίξcΓΣΧ[α’j-UΣDΈ+ŠT.O5ŒAό@”‘U Wv‘*2tBqωHΆάƒ-gΠUΎΥ"K6`Ž~W_™ͺ s&ΩٟσVŁεΏL,c–€±;η]‹σΊΙaΕΨNήΟΘ‰­ΥXηϊYjή³,ϊ₯HλνŠpη†|’ΚLJδ Αœ¦(„ιœ}•‡£Δ€,5oΠ§xlσQΕςˆΐ[ΑAήƒ*€΅j₯°_‚ŠΩ`.GλZ@ΜPμF1§!e­οΩοπGΝ‰qΗ'‡ύ… »ύσOœ³ΫˆέΡ’)°γμδsieΞ=F=IfD€ŸlSŽΊn.fŽ4|»BŽ«v͏ό‰Σ;{ˆ·gδΨΠ’GΡΧFwc—’½Ÿ―ΌΑέs#`ζwͺξB3NUόβžOμΈΣ Η±a˜\Rχ/Λ}bΠjΆSψωΕ²‡³i=›Ξ§0d­ =a„¦ΎEgyτ~ˆAξo§a/Α©žνoαΰμΙΖ΄ξn©N[μ8''S¬<5„PrB¬aR†δ¬Χ3Μt?!ΧŒ™ϋ νkΤάbθζ0Ž*,¬&―ο;Nυο#•OζΆjweY «nΓLƒβ €νKž€B‘œͺrΑο†V#BTnΨI’k •ΰϊί$8t.ΐ.hΡŽσΕA»I²-•Φά(7EΊλB%Κ†λh£KμŒξΘΣΓ{πyœ'ςvωΎMιΎ~GrwΜ²Ο‚ρ)UπB&λψZ‡τ}¬#™°Τηp,φEt₯ΰ!`ύŒγ+ξA₯ULh­c0Lc“›ΓGΈυ$§.X?¦‘q•iνΡ! , +mΑ|ρ«α%¦’νΧκlΞφ₯ |Ÿ±…wΤ§”'Τμ«Ν°}(ŒCgœŽ›²oa-†(ψ₯b °υ›=₯±Ζ„M΄₯“Εj­ΟςiΝ{ ‹ύάm₯ΔΨG“SπΤp,φγ;εΌ±ξΡ2&υrJ>+&‘{‘ Vˆ]ϋύ4Βσ4ήN_¨m΅d‘²ξ›4"7™ϊόΆυŸ +ΘΘΩC^²…σ6+sœŠκCΘ(Jyο™*3ϊ_ϋή‰uχγ,ˆ7IΛ"·¦‘ +Π₯hφθΊΏΗ_1ϊ ‘. Γ +oξβbAΘΑ€„Δπ-ΖΕ±FγL’ͺ¬ΤF! ‘ϋίM§ΞψŠLϊNEθtb-δ2ν±_4ͺυ•JΖΆ,nŒδ*Ψ©±,ΑόK‰H•ωθΞkω)½`ε?^ΈW¨eA²nΚα…|hΠMΧ‚οΛ;~†Ϋ:²ϊEšΌμΩ&ΫρπEzB“l{3)1‘cŠΟθyψΙΖ{₯7™ΣCψOaGbΉlύ\=#–hΉ‘‘FmQχ¨‡ Ο ]θr€ n½Ξ  ;‚μ3ίχGΌŽ•bKI’·…=²²Ύ ά&Dz½aβνK[ΕE—½%'9oeΈ•u Ϋ8WhEΓ‹RV«φί±GϋΡδΦG» U#ˆ‡¬1ψΩ‹šŸΰm` YO’Β50–‚Ν>ƒΜL€έŽ}΅Υ1Ύ¦cžxeίⲦυa©Z΅ιpu*%}tH-ΒSF7ΈCΈp“ΠΩP˜i MH™ι» ΆΉZ·ŠΕ7‹Y ˆ=‚)]υaΫ―%#CςηΒ$έΛόιk³ξήΓSG .dαΆt1³^ωIœΘ«J{Ϊψ$θ-€0KψaμΪ)η›wd!ˆ(θ°dΝψ”ΗΣΧΉμ"vU£D@φ’ΩγŠ…·ϊδςΏς+©§~4ž=š3†~ᯚ0°/[›²…YώΞ„ΦπÞ |€9ηMζ>XΟ–π“ͺ¨dΣ‘¦[&‘Z0°χRf<qŠAλ±sΌοΔ―ξtχSœXPξΥ°Γ]Ο$ΈG‰§ίΓLO―²†9{Ε/»+-Cr‘΄χίƒ SŸΑ¬σK$ΟGΡq‡GχΫτ™yΠνύeΌ{Žά=ωΛΥuΉώ$πi‡uφC{άιΟ’²‰θΎ&ŠΊ‘±k>„J)œΜp2U~Jj1΅κεƒ`oΡ7έ€ψ{5kό€KΓ°m8ϋώ&³`‰›€Λœt‡wΠ·ζ֜S'σ¬ΓD?ˆτΊΗΨκ“»Ίd‰ώ‘§ω[S»β‚5Lή1DΜiτωΖπ‰Η/₯;?ξÏΎν€*„΄Η«-«‚R%e>:X13—ρF0Ε>ΫπuΘ¦ ΉΑ†h±|―#¬ύ]₯tΏέω`€.ΌΦέΔΛf€§O`λ02wθ`䦁8DΎΫOJIεnΐΉ0εεAhdoc•Ε’{Μlf΄0™ŒΎΟξHLώ«Δ† pƒ;’x ƒ†c;άVe€ŒIΈHv– +)sΟη†sMδ6ckIϋ¬bόϊ ½hΣό*sηΊυ†Κ썯eˆc¨p)%ΘQφΐ7Πδ'0pk>ΦF—rndhΜ¬Ξ¨χΞό=«€DψHm’’δϋwΪΔ”’܍kάοyɝνM1a„Tβ’Uθ„jMk*£Τ8h‘-vYΣ(sqL)£Irڞϋbη…萀ώ΄φ/Ιν.|g~’ ΪtPύŸ}έc6tΎ¦bθω5ΪΘS(-μj†j)Δ:€ +uΠ‘/ςˆ Σρ*I )dΗσΊ{’Ήάf“f + ΙJά•vzA˜’¨@ΙABb—Ύ£>‰Œ‚·:—SC —$rΪ½8θ¬ΪζΜaˆi{ZΤ‹wίΐ W‹χP¦gΝχ˜Zo¨žό5cOθ…&€r[σmR,ςν”°p)%ί΅ 폦fχ="3rΧβ΄x²Ku?6Έ¨ƒ\©3^,™k\)Q˜`ίπΙΊ“΅Ώ σ7βό‘ySŒόc΅AAn’/RοŸ8AŸ3ΠΙLFυ£ veuͺIyIΆB3h‚ȁβQ 9EΫά +4Ϊ=1Ν$ΞλΫEv4!d~³ωκ:. ‘λ‚ΟžF—ω6HXm±ŸξΤυδŒur²ΉmeΑ M₯FrfάZcςΧ…ιι’εΈ)ώTW8(ΝΗ +Α²cю-Ϋ›©ΣŒ“O?n‚œVcδ|RΧ3"Mpv+ ΫΩ?›_ƒvH_ 8ͺ)nλV!γ’|m17Ή±#ͺW©Ϊc‹ώRΣϋΕDφΨ΅«α\#}Μ'œBίΠq]–°\™3ό-λτmdώΝρΆ«32&‘ε+S`>AwSΖ4Υ^κ'%—ΈFΕ%x2δ½@ –”ά(υ·„Γ_ύ‡:y¬&οlΔ9Φ›W#ό`›«δε½Ώγ’Δ9|‡il{θ τΘζ&ς΄’λδi£ΑΏΖΝ²φψ·/γzβ"εyTH³ƒ›‚“- Ž…Ύέ^Θ‘u p'>)x– σ) +X(Βτ;•ŒN­βϊ8Ǟ…'΄Ϋ=έ~ς9Ϊ"‘qH‚ΌQŠϋc‡€t{Ί‡ ‚v ˆ"KΆ£ΟF=oy0:Ύvxθ9f)ΧIŽ@&R't.%Όƒ΄m ![λς‚=+Θιv3A±[ιV!3;yuί. ²0.Α„Ί9P²ŒGυk£kλ³»Υ‰??ϋ"EgC„1\kβΧ™Ύ†ΜΊ`²l6‡B†0#»WT(ΟRy¬T`ŸyΠΌ‡‰~ άH>δ˜ψΧ%s]@ΫRo—tG³ΰ {]Δφ7*)ρ³:ΧΒ>Š,ΐ‹FΌKSAœͺˆ«IΗ½ϊ°δΉύeσxΤ…ΒΥ›σ0‘o)i,GψΑ‰Dyϊμ?Κƒ:u‹O‚ΐ°x„ψΎWœΛŽ?όαp½ΆξΧΰΒ^'οTžΆ‰ »‘$ΝUοΖέp33«ΨLŽ'/tΏT<[Α. ΨΠΡΌ±¦,oΪ(ΨeWΏ5iŒέ“EW™‚>£'ΆT~‘<Μ³~žΜ•1P%αίYG°7`ΙάΪώ`Ρ<εdι_˜žžΪΖ\:–ή4!ζΧ}Ϋ'ξcŸŸN”ϋΦn|Ν_I€<όeP^y―Ωβ )Ψ@ΑŽΌΈώT y²ςάfNV”κ°ξΤ †L,'|tvδ&VŽ―ΓOλ»›_΄sΝJco³ΗΩ|ƒ%ΐ|ɟ~ 7…YWC-JξΟu q©r°f*Uϊa…τ +pοΕibˆ|۟ _ψf4δ ΛͺΥ\[ξ΅M΅ eΙ;΅.Π+ΰgύHc8Δα˜,¬f–J»B^nΝΩoϋmVzοg±ΖFJ tRλτυ=ώG ΉΦ0'¦όΫ.Cή\鈻υ©γ›ΌΤ?[Ιt]Ί «Λδ$rΦ“ΎψΩΚ§A0Κ==#~Γ ψζ7Wiii!Ωτ)€ΐΪ$ž³  E’ΧτΫγΔΉ-l0#ΙmΌov\S“=“#βiΎΗ―*9ΕΉή!ΩIφ–O §h28υduyx‘<7·ίΥ‘L%Yš>–œDμΚ°T}κšΧ‹3-4QŽjfŸg‘=ΡεΖ¨†αΧ=!u«d³ lΔCοUή‚Uˆ\Ε¦Œ ž©F8D9Σ‘cμ₯³ 3}Γe_K—Ύ¨ ψŠΉLΦN-Λ)hǁΞώ9}p/D&$H8ζ ΡΦvΣ e4γ˜@ζHXR“ΨNΆOγμ[έg]4-$υŠ&TbΖΐ5€ϋ΄JΎ6‘O€I¬Μ<-ΦΪ‘‹W=ō‰ο3³NΨDšLοEωΜΠν%Θ?θγΚ}JΛ ήήΡΦβ1λΕoχM.ˆι»1~gWΚ4*Š|Β©ϊ׏O_ެΦύŒ§Sf-αyzJξΤO±8ΫΦά†ΨΩ$ΙΨ‡Α_m©’E:΅ξρΐΑή9ώƒΫΑ kͺ]~‡%h.yDς[§Lk˜b\ΦBM½“£6³ότZψ΅H+BΣ<“^qξ +2k€j/Ί¬C +9ύ-ΦQe{zΝρΰ"WŒd{JΆ™ϊž€g£q§HD*π”Ψ„`>ΑμΰΰjӞΕ;…‚’Έˆψ=IΟ°]Ώ«flCΗ6-Ύ”7.­όq{ΓȞγDwd,κ0?ΥΥΓέ?ΟΆ€=»γMε¦SI2π"ψΓ-79Νί,!}ΞθGyz―ύνŸ_ήeΐf7Ώς}εGΌΌκC1·wCQ9•ύPν+g3rΡπFΧi°Vρ΅Dhξs―I}5ΟQœ‹―= +YeθΡ9D^$“© ΌθΤ2e) iŸ²ίXŸψDuΡΓƒvC©ΞK•υ‘ΉΆ?ΑδΈ3ͺΠŽε,±Ζ#Ύ²ZΑŠu·A•Άi˜p0όΙ―³'ά +šOΏ²Ύml}² - @ˆžΣ3τ7#_]ΏΰOƒ&‘79)/`χ!·y.Κ`―7ΰΗΠK²P–³£w‹’jΙΝA>BΌΛ ‹hŸξs.©΅IP=ς„Ύ8˜’ίKoύί0ΦJ₯h5·Ο= Z'jΉ{μ¨Y0¦γ&œAU<φδ W|bύ9ΨiSλžΐφFBI§h­ψ$Z­Q3†ƒ†~ͺΉWT^'Υμmζ)>ψXεH–V–ˆ© ]μŸzwYνbΑ–ε!‡‰ΓΑ¬Ζ؍±9™ -ΗζŽ‚D@ΡלμMΨV$Ψαe8W lΏ±(•ZBJI!sͺrq€ˆ΅’ΔLΪ&?prŸSŽŒƒ'w΄δ§¨Όα7K’ͺͺ'2ϋehjΊεθά&σΦO½±aΠΗx§Ε€ZΓ•qš€M2z[ξi2±Ε>£5HεΔ…*_νΖ))›…oϊ+Ξ‡…!,H|^/ή°i4ΜΘ!ιί·ίΰ€\ξK_ ’"1N°~ΠΔηOLd5Βό’zŸIΜρΈφ8ω]ΞφS{χΩΜl»¨θœyμuσ«Ω½Lυ‘ŠŠπκK[ ω] +†΄`hM’G}sxHU_lͺ‰ΪΒΣ†JDƁ§ν-ά܍βΨ›Ο—> +stream +xڌΆTœί²=ˆ»»†ΖέέέέέiœΖέέέƒCπΰξάBπΰά&Ώ{ο{oήf­™ΥkuŸS»ͺNΥ©½Ώ―)IUΤE-Ν€RŽ 7FV&>€Έ’7€……‰…… ’RΓΖΝψo+₯ΠΕΥΖΔχΓΕ]€¦nm¦nέA9w{+;€•‹•›…ΐΖΒΒϋ_ŽŽ.| S €"@ΞtE wtςv±±²vϋ{Κ-4ζ΄V^^n†…D€.6ζ¦ €’©›5Παο‰ζ¦φuGs ›χJA#`νζζΔΗΜμιιΙdκΰΚδθb%DΛπ΄q³¨].@ ΐ?ν”L€jŒ  amγϊo³Ί£₯›§© πΧ`ocΉώ pY]Ο¨Λ*”€ ;+όہπŸ«°2±ώwΊD“Θτ―`SssG'S· Θ +`ic(K)0ΉyΉ1LA8šΪ»:ώ7υ0΅±75ϋλπ―ΒMR’ͺΣΏύύ§;Ws'7W&Wϋ:dώ'ΝίK–Yˆ;:8AnΤ'aγ4{λήΜ«ΘΡδϋο΅₯ ΘΒςŸ,ܝ˜5A6Ξξ@Y‰xό5!όΝ +θΰdaaαζe@/skζ’kx;²ώcώ[ΏΏ―“£ΐςo @Kΰί_WS ΐΝΕθοϋώχ•`acξ0ZΩ€ώ'ϋ_3ΠςίϋΏ“w±ρθ³ό%+€εŸΟ― rΛΒdού?ξ.³¨Ί˜’¦ύΏώoHLΜΡ ΰΛΘΖ `dγd°²²±Έ.όwS›TΑς?±² KGοΏ‹ύ{KU°Η¦OσaΠώw.%ΗΏŒhώ‡ΰ,œ,ζΏXΣό_!oμώ'ΛΑΟz€άνν…ό?PS{οΰωκξφ—ϋŠŽϊ?]΅–«"ΠΒΖέαDeέLj@deί—hγ*eγ΄P±q3·ώ7UώmΧόG`φ6  Š£«Ν?#+ ˁύU•Ήέί‡†λ_>ώ ώΝ>RdξhρΊΨ8Ή¦..¦ήGόwΗ πeύ+C  ΧΏ `f9Ίύ όmΟ`ιθ‚πΟΔφIƒtTνZΖ·Γw†κΐZ'ΞΓ£7–ϋ‚±@΄ ί%β*ŒΦF²Ύ·oάβOΝΩΨ»\ΡAr䀂‚χΤi$W.±k$Bk$wψ΅€ΜΘ$ͺΘR(ŠΝ#–Μ_ιŠlbiςΙHm* +δƒ81…FJήΰ-RδŒͺ[Β ρ‘f CŒŠ|Žpq ~Βί΄ΠGΩYW.@Ψ*{χόv>ƒ€Ώ²£bH=4ʞϊ2B– +~(›YԎJ₯ΝOΝXŒ%Αμ'˜!†,ƒ·Κ8ŒI79Χ«iKbΎKTΗ@FΜ ›No 4χƒέO;Π±rμMΎH~_$˜¨m4°α±Ί>υΦ«’ΪΌ„wrΠJ%?cz΄O<| RAΊ8¨ΐYIκw²±Kn7±½\ΌΪ7ιΟi¬Φ€u„»ψΣ’}…h+¦d;°ήίͺ2·ΣYφΣԘβς­yά]Β+b…=Aξ!S _Oέ۝ι8ΏΒkp»¬sgk^ιYΰ£ Žu…πηn‘ΞŽ]:7ξ‚lN‡%αΫ±B Β +υϊFφ'me6…,εΜ§σvŠŠ,y£"Fκμ±–—έ Κ ΧΘ™Θfχ―υ\L½-9.λ Λšέ5AkΡ΅ϊ­™)>΄>ς$G³%ΞM,`―Α;ΙjΡ9ψ`Ρ1Kκψ1τ½ΉJ˜οA±%˜ΰΦS«k̟ ι―ΤΎC¬Λύ"3ή:.½ι΅ΚzI’γL³IΏOSi(*z˜Ω΄ϊΫqiΫ{ ςώόέ§ΩΊ:wΈμΔ·μΔ‡…Ζ RK`cΑ3ٌnΣPpΈcΨWΕLPH™ +Aφ‘_Q$vh ™.m(Ν½z[§7:i_ςmκFp”œόφ"©ς :;Χi“υ…ΏΥn.ψ;φ²(δ΅&>XΥwΚ9švwIκY΄`t’ŠτŒ΅2\Ίβ‘Fy7ΟΑΐXΌξ^θ*fV‹ή;ΛNhr:d ’ηΕWΑ51VΐyIž_(ξgΕγ$Σ±;vοΤf…θώ’gρ₯Wbzyo šŠ’, n••{%B°4œŠ'su…Τ²…°Βϋ²ΏJΎ­Ω֞BpάSš`C«»— σ ξώΏΓωv|AžΡ\Ε[Eΐ‡Π % +MϋUΕ‘;Κ·Q‹ƒ;‘ΤEξΚ(ΫKLQΪχz―A-#y{H’­DοΗΪod’₯<¨hOE‡#ϋΓmΓ½bQΆ=(Gέ1CκY&IF¨Ÿ__ιK σ)LGt¨UΔmΣΦ€aEτΌΊψf―U­Λ3sιY—(¨Λ§D2‡VΛ–o¨Cτ‚Uθή—ŸΑn“SΉ†©μ'ˆΩšώsŽ―E"qπ·ϊ(†^%cœΌ_[ύmœΚMο`_ ŸΘ&ϊk˜#πθΖ†ωσΐtίbtγ¨uΛ"γ™«δ2.ͺ~F‚K—½θ†™τ'‘ύjT*8ιa—R?œ +”μ1Z η`^ςο‘,1z—_bIζGλω…μα9™Ϋ€ΝίωX|έρΎ Ο‰~]·υχufΧ6«€ΞΫ yΗΔΣ-³±,i«'ͺοƒΉc<ƒ­ #½CœeꍐԷ¦G%Ϊ$ƒšχ'W{ΪΧ +G³hOS}[\λ B‚CϋάΡ퍀­•ΖHV`D^O·iˆ2“\‘½ Ί^Έ°SW‰ Μ^#ƒ©1θώΐ΅RCV•pοœ‚ϊD ;|sσλ³IΊ²Y£|s$’Θ–ΤΌ”8π;νΆi°ΐ<1s:μΩYŸΡͺ ϋ +Φw $Αbπ›\>!š·ΰ²οήΟ§έΞ½7ΰ ΆΟQ…žΐ}Λ?L3λΪΤϊͺτ%²«™2.άζeΡ³π$z.ΐ'£ ƒγ-G^Ž΄ώΤ.¦VoΪrk™›Žχίή“1ϊ’Ÿ/œγ–2Ώ`#<π™)gλ_Σ’_ΘΤtΓώՁX“<νJώ,Τ΄*°kq.Sΐj«>\š ³ύΎΠ‰n²$GDέ+²ΖηΐΔΑμœIYεd b&D6°<9p—3’Ωƒ"_ppυΥ:-ήΓΛΠ"ιόˆŽΜεβ)£­/Žί€η’Σ=ΰβ@Ÿ„^Z<ˆοQŠžL an π4R/Λ?SΤMD}ΦομaΆΐa^°ΎJkSF»Pen›ύΖο’ΠΙ!›…¬«΄WΣ‚ϋ¨uΉL_K'2©Μ­§Nj’Χίe±Υ»C7 °€Οr­₯gn‘{,τΙ‡δ>\Zq[5ΰ—`‚Κδ₯Δ¬=ΨώΊ†Σ¨ξ'Ή½$ό(cσMΥ<Ύ‹-0ΫΤ“½Š-ΎΕΐLu~a4Ϊρ€'―Žζ<ΐ6B-gˊάπBΫΉ%χR―½"y―+θλΐ$ha‘P2οψ›,?ξ΄Ρ–œ~twtΜ {Ι7u:-Π;ύ¨*έpUω©Œ`έS˜β§υzϋκ¨σ{“5Τ³ψ~”ΛβΨ{h‘%G!Rλwϋ₯'7‘ξOAoŽ·ηΞ;:’|κ,ιMρ" +Kνρ)ϋ€³T&“T­1±ŽhF‘”C,§Ά˜·šœΆΠ―6εtηC¨ςς@ηϊΠ’8B²|@ψ‡‹μxΐψoŠr!ΧCΨΝσϋh–z]‘ŠηΞ± +Ξ—Κc΄6_ٞ„!Μ)o裐FΗ9ρ捳PQ6Α~±gα‹’ξ2΅(τz„+γq†€ΦΧGg&I?Θqj6Sƒ‹ίFX'ΖlŠ#tγΞίν˜ί‰ρkν³Ma–5ΰ”*UΑBoڎ-Bϋ6ι”£ΟβU…ΰΥ,LWΉwΕΘ5ΨzJΜβ±ς^Ιis+|‚Σ΄ό쳓9ζq Δ=gŽΓ…1† ―>™ί#ΕDκ ‹`΅²2΄ύ4»†nuΟ€o}έΥ²pΤθ0–€ˆ(Μc(θ‰Ÿ›ύAΩF›ILΒ£-'w–olΓΣ £9ΐŒψΒ|~ρŽΤkncEω~χ”nΘݚΉ:O?μCDγŠ~85ψBnRε¨h.»½Ÿ"-ƒo>²-―6Hc‰ތĊ*Ό›~"D«+K(‡NY•ζŠEλτsφqΣλκαSFWBmΩ:Lα":.r}·²RAŠαΟƒΘz[ΧΡ!{Ζ]J/Μνmχ£ +ύ™…Qά¦…ΣœσψcΡf‚»ŽOσ 8νζρΉΡ¦‡.*ΊΞ­mʃ–„Ψ+μšΐ²|U.ΫXw΄‹hδδ2·ΐ]rΙ?‘“Kον<ΒA¨"s'θŸΒ•—Ca_wΫίϊt/2qϋΉ4Y"G―MzWuVΗ!|ΚΎΒ#­+FT|ό~…AWΏ*v2[š\θϋ4›υzΑcc`έ*Ρ ’S»w§‚g–ˆ'―φΞ€ϊMΎϊ‘{ ;Ω]`tΜΓΛρQžΡΥΘ7<΄ω5ΚόςγΟ+@ί¨₯,RΖtLό‹M‘o ?ψΞ·J Ψ0“ςΘs~‘&mρΙG»h6]™m΄΄$iŠSΉΥαϋ_©™Κ-BςτΑΘDτRλ·Lˆ”ςΏΟ‹?—΅ΧPQΦN6΅υ ‘έΝ‰Tъ_Υ°Κ’Ž4Ύ±ζ·|l]Ί‡>δdœΗΔρKΝ}‘^*Ώ¦~ο%ζk©,±ΐ>ΓΡ‰œωΙvίNW”x93α±ΒEΎuΊΪ”)Q˜9eύtjΠ4»g!O”‘:½zn’ύ ‹›X76ΗP>YT2Œ32¨QFΝΣ5ξ”ώΝΰš^Ξ Μ;,#ϊΕ χ‹zIάL–·μ#DEa7R͊Ήι:”±γE»ςέ «Υt‘Τ༟dΞΊΌγn:Sέ rΫή]o]±ΚCΌΡΈψƒY#Βq‡Q›έ7μ3c§²Upd`Ισi.ΞudPm‡1ƒ8Κ›΅~sΓR`’–Γ£ά’‚$i³o@ +&n5σμJόͺΧda›}Ρ£)Ž?Τ7γŒ/€,ϋΤζΧγͺωζι4žŒΥNΦυΊ σδŽ²£™/•«qΒσqs¬υΊΑΞ%•ž΅R0,?lS+y³¨4—˜p+ΙΘ(3Ζ'Ј‹ͺ ςνŽ>ΪΈςl6eU΄|@¦lΰB#υ€6,Υdr™^λXsΒDωΟΫζΖ`ZiΎ^ξ »’’±ψ½xΝΦsαν ϊλκ4η!β”ΫN±ΓτΪ™ύδ‘»f…ΉVΜ#σΘ€§+";CΖLƒ΄ωΚα/BΗξ!7όˆwNΡy0’D<ϊzϊSIl€±TΦ1Θν‡KaqΡ+³lx/jVΰΧniΠ―ͺΒQ΄½©Š”‰D‚a¬>φήcr#νχD§2ύHΟ“φi:šj².}ͺjχbΎq:Θ<—―ξŒΣπ1‚Y-d.βb W«EΒ‡°«άi«…§YPce•€Οš€YγL΅)β­·Οjδ.!=Fγί8Ÿ>³yΏΦ™₯8R“Ž/";ΈΎ;6T|Η½yώΆˆRKlωkΪ85Iχt€EG¦’’£ž^q]Νσ07»Gγlt;ΌΦm VςnδežΜς₯―πΝ2›½MgͺψZk‘(‹ΙΊo[Άg€ƒ†iέ―9o _*>9\™˜&«’Ίτ_dM{¨43S±Ž³W%/„ΘβVfΌ‡Lg’\ΗΗٍ­x–uΟ?Ωοι@O©Ksτ1α””| Yυgͺx}FΎlBGΐ‘#α#"ΐ/*f—Bž4κηΕ›ΫuΎΘϋmφΖΧT%©Ε W—Ωέa%Qψ͒ש'‰‘ΛMsf Ο +βZΣ‘΄Eρ1Jl ¨ά6%Όΐϊ\@…ύΒ¦1Ϋδyšv‹mU—…’ςPΙ>X́ϊΊ°’pl¨g qV°ΨΌŠ75ΖόΦrάΜ/ρL$“»EW²λu`υ<ν–HՌ«3Ϋ”kω₯ΚΑ bZ)PΈ2•λΊΈτύNrώκ[γN½rψμγΊαο¦ΕHƒP-tκͺΊ +b*Ζϊ!2ΪPbΓͺV–‡“«^’BτŠ„3}―©uΘDf(o§=„dBΔv‘|― Œ•ΡχΉI'£α^‘lrwΟ9Ž=34h:*§šΑp₯dί.Λ»BΙ (K₯Βm+”"&b ²‘–P ΌŠο.ˆ»θ+.œψqκŒCšXγhΈΖ^_!Ά?M¦€ΘQΆϊ’|gΐ47+4K]t$sŽ ςCσλeŒv”Β“U‚‹₯(ž–Α’]L8₯W©‡}Ω_Ώ_Ί¦Ξ ,Η#Œ•©ŠD‚¬^ςΒΗ'‘°^^η΄ζe#ΊΞNΝ‘ν₯.:cιkό˜­u•ŸMt―­Τ]ŸxζmLsH“’57wό&·Kڊ +ϋ³Τ$Š ?ZΟΫ†ϋήΌϊaΦ%w€φTJ +E`jΑWšΤί4Σ‰8?~‘βύaά_!}ΡΉ˜^β'jz0ΉV%CάL_!~ͺ¦ d(«Ά9ΙH©Ÿv#^b~£©ίβΰu4_€α|še2|}ΛPLžž§œCΜΥ”bcςvΌ Ώ‚;_ ’ΆhΊ"oʁ_²ξ)› +]{β:—wsΕ΄§κ‡`ώ ϊ±Ÿ?˜eυH^ψ΄M€|/z“JΌ€Ί΄H +ί<‡Τ%β%*_Lκ‚*yLΞ£gEΫ|υώbΉχ’J&IQ±…,Šiυ Š+™ϋ%k»θΡV2)ϋCΞπαμ\ŽΚ½‘f2”Sœw‚Ybόsαž€"›Ϋ7²΄rθ΅”n³l”α6ZNξΖjnZΝLrž„ r_” +!ƒ½ΉΨΔεPλ· –»“uŠyή4κ&Rύ.S'%‹λ]‰§6σŠN‰zιkΗ 7γά +Ω&†˜±z:3=Z|™ο[Γtp“•ΝπQ)$AΖΚo€^l)Φ ¬ήό>ΰ +U9ΖΨΘϋΈ›PAθZο–σΥaF-η©n+2ͺηϚtr;ρŸΥ†Lφώ°1¦ %£€f- /±ίQάΰΠ­7’U•ΦΛ–Ω=σ1΄Ρ–α…#Δτ|χ(*΅λ|ϊ7*ʟώ±ΌP―“z―ž˜¦5KgζH¦&•—o;*Τ‡ΫΚκKR£ίJp;rΣΣd¬ +HηύU»Τ”ξ4ƒΛnΔͺώ3ύEAμ,ϋ…ψΆ\]IΨ›!l=Ώ#;ΣχΰγŒΨt ΄cgΜ3 ΐ±Ύκ©ΪΜί»“‘ΒlΊεLν±R₯‡ΙιxVnαX«YiΝ³ο+Ό·ΈoTΎU’F}_%”VYWς$πΌRΫ°σΪ{ΫΘn΄ι+φw‡MΑΐb%4κ.ͺO―»“j~£Μοspmω˜œšσΚ½Ι&VίΖ­šš™ τ[HBο*tΏŽpιΰ,ψeρœ§Ϊ}ίš\S›»ϋ•=» ]›£τ4;Υ)·`τƒΉσa£Σ7Jz»zڞs 6_Υkξ ‚‡M­V™RσEŒHμr’,όV…ΓΧ§™΄αΈŸL‡μ<~ςΜwή”φgS"‰3«UέΎ&ΟbΊΫο{γBg+~/RΧ¬ey4ήΓ*Χfξ–αiJετŸP±ΨοD*•>erΰh›¬Ά8v3{nZΞΰ?E‰"άY 7l`wQ0•αΤ3χn­Θ ˍ±©#υγρ,S•ƒRά–λU}{•‚=Ι₯JuW ύRšMFωM, ΎNŒ(π2Ύ_oί‰έ«/γ`€GΠzΉ†r…xp½$Π“~t4—“›aΰh3οφ‚7xεlΞ!boˆ~‘Ε i3ΐqCΥ]τ8‚κ5 λ"6 Ϋa@sŠα-Ν4}ϊƒr„/ω‚*»(­n‹’^Gώl/iΤ•(ί t\Ι Ÿ¬ +ͺ3 >žΎ² +\γB’YΏ€+eΊl]ΌƒŠ<ŸεV;™Ψ°Β‡:gΌ4zYJβT«qΥ«‡¦`HQg±HIΉΕΐC7˜~ θ X^# έWVΕι|`5ΆBgzT€tΦ­ΈΟΞ’u€94›P‹γήώΕNCγ©‘ν[iγΥ!°Πџ$ι[Μ&/¨?”<^WΤ;@σώKΧR ΡŽ‹‡{ζ€TΥuƒ!8Ώ—Žή«―λν" ΤQš\_γ7$ώ ˜ŽΩ[ΠΚp¨tΆϋo―θφm/FZ,ο—ΙΓΞΌOh{ΪΣxn’qάΈπOPR‰GφOεμφ=β”ulux!ά#f{0<=¨JM‰ͺ$ς©Ι4ΪυޘιKΟafΘf‚λΥήΊtζ°φθΰwψhYF‘o€Qp$Ή{½4bˆ@#LΛϊρZ}ϋSz:Ίi!WΈηξ­τξvd²Pε”ΓKMM0Έ#EYί3$8π]ϋuδ[ΉnΏω’McœiMŸΟ‡ώBΣ₯Ϊ,Χδιύ&‡EŠοt¬hη:Ÿ62aj½Zφ‹ΣF¬‚ξbˆ]Π9”nώCwΕaA w’ ρ›ΚhίΟyΡηΐ­P™TέςΏ?ϊZK—/ g ±ΠOΛ’Ή•L-Λcdu6ˆΐΠεS#<Žχ:vYΕyC]‘\Λͺλ|qŒ[ΰgΦOκΈμρ@ηΥ·ο₯PΝχ†(ήΕμΈ3{΅5 Ԑ%hνΦΰΫ?7λUŒ©:E ½mA}nϊ£nymπρLΔΕ11θ_εΥCλ +•,£ˆΐw―°Βέπ{ηMM^‰2τ㚷ΔjΝΒ8νS , † ²2@ +׍η'rFμ’‚ω&J+>Rβ·ΒΕτ³Εlk]²ŠέSΑ`±>ΆqoΘPhδEΠl„QΣΙwR―ϋv)ΕsΟ”‘ΉeSwάĐΤh'ηXΦlΔ6›­RΊ―tδƒ˜ΠŠ[„zd|¬^ϊPΤͺξ [”Ύ¨+˜υC±‡Κ|Jςuϋ Νq_§±F‘³ώ¦ζ―pμ,]Λ[h'Η”cχ©AQ,32κ=iΑ7‘Ϊ/p*w_π¦7fτm“qI~¦C̸ΒD’’Rϊό¬χ«Έ{ΈψnΆ£`ς³ο™ΦΣΣ{υ•GξIω·…YΰG^μšŸ’nSδ‚q€jYΟ +ΩLιŒ,’·oб‚΅ Xœ@εhΚΩ^sίόόjίIΤ‰I†3Ρ0P[x™Ρ—ω:N$ 4X θι !άTρk+η:<³π}¦œΗΆΎΫΕ9[2:,7υΧβP͍b †hYiύΕ|λ€?¬κύχ_Ϊš]$΅›μ?š 8hG^ΥWΞΩ@ΧAέΑ:mΣEΝe—\sΟοΰj8<%…ίφs i’a=œΉwӎ ~AQ'n>Vg WdΧ―,Δu¦»c©)ZβύVπΖ‘A)₯jƒ(8ώSΒύψu‡Τa(SOR}§xTΤzV*›h?κœϋ™ώ5βζ b*TΧ`5™nYά{έφ)©ς‘iMΌ^”·@π*ž8όe4ΆNΖE9Θ΄xWϋ3ΝΈb,ιFe6ΠτMΓ Œ™{FθŠh₯*ΥΩ ωΆ(Πεμ}ύΗΉͺ%’€|ž%ρεjΑ}f)F!ςsmζx&Bσ„ΗΝhη4‡­Ρ(ΑcvΧ…χE\ώ`ΡΒ]ImhΑέ‹ψ‹σoνΘpΛᘳ•!ŒρS•S2Ό@ͺkΧ¬a.hʊ*cή8ώο»XάD9²Ψ¦Dn“N»§g³]܎HͺσςΉ Ÿk »σΜνΞω=jͺΧyΌΪ—˜-Q+gΞ^δ9ας]ϊέΊη lΊ#sΏ‰έt€J˜Γ-21όΓ°·1Ώ₯N+‚†[*sηq72jpΪ .Ύ‹@Šu²0ŸqU‰μ]kˆτnΌfθΞ;Nθt9Ί^ΊΝΜά‰gŠΘΫ·―;€δρΌ‘@†[NbfOϋq9ΆΔ”ωsΝw‰‹jL΄Αv³Α^ΣΡΠ[ς)[£jΌxk©žπΈ[rο;p)Kwΐ°Ό%jλέ5PR¦ή\ϋρ+0Ρ’l¨#.²Δ]&a±"&5±GQ²Εϊ΄WΧ%Ί"ΘΉηC‹”Γ›Ι4,ξνW‘i?RΞfήΜάΈ$kˆu +37ΨŠ,γn>nν‹δxΘ…³Sw/Π™j.Keˆτy‚O_$)ε©•±²¦ξg &π£ž…]š­―‘-8ζΚ+Ž³DTz)‹₯}§–ͺS‘?&$Ȏm~~,RΚ„’M_λ΄1Μ/Μψ˜ZψΗξΤ«ΛΧ͊§¦ΆΠ}΅]-GYY+u’ΤRφCΕΈδήΛ‘Ξς§ΔΗγ—.aAZ-γϊΗΪΦWσ“pρ l{.°‡γσ―žQJήrά`βκph¨—°JΡ >ξΖCV+Γ>―GXΫ.7§~†ωIξ9TΖζ)oΌ”€gΐ Ζ γηMk!Ι'½ΞP-;Š7"MSCŠI““ΑO8Έϊ£0ζζαcl+m£RxΡ2μK2K₯wΣΫχ.iΊθ—“όβ@ ۊ%K ^οbωœFΔπ}Χφs@(.Κ^ΣS iB’:՚"BW—ΗYψ4xͺbquIB^ˆ­ίK‚d τΙ}χdŽgs‚){²/ιβ4°{nOΆVΕošAΛs˜Ζ¨0Μ„ΕΪ­ΐ]Φρ +ηeςμκ ώΖΐ&·„λ.ΆV'»MΎYe•Ϋ yΊωZ·jNΘ2τJάΟ₯OCnΪυ }vŽ|z ΏšϋΛzσυΖ B¨Τpλ†Νχ<³c{^eά›~E˜ΈLκ3;kTȁžΫŒ<DŸ,{5σΫ±HΐoΡQXaJ6s‰ŸJΗLί–~d嫟L4e‡Ξ tJάΤρQO›aΖA.›²²e“%ΒF¬Ιߝ³ά@2 „ρώΙα-+Δ.Γž’Ρ˜θΜ6ύ±ΧΗ_WΨrΨκe†Œ?{O\νέ½‘!†{ΙM€ΰψCœ‘’ηdώsτΪ‹η~…+şο’ dΙhΈ9)ozUhΚώ={ϊpN3΄‡`A:JL9aώϋΛ―;\'Σ²rΉ˜Ϋ58ΑφsnI ωmv^ηV7΅%0›Ρ Α8&«OsψωcόP°šπ…(fHΑε‡_ŠŽK' π{©qΒτΘ¦kOԜ!)«y₯ χύόŠΎZ6Ÿuό^ΒΦΪ]­ΧœΛφ#Ύ«κθ‰$χ§ΟΕΝΉhwX+;¦aΞ-έUόPΟΘ!¦_Τ‘Œ ‹YώŸ4>ΎsΚ©Ό…guˆΖJP%‹¬>Ϊ³Ή¦ˆmZR·–P―T&0Φ‚ΩQpΊςΩGD”A†”"½ϊ4°§—ΘDi—τHΪ~[~’v‚ω‘U/+]‹«}x'WJάΊhχΦΒB‚ƒeΙτΓζΝ//U +ESy‚ mf°B%liΥOxͺY2 αͺd•ζΊΒƒ^*Vπ1όV"ΈΝμ”jΒ‹b)³CsCΑ<³τΤ!§Œ.Λ3΅;?E^KΔ™Wvœ€³h–€<—ϊ„ςw6 `V™ώ”— <‚«I3uw;Χ5€y[νΛzv¨ύ +ιήδηyΌDΚ’–;ξνPŠ]8Ž_Ru +»½‘R{Α).*Lς£v«ηym±^^‚Rύ˜Ψ½‰κLDr‡τ'ϋSώκU°DΧιΧ!ΡΔ|‹θ`NαSΚ8Jγj΄ €M2‰†ΰΙu‘P”³ΰ˜μ©@u„τ σ@2Ύρ³Wτι™Ό*wΞ#ζOy„ Ÿ/Ά­}pήH}γ ΘΔ7„δt‡ °.&šβ)ζώ<ͺͺΓ}±V3‚Ι/FΌΫ Κs§ν]¬Ή~bΗ{Υ!1lA Ν|SιG}ΎœυvΥ6ΐΕ|tψŒ) ΦΩ#C*^SR’ύ'κ0“FbΉŽΆAΏK*T =|§[2ei€U5¬bγεšνM’½™ϊ£‚|*ΑɌ9ρŸM­0ΛΈq¬iΒ¨on‡άδ²l†DλλΚμΡ 5ίψDΑQŸΨώTΉΛ’“i9ψ€;Ω‰h)Δ‹½Π ξ›{ιqΎΏ@š•‰`φYβ·A9x瑆Ÿ—ζe&Φs¨ώ=ϋzτζBV~βΖ‰Φ 5— ^ζ? :R­fΙ²ζΧoθ‰fM°&ιfcΨΆd;‚dUΠΰ=XΗΪί²«ƒwγΤ‰Ω”°άΓp$>ω­ΓPΑ₯Ί)$Λ#iο+ψv­ΛAnγ@Π½μkͺQ-CgΫι<А~)Ϋ¬¨»Ÿ LpΖjΐΑ²!ˆHηΡx“²A¦τZιπ5ZbJΥͺ<+!q‹…Ι"όΚFόΓεe»ΡΗΗΆΑQΓSu!0Ξ«$5₯ΊΜˆ IκΟ.!h#4wί ή8ΈO*μBqψd|₯"Zφ`ξΘσ¦·X₯B6Opτ-Z™IΣΒφœWΔΛt‘ΉV.N‘Jo­η\0w³t₯?l/ΜbΉT€ώ±QΊπ ²ώ±'<―djƒΚϊΐZ/Ώ5ΐ₯δC8EΨ[W)θ·‚'im–ΊmμVV•κΐŸ‘ΫΣzΛc§15ΑtŸΟsrƒ™ΊΫόA ![²L€ϋυA·l€2ž]|Ψ*UW»ΔADKaΟNн±γ‡κ#S₯*„©‘geσ Χ’ΆΩoφΒ­ΥΜύΕ εΐ™η–I%Ν­vφ„Ϊž…ˆόLι)7(οq_V+[ΣΨ'[‹Ζ•: λ°Y%ΗισΈ¬ψ‰>«šƒ}8Σ½GYώT·Z_x· +vX²œόψ ιίδΙ0Τ^κ^Η‘ΰ}2 +'ΔnlžΤA {OΈ1‡ͺδΞO +ŽOc>‹±Ίσ»{άία-<Ήη3θφΛI3?L:υwVξvά!βD™e¬XΙuzΩe©5@Λ²Ισv20QΗQόκΉh³¬ώθ·T‚a«ΓΧψ>θΝζͺδζd±τŒtμ†|Φ΅3—ο…ψιωH\ρMπŸ(Ηlτλνͺr₯<±rκ@φB5ιq^šτΧΫwωSz:‹›ηθ²eoo²*κY{ςB•ϊ½νΞβ³Κρ«7§ΛxofζW_Šηœ/3„”΄ν§¬Ξ·σ`μΚsΞz ŒΥ«`Σ}pδιZέυt.υ,³C8)|8ϊ\ΪA}Κΐ«ίoqΠHF$ήΉPδΕ‘acIΣ©—!<<¦₯7b.j—mΡ)U₯Τ _π›8ͺάΒηΪ‘zε7 yϊ¦ kMˆ›ΒκκΌYφG§&Qz€;ο·έkYϊΗ“Ζ]Ϋ3pzΝ0Žz Eΐ\Ί[οMΫΐn,8ΉžŸ›ξ +k6‚ύ%^ΐ6•=P:Δ€²KΎπj£"†zξ–Y8zνi|Υ£FsvUδγλχœ  †6»-υFsGιŸΦ£‹y/μ? + Z?rΕ;ΊΖ΄š»ΖLΘ,Ν#;vn˜Ηs1«Οs MΰMHŽE>0rzL)Ί;w–ŒBO~%oi\Π¬­ΏŸ1yψβ3˜ξ€F,“ύ·3,ΥΜ_4θGNQ’UΡ`μͺIbε±GWξ3₯\u•H‹Δ!Ω@sΏΞU―Άδ IΖάΛηQ΅6Ϋ }γιΑz;Δ)Ύo‰‡BXK)ΐκ0ΒΈ2’QIΠο ^π²yυϋΜσΉlκ€V»½ξQ€RjίλŒfΙ%+†!œms€ζ91)4‚‚>UΥd3‰<h"OoΪ³ΦKeΥ›H΅ο~¨Μ€πΊ:АΪȈζFW ”<Ÿ­ΓφΌH“ΐ³βB'wgœš4ΎXΣύŠ ›Ž@\b 5Λi ϊ σŒΔ06/–ώFrΰ£a§œΓωY§15όΠ¦’•ΟΧΥρ˜Ξ7.αΒΧhEPωύ]=­ΎΖvϊ‘σ•qύƒŸβ»‹oύΘό@3ΦGXm œ$ŸKh<ΠhšhμΨ…ψξxžLΏW9&7=sα›@1£ϋCΦгώ?ώgaVˆͺ)Ξ#“ζ)’‘7XŽœΙΚEΝBΕ©xΧC™¦n³ΙΊ”­›ΉΉ…΄, €/’ΕΨδκȊ£_³Ÿ1ˆEƒΏU2Τ”»@cWg@Ν”ωߟIyHvr ³Ϋ: ΥΪ·€yέΰκŒψεk΅w€Zά8~“Dύ|αί³ϋή―j¨ΥκΔθL―»6i¨nΓb,„Ώύ*°ΗC'°€¦ayA<’cέ‘cΌν-Φ@KςΝ2tε)]<°Σv¬YΩqŒ<y5έ QPƒOΜΦ''. ›Oͺ1ςΰbβO&Ρ|"ϊ…΅ΡΧγv†Υn€oί` XβbG„υM₯0ƒJ5PΒΙΓŸΝeΧh<-ͺρzΰžΡνάlβή¬΅ μ ,ΘB3ΕΘϊ‡XBψVΎΩω{‡υ'UC0}?ω*œδΗΒΞ·ΈΒύ΄?ΙΡW‡!I‰Υ· 1 ”Οώ’‘O>ΫΥ’ZτΡOΕ΅!Τu½;ԝc”F#”ן? ΟV― XυλΨ¬ί ω9;斏~G·@ΦΟyχψδδKΕύ9)‘πη‘κWOžκ؜*μq™8~rf"Q3" ‘;omjR5©μχ)]‰Τ˜h<„ΝXxτ·ξΐ;'φ=Č. εŽ[6$E’Χ«Ύ1άΙ`ΏΔsάiŒ_ΏςΕ6Α‘ Ρ/bχTΈήήsΤC-ϋς:+Χx'@N§‹+ZΑΉV΄°‘ϊLrδΘHΰ(βτΝ5Ά£[,Όn$ΞΙN ϊGΛ‘ΖWš&ύ6gEΒE.)„ξ’{MUάχš>ϋσΝΝΒԘ©(˜|ω7teRη+νpβή$‘2\d―ΦΜ<ύ3Λ 4?θ­G»ΉƒV΄Μ7zyQ Τ”ˆN/Δ§ρηuψ–W|l˜Kύυ‘HϋΫτ8°H_’oή"‰λ/Β£“ΧΫNλC@8Δ~{ZΔ8τ[΅AΩYΫ­ ‹o™Bœ?•ε?BTfpη9Sšχd…XπΕ:Γ―M³S―»3ίτΠ²Όs‘zόϊ±~/^Σβϋ“,¬°[#­‚ˆl†BRΑγ&³eβw}¨H“6ΡΞS―ώN―ί"­Yλu τ’ΑΫ:F2}φ†ρ:­ύŒχCκKg<7KnΙώbzƒ}―Φ~Ζ>CψΘνό#μ(μS‚ςxΌ‚Κp€³νR–ο'Nq—ώfό¨ψϊYKbPϊ(³˜`[cϋWή{²°:ζ3:˜ΰΤ+ΟA\žOYk!Ές=0:,Hχ3Ήi!Λ’ΒŸζ=7=Δ»,Ψ!HE|_’m π²½u―’a«Θ@HlΰD`nGM»~bj­›η†PίƒλΛώ°Τ>»w/M›πΌˆΤyοeΦor³Π2€5€ΛPŠBεš…το€tσKϊyPNŠςβwΧEFwBJŽ:σλžς>0„ήe%rήα νΓ!Ψ)†SH8π§K ?–c\θœ>ψ–qχ7θ9CΝZΦhyϋaωυhγŽαqέόD)‘ +vlξυr.A^IšxΆ±ρGfΏ ι.E»­•=rήgή’A•©ω ΔσΛΆŠcG CΨrΣΤώ “ΞMπ,ςDPϊ+Ύ«_²Ί"»Κ^xrFƒr#1m›•ByX)^ω {“αψλu=άςΉΉPͺ?3·ŒΏGξτnC‘„4JMl€iΫ\ͺ1O³ΞB›7Ec0Ό?¨ +»Eˆ-Έ ¦κΙ“xΩ?Δqχσ2&`Ž'΁™ΰ¨Ξκ-gψĈSHΒ4½ΌE`[aŸŒ³›PΧ 'tΊ£ˆŒf˜›LγΪS•Θ¦Σί’ŸύˆρZtΌ30KΧͺ‘~ψg'6wޜZΈ=jXTL[οžE^¬Λ:ω’jΪ©;Ο·3~½ΞkΙc‘Y5½A«•I%a‘%KΚϊ8žXΚEUPΒnΏ· \Τ°τU¬ϋζχ˜­4“a +ΓΗ΄²6U Hl3y\ΖξέτιFΗΎΥ₯S.ιιΣΒ*^ώX©£τiκξώYš²ω†χέΖQ!n?^w+Φƒa½n’*Ž&­ΗJM! E”ΔΒΏ³ˆGπ¨˜ςάζ+#0,‚ν©¨Jηύ€\-Ο]VφWΆΜΖSxW2tώŒΛO/eD‚†D>Κ +mΒΠz `z§(|€ΎΝ‚šxμΑ‹>ά)Α™—)εWω*δ]έUqŠν6hΒϊKνCπΙ ό)k:ΰMΔ%‘ΓJ\Ύθ©«φXθJNθO„ΏDτα Ao^v ΆΟEΣ€JŠδ˜>oόNω +[¨KNζh™*ωDsα4ƒΒYI²¦ΗdD—XtL¬¦bΟiοήψ`gcΜ(UeŒ -P΅$Ώ)@ͺΙ₯ςΈ>ͺΞͺ£|{ˆχ€9ΞΛ}²d)ŒŠb‰1³}hGUΜC€ τ-qGΥω”Σ$–Ωjμ«μ'ιjΆτΟ+€|7~ΉΓ―}.b°Uη}φΥd/²?Œ„(±Ξ9Θƒμβ[ζςl°&'ρςγτb‚\*ΞΗ“…«Ή€‘*/CzΆ―ΘδθVΊ¦»ρU]3φω!#¬—_ώT>ˆ‚em0ω7ŒΉ£2?0{SR PNΥΆh믊ζ5+oϋ³Ž¦}-u‹>ͺ³MwζψΫT' όΊ/υΒάΫωΥ|i·%CΏXPœ_V€θΪΎΠΚλ»Ο@ώ=α-T=„ X¦»d>Η$ξ<4y^{ŽςΦκΣ3κ/mΖω„Θ&žΚ>°…d½ΝΤ–ρ<&r1²ε4Λ¨ΤP θ:πνΒV‚βTΈk—‰πζhw>δΈ ——J@Β½¬L›ΤͺΪ’NΘή‰Πς-W#kΊθ #ΰLύοͺρϋ‡i»HX *Mc/Γfkw +ψm˜UbΜτΎΈ¦Τ^©σ§Ϋ¨€δΎ$ncςξapγt“:8ύEλJ:ΙU6Ρέ~‰έΣRιΰαrbu6£°κSΤ/¨δ€ΜH6$ΈΔœž: Δ,[m:!­oωc]ΨΖύPG*˜&΅J°œόε%SHΨ=2Ÿ ·nc"=X-„2Β³# zŽΡ€cΏ―ρšι "ΏŽw0€‚†8hεc·$χ€iuΈ)c[:4•\ //(Ga7-ΉΤΣΈtΩΖ―=-ΘΑZ±χΔτ k>lΩΓΈYε?Ο0―dχ23™5μj‘‹‰‰Ε`lΤΠΓ"7Ba:οpψgΓ±kœ²}ߨዬͺ@ό4ΉQ>Αΰi'ΘGw²3ΡK?₯FΦκ₯XΆη(C~Χ λ£jj  o7ΑχΠ1—;ςkΤY^ΖΗ§oKΕux©υ~=Ν]‡[XΫώmΝ&AχeΦXZϊΝs5_17[ \–+š΄`x”}”ŠφqΐΜ8¨_p¨΄9Τv¬Τή3Νm=ΠΤΓε\¨‹DωΑυnΤ_;–“Y’GθΔόu0ζLVlœ›Šΐ‹¬a1γώϋlν'^"³άx§2fŠ!^νλϋ!Ώ>!C¦ΆΡοوœ”₯£œQ¦*^­GμΠ\Πκ†Šs”ψ¬qηiΏή:Ξ‡'†ZwΏ dÈπl+8oχ+]: {vJe\GΗ¨wίαξ…Ο2o’9TΞηt’7=Ό'T΄ΛΚrΏ^³4Ο¬³¨_gŽςΑ]s£ΑVκε26ζ8 18λŒ‚E(π™Lu9—n&Špε@L;χΆΎυ&O°²Iν’έΧUIAˆu~Fώσh½G1wχˆ‰΄5w ξ§Jd +訑›Ψ‹±4‚k`FŸ1©!Œϊƒ$;φJš—Mΰό¨μ¨C ΐXΜj\Β£ˆUΐοΫζW§ΩQ‘…VοSρ]rπŽοgGOρΤΥQ©~ν>Ώ” ύ㞈 %?/Νg.@ΒΎœd•ΥΏ 葬•›«Zω”ΠOlƒΞR¨Η¬ž@¬:Γυζ˜N –§ˆ.ο…Z€ύ(`‚ά =Žž6τ¨ΝPDϊ?ΌάΖmΪΟ 5GŸΘQγ[%Q…ς +€₯Η§Qή#G#ΞΉ<α+w;>Έdς!•ψ=σžtκΟ=ΣA†·> T;)rά#CΕ*°Φ0‡ϊ4mm«ή^QΊJΤ{œzy‘ΡΊo%PΞ\λΦ‘AΣ’??ΙHpg\ΏiW& +όQ(X.ω‘"Ποφ΄θzεε`=ΑωσΗ­q©ΙΖβ’ΜΕ  ςYϋ`½r|Fβk§œmI³Οη€Τά($±αξτy/ΖTF(Y'ΤE•~žώΎ‰jƒ±·ΪڎΔβΘB%ΐΝΞœ9 «­ϋ¬ζoάΌ|9IπΜUeν΅”ό·Ν»MΊΒΡ₯²<5aΗ(w.χσaoBG]ΐr₯  Erό #_'›°E+Χ²γΏzKm˜ιr?ΖκΛΥw)μ€νΖvΰP•ωϊ%!°Αp?δZPQ4w—ΩcμΘwٍfυfΨόξxe—/κ―ΆΊόIρ́Pmό|c₯ TnΛΨ5ςλξ›ειέ«¨¨jLν₯όΟgν˜υ›n)Ώ0#Ο•ƒάtΒή‚Θ‰B€YŠΪΝt3{ζO„"gT©μ$J/Ιυ–Σ€t+X­]XU䌰ύJ6hvΐœ€€KLνhΡ)¬Ϋ‘ς¨»j/~”λΝJay€‘pΐ–Λφ—,`cYΞΏ…D›ͺ'ι!†Ό·e ±T (ƒχXί‰ tƒ'ƒT}0R΅oΞΉ0ηΞ2ãوλΗΦΊ‹ΠɎ΅±ΤΌLM=ˆΰ2]β Σ%/§χWνͺgWƒ»ΛfμΔ›G΅z)rD6,ζΫv;(†f±κσ_ΚοŽγhυV˜Ιό€o?‚Η'¨Ύ†iσηˆνοΩ~J€˜. ΑχSλNFΤαVΝΨΫ.¨q{  Ξ0Ibε‘dόbήλK0œBΛ0οΏτσbΖ[nœ,Ξwιβ“k§Xeυ4ά«&Ϊ¨~hrΉ)ΉΒŒ‰ΐΔW¨1’ϊlζ±™Δ€‡ΪŠΞ‡)Ÿό¬μpΑk·Θ&ΞϊŠΫ«»(*uΞFεkž»B†Ιl‰ ·U'pϊΎ†‰’φύ·Ωε zSkͺD +ΰnΝ›Ό°HξMpπ5˜Ό9"_ΡΤ₯©‡A0άΔΌ‚Υ5 £–γ§jJ–.ΆλЎΈιd-bHΙw²μΎΑLRVΉ”ΚG9έ‘ςνvΣ’·83ŽK5QJϋšU'­™―^ΜΧΪ +T‘l-Hpσp>ˆ ύN&βPΊ€Xšžpz($4Ρ­"R±Ώό=Ο 2“`ustLΐΈ© œh6·ύYΉpPΦξ–ϋƒΫ9)zXW­A±·³.;D λΦh½ŠA"•Σ%!Rq₯Dω6S’Β 7›0“%-ΔH#™λw',?•΄6eΛ—έ)1qυ +ΎPn„ˆϊqΨ€{ƒCTsΝnΊς)*W%Hίιo_Γλ­XHωζυο ׍δLlΜL±j”KΎ–ίŸό>MψC^doFψ>β˜ρΧΑ9α;—Jέβ|ΩXې‹œ| –šuάCΗKκϋQ·‚;λ2^¬tΝZpEΟσ05m©j,VB\ Ι„zX@NvλδΩ*N| ΘύržxΓ{Β-²δ™ Ό{7)±S>$Τ ψς•˜Α΄‹Σbδ’ ΥώαaY†N η;EΆ†uvMgύxαikΓEŠΑ"uΤΠe^Β­δόSvRΰ΅;ΈΦ(Hύ·ΟˊW–R₯M΅―€ PΦcέdΣu–›Ή¬†b ¦ƒ)qΤœ΄Έ‘m ζ―ͺsIA€HRοN]GΉ!Ο"±R nƒβ ΊάΖTL(.¦*ϋR©"LRb[ˆ’E»¨xvϊΆμ©ytώK›‘vΚeόΐγQ_αeι΄΄Β g‡˜ΐLΙ5 +&ΤήƒΧI4b8£NšίΖ¬Α~ΊO’%&œ9$zπ!ΉΨ˜rK«ΥjwJv".³9pšŒή"]Ϋb fCο΄|0œιτεsŠMŽPŒUH?ϊ11N:υ ΊΖν¦cΰΌV¨«β@y°’8Ώ¨σrbρ|†Π5Ή!ψa%­άψτ%©»δΉf…jXΊϊW4η¨η΅δœχψν‚Δn‘X(ΓZε@&h2VΒ³²q»–Kι]ni>σMž……΄πXͺ|' ξ‘d ₯#ςGΗsαw> –α¨Έt’7ŸHUμ(τA=r§ο’ΣΣ6HTC0ο.Α― έωlSY_-)'Δ7S§§?šΧ‡aΟBϋΌ˜ηδξhgXΉ0Χ܊Κ]œ!nB=§ΣHτΝgό1Ki<: †‘vgʌ*1Ύd₯kcαΚykTμρΒߟχώMƘ— +pφ­<½"Κ-όbŸςƒƒœ4’E3@‘z~%uηKo¦yg+z| Nœ³νΏ—ΙΛkθδΑΌ.ηω2|¦ϊRγ&Px‘£ΰb§Ζf˜5Xϊ“3Ιzl°‹­₯v—ΗΣΌrεj_ўήsΜΝ…70ΫΞM£_²]dρΤ`lΔ—]bkωϊΜ]±†UfFrΛφ€ +eG(aέm¨κb’H/4ο½–#ΫΞzβ’μ@:»(m½:ΪcA  ώzr-ͺ3„Vω‹ ‘Α‘€q{ψ=/sμU ;¬έ^cχ"νs’±œ-~YNίΪDOt‚iι$v$‡Λ9οXΟA­œYzϋ"ΚΜύ܁2&~‘2šπθP‘κJnξ΄)…7Θ}!΅ .‘zv,@IF%Nk α―)ώG<#‘?α)7k°oηήI·¬ σep επ‚O˜pv‘U[°ZT³ΐ™νdUkΨ<Δ²ΌHΦέΓRˆΩ}ŽdψQ’C{]―ΣUY™νw €τλ)λέ?μzyΪWž΄a›΄<ߎωΑ2ψς^Ž3s‚`…XMσύΒ—SZC/΄²p/ŒYΟϝα˜%ρω $u6Ξτ―*9ˆ QΝ0Z rWαodΘ,~^6!ΙGΔ.»S±·S*+ahΙπ4χJΗ’PYƒζCJ)P,[x{Ηψh—½ΕΞώ=HΞ’,ηwdΠrrj³p8ϋ; κ'»^K mΪ₯oΨ_Ζν‘²πWΗ>/ 'Š―Κ²‘{ +‰άΗαqfή.qπυ~jγR‡jζώi_βΏΔpVΚ!Npr F‘+5Ϊ|΅Τ^Ÿμύ—’•§ΕθΘΈvvθ°/ς|0Œhb\`ΩΝzΑ=(•½Hε>νkπΒ^_ˆ¨γ L gό.F½}δ‰’λJΥMƁΰi4`_i‹ΌmΈΔrŸΥŸί΄K4wžP€ί‘LnGλ KβΗβ…mπŽœ*V]ΓζΦ›φt’Ή(‘‚Q^aœtν'»XΓΌd¬6ȌδΨ';La“Έ’y³©2λά½πς’1kcτΔ%Ίκl3μ~–ZCγZό½9,·d±TΓ)’°άrR[w·iΫλAή™6J“έi*eΧΓv…Φ‘lΐwΌJŒŒPŒ κο;§€Fx·aεΦΒΣBNœΥŠ χΒΟΫ‹’iUίιmΓRRπ†χL‰Q"+Ynΰ +ΦβώωΫϋe€“¬W…O[«νΎ[Η#~LqiHΥŽΧDΞΠ–ξ’‘?―„λ,iG„6œ"Ϊ‚ Δ‡wλŒΰ,ωRR³ΪΜ-θhN+!g.2 Œ‘«0‘ωϊΐύ3Φ₯?±Ÿœ d,o€Ι{Ύΐ8σ Γ[υ«ή7ΝD}ύ`ωF¦ΐφ,¨PΙΈΦWΔ.YΙΓ茡€ΗW”νƒ V}€hͺΕrΙ +ΈU ―$uDŒ΅j…'Ϋ³‘O…\‘fΧtv`uΊ%šPͺ ^¨f8²M_+.˜Ž–Θΐ] ΠEeτ*ΒX»ŸYe›Ό~0wΨOaL•Μ-Ά~fmε\›ΚεKxpΣCί+MW87^@ζΟHΫλ`Ζ4Μέ[&°ΓΑͺe2‚s|a:O–­‘ζQ,.₯ΫL y§u,ω;`yΛ$žOρfdOp}%jm")©kQ‰Υ›erKxθ2Ί$*£5&}χ‚nXP Wš_a Tv²QYꚿŠογaΆΖt[(ς•‚q¨RΈŸ$+#+ D%oؚ`]Hͺ¨1 r¦Ϊcm›ά«t@dgFΣA‘ŠίiG@KcXϊsΤ-/ižρ`Ε5hbΫlg[,¬·ΎπoΟφο·%Άι\Ϋ²¨0V|ή\π—!Cz{wίέ]φδ‚Vv{ί©π­ςΕ‡Άm8WΫ£hV1ς±#—[HΙΆ›GβY„P<œMBγ’υtͺdό’Χp#Š…‘1nχ'¦:ΊijYΧz˜τ΄Σρl끞Πνpλά£` pΣ ΐ¨Q{ †€^v# Giz”άΉβ7ό”ξ…»’η’‡γF&ξΛ?ζΡu&ίtΨsRͺ‘"μόξ2x‚¦ž%AoΌ3#O €œν~Ν²ΟG ΛΫoΧr/JE•ό³Θ?OTͺΗWΙJ‘)Μ[ky΄Φާ‚Hœ91~ͺΨΜ€±ε«g5μ+„θΥλƒΝR/= +}œbϊU±@X'2€σLPP¦ϊY|Ρƒυ‰©αή.ΠΞ°+ƒΉ!πvΏZ Rs— .pyξσ•:#enΔ“@jf›IdΞΦƒ‰μ uDK m͐–Nκ |šύξΨΈMž΄u―ΰ«’Ά0›{Σ>ixςλΥwtPͺjΣͺ³Τ.αΕΏIŸ˜οΑ(Β™MN†W–©³χωa(ϊβ;Sψ†…οΏc`‡‘!Dž”Η¦.βί?„η_P[ ‘ψb«1‘ PΎWΛ¬½WNΖρ)aΐD§½ψ}k +Šψ³€ΖΗ’{εG·z°d­ΛΓΞ霒ωά\₯7’†cp΄V Ž₯ρiΠφΝόgT}ΔΘΨYϋΑ‘OπŠn,#­»v#ϊ3ςί.¨UϊδΨΧ•˜Ϋ^»φͺƒο’O…ϊUR/α{V;Η°\ΉνΉ]‘₯S…Ή–Σ-”ωΑέΤ•‰i…tί·γ¦M4Δ’SDU—7 +‘z0*GχΓG©M“Š—h@7)₯o±yΠ=eΞk|(@-+2β$μΧ;φ0ΥiΒ£ΐcΛl<ΓpΔ±\1q2<“υ›wΞ\ή™α‚ζεgεͺ± i¦u[nΊK“ξš2Ζ|δ1Ϊ6mΛΔM>έCv‡9¬ŠΞa8Ψ#<6"¨ΟˆŸˆ,2ς±v\›*•o$ž% ηᇻ{φ¦―4ž–τ›² }&€AΖ΅#ϊΛ›;y3s‘«zUt’_―K5#7/+κ²?ϊλέΨžρ«EΉp`Ν*λmBΏzB69€-αNφ©‡ρήχ ‡9ΰ8%rΪ”Š°`¨ ήvΡ>‚ΗJ^π£Ττ£dγIΔ΅MŒ¦ΤΒ«0ηŒίψJ€…Ι₯ƒ6IΕ#’œx'`ΖΡν±χΟ!‡aZ·‘΅›gΐ½­n˜p6 +φ±σO σ~ΰηθ£Hmξβ/£ΗΨΰDφR?ΰπλΡtI݌£’›UV;ΗyυέIΫ§Ωͺ}¬ϋv~’λ{5t|+bŠΏo ™Ϊ€Υ~kaŒNϊ€ Η‘Hσέ­Jΰvδ39†έυθ&Όp`―§³ονΣYκΘ–—/”"Ϊ…^ω l"“8 q=ίηό ΓΕγV²D”isD,ν#7λGα¬Ό{Ό 2NζwՎŠv~Θ΅;‚υŒ ΟšΥac`JF¦³»•:}Fœ³ϊγ£v-Ψ†˜š³oQD“Ώ›@Ρ4²ˆ©lΠVœΖ-'Νλx‰WΤ_{«±hjIδsXšΦfS_]ςe/λχΡB,l+#τc| œ$ζJiψ]υ½hD§Ή ™ΧtΊjf$HΟC†12ΗIŸδ'ŽΥ’¨)Ub#Qςoͺrb†Nkkͺ^£ŒsLs–…ΩΖ|—›3ρWΆ΅|Υ~IυBξ±SEO-=ΥΌeή)o[ .HζN€.©q,1›ΛΟΛi··ξΤoπ{ρφχŽžΊψleΐ•5­Kn) +₯–Ύθ5#Ί mΰF+`2ϋΫό“CΑD§&Kˆn6ΛΉοΑ Sd’A,FEG‘jΡkήv›ήobΗ5§ήvκtηž6aς /θo†‰HξPΜΛ'ˆžr<!Θg [—nFs'=Œ˜σ+ε€ξ―€5\ηοξpa=[„YlΠ€ +α²RUxT$ώωΆΚ\θΜ“E,±¬ΙΈ˜±΅_LSνξ ΐ•¦‹ώ3ΖίdŽζjθΒδΣΉ²*ϋ š8ξΌh©θύ‰ώdšbV₯,Όμϊο&Δ«Λx’ώ­˜],ψΨsŸf +Q©cRΤ±·%”‡&ΕCl‹LIΈ’.ΩΖρtϊΊοb%y°ΥΞOF; " ‡ρ »ΆrͺRiX@υ/;PM?­’4…ρ”!zλ$άκ΄uwͺd"{Α^G]8P˜ψΞ_Gθ©=Νq/*λc[ϋm\Œf EΏdβθχ0XD¬7(οΣDDϊλπ †ΦV¨7[υЬ³hΚΡ@bΰs.r©Έ©e†Mž; kBœέ½ήͺό ‡ΡΈ΄G”ZΔωct?&ύšΨ!h¨"YΒ$.Υ +yŸW—χ(1=+˜±<Ζ.εόΆγμ—·–šΪAT=SΉ@sφ_Όmεgο‰L6HšδQJ`ζb„y#Ω’“0‹Χ1>°4V‰υ½χ9Ž}ώžΡlZ–*ώt»|½ΚŽφΥαtGοb vα‡ePΐwZ―9“Αpϊ€Λt¬=ϊˆŸZ7@₯uΙjYψ© ’~³4QσσbWu=YŠQ¨3+‘LY%₯ˆΧFS>ͺ&>nζ †§ EΗΊΕ΅Β]+Š€2ά εWΧ€ΑDJ²+2’pœ‹$°^ξΈ£%o[κzε‡ΗX>Ο<:9Ϋ«Φ½’―©ΊmΙ6‘΄bJJˆ€ΊΒίο„Γ–‰H#΅ΦΪΐΗά<}ό†Ζ‘΄εΓeX5ŒFf±γΌ2Ι?§oΜ¬4}ˆ[3­G’Ϊυ(1*Ί«ςϋ΅mχΞ-e{Χ4“»6²ΈxX¦iη₯Ωψ|'oξ? €I’ +K~Q”1~οˆŒž&Ξ@ώ~>Lͺ`f‘-½τ‹Μ/8λ:rnP&#ΜσŠ΅– b3Χ/ϋH&Θ”Θ!]Ν§|\!©Ωκ§C‹R†³A*ξW2‹`YV6ήy₯¦XQΏσH~ΩπίH2V)…Z—6ΠrlŽ’Ι΄Ιθ +vΉ’q]tψtJΈΆ Ȟ ȟ‚9υC< Πg«ΰ3σ BO9uφ8‹ϊΎ!"œrΦ]«ϊV/@θΦl²#΅ˆΟ«–†Υ(83 φ(\k,bέ‰ΜΚ Ιͺ:kS‘[Σ‹E VͺΠΟήήj˜qL @ aΥ}ΚΠΣCvήβχaF©τ›6$Β°Ÿ^Σgo(BxμΫ’Ψw€G'ΈρDXό…~ΜΞH˜ ύˆλό9|Wϋ»Ρβ3ƒ _;HΪΣ‚„Α•w¨ΓŒADκM[RaΪ ˜‚χy)P“:­ηΙ\t^, °A‰Τv§ΨΨξ+ςJΠ–ϋmdΏ©M!‘‹ι l$Fγ©ΐ!ύιt«Ky΄:ΕΝƒT―²P²‰Κg†τΫ76TιKxμ m››kt π΄‰τ½š±ν/ρ+d«Š3&2ΰGoΠίς|#x€όα h]όͺ½ΙΨΨρ>ΙΌΐΎΡ­ +ˊ_>2Γt-ΐ%η ΅žΑ?X5ΏγT“SMβ˜:²*\J-Υk«SΓθέ6iφ…α2πŸοΠΣfX ­?>H7sξΡοξΒzμZ(G‡xψ}AόώZ₯}³~Έ,n…c.ΟCΘ>αAΔ(Ϋ»=φB‚˜ο₯OΆβΆώςqΛΐ]ψϊqd»°<<^³>]ΰ΄…ώ«Β+ΨρΫ›1,OΈΜŽπvvΝQ¦`Z‚$ΠΤ$†Άνž€Γ~Β +=¨Ύ4qƒρ#Œκ‘’IΆι.*‹Ts7Η°c¦££»`*Pγ\¨‚£²[‘ލ +¬’¨κN‘-ϋ0C¦…ΪrZκΊΕXΏ#fAΊ£HŸ§iπ)S₯]5{“'ΆΌd3'Hঃ―μ6=>Wω6&N]*Κ• ˆwŠ€π―6^ΔZ›ύ›~fΑfΟ·ρ>@`‡^RΏaͺΊ΄ήΌ…‰αΠsαώΧtEmΗ&ΞΧ±šυ_iΗXΐ|c—h₯ڌ»¦M πi4gΏω΄ΛCx&Α8šτ'Π„Σ©Ί˜j0`rV0_‰‹“>u +E_βχ/R7±(brμP$͘<-ΜΙ!`‘g­“PH&η˜ FŸ–ΊŸqΦXα“`caεJ§GrΠή­4ˆ-tε~’ ςkFL{Ω’»ΒΆ}JhΞgͺ²ξ£ΎJά‚ Ιφ­.!JœMρAυ#ιjΩ³ψ’ΌKΦ[+q΅78ΥτΝAή}ύηo\ί —>šȁ v0œSν,Y›B:Ε†ή˜¨jQ„?Ξ9 ΊΓˆ‹Ψ%n0 Ν’y%93ϊτž’R ξ υΐ9.δ3ΎAtEl ƒά™ "uO'Tšol[Zʜxp}™ +ΰ£Rθ+ύ1 ©2(ΝLΈκ-©αw³²DTςΌΪέpυς ND/¨Wg’ž£oaλ—Αή‡T½¨„κ³Rw»νΆΌ¦fΥλ³"Έ.ΓρQςΥΖڈs #‘;έώάγΧΖB6ΐtEδKν_pEοΘΑγ’αOηRNŠ΅Η0#‹ŒΉKk•7£9Μ Ο·qθΣΪ{Ώ~ΣVΕ˜‘ΓΘ©ζ j0Λm2Hw€―kΛ]— |¦ΞPT‡’}ΖVZ瞢―+rΘκε7q_ŠάΓO-gEυ!χ; λCϊŸ."“< Kˆ$h•p’”ΜͺHŒ™ΡPΏ ’gύ” mΚJ€}ΗO1€½ΦΠ―’ΰΐ”ΝΑF™δΑjGq_F€n ;οi$,Ι‘.z†_‘?…c=“‚ύVαݜ¦f)βIcώΆ΅4G@žT΄œιؐλ4οRΨ;[lؘ +\(²Ω¨1ΏψϊJfxwεxk>˜κψ\Ÿr +-JAΚοŒΚV²_?rΦΊr|2΅³εY£Έ¦ύΫ +K?NQX:ώ¨ηψ°zښœςŸΆω‘ΐΉφŒL‘ησ6}£CΙώ|Opr[Κ§n€hCΐ?―ŠΡ³kP’Σ]Μ:^” SςηΏΔ„Δ'όWQw›GŒ(~SΎΟk%Ρ§―ε¨7s'τGϋmΣ₯n ƒΠ(Ν§ΫΙ—pρ€ HΟκFχ)Ψω €ΥpNH.Νόh Ό½€@?bmΒΘs½ŽβLF5Β]›oVξ…ƒόn$αϊžN:[Θ! ήζΡ­Β6DI.?)1– +~yΩΚλzjŒýÌuεΑ² +>znΦξ?ϊέΦφ_ώwΓ’δΧρvΑ]£‹υΌΙ”X¬03Ύ¨).άA%™ϋύƒhT7Ӎi73‘¬Ωu Ρΰ$~€Μ«Σ/>Fo{IM‹τΝΕώ‹kdˆ" ±πcΒ-ux΄[Σi©…ƒρvΝCη·π,kγΓ§†k>κ,ΘPΤyσ¬%M}[eΌη~Υα}\ ώ^wτ/iYαƒΕ§‚]HI—πϊK9–n―ΐΆv*ωmBΚ0—ΒΦPΥκP[6`₯xšP`˜$XŽι†γB%³kΓ_Βα„ψ«yQzΓ'θ`LώΈ<Ϋƒ”ώŠJrΕΊ·τ0ό:΅’^ΖΚύ† Ά[.θέP„Φͺ9[ŒFݞφvaγγ1ξGœόε’DšΓ +#₯?$ΚX9(qO?£H-ƒB[γ™aΆΰ †C¬’Φ_\bοŎ›ΘλZœοZφ€hMGου{0“M3,ΚjB‘:6˜Α›‰[ueΕ;ΏdΡ–xYδΆ3ΠΜ€Ώϊ}˜σ} τ’8 ΰ–ΠΨΆ™ΈVčn`Gn]ώΤΏΰν©“{0@CχŒγi\o`.―Χ“"P•@•Φ<9εηKς°Ί*‚ΆUδθcνeΘmΗx +‰wόSWΕ"(…ΤNv]ͺ78Ηϋ +ŽρςŠΆk΅ΉθΑ₯Λ £ y€‰ί! fσKΑΕ²A*ž€]{hiΒ%ΖnMfΔGŒς™›ΊΣ©ΰπ€}OCXͺ‚•"#”g+.ΣPΌ”"‚U}>ΕƒτΓ₯∨Ÿΰ7\Žl<\q«›άε§ mΠPs‡Ar4ΊXwΪ¬aO"τMg>,ϋMw¨Ϊ*aΉδ@F“ψ{<«δ‡hVŒ1±αObo/:ƒSnž>σ―ΘΒAh9Ϋτ|zΤ“,&¨tfϋγE>ΰ£pœΜލpΤΓ&Ε‰Ν†|α†.ώΌK«_\α8½NaGΡρ˜A„/\sΖ‘—mΏ8TΆί#μI¬δ%xJNωŽ]U5θS‘΄χ›λNyC΅άλ Wεηӈ +endstream +endobj +853 0 obj +<< /Filter /FlateDecode /Length1 1374 /Length2 27197 /Length3 0 /Length 28158 >> +stream +xڌ·peίΊ=štG½c;Ω±mΫΆmΫΆν€cΫΆ:ΆΣ±Ρ±ύϊwΞΉχΎϋ―κ½ΪUk―ω}γӜc¬Z‹„@^‰FΐΨΞΠDΤΞΦ™†–ž($#¨Ξΐ€§g’₯§g([8[›ό—@’jβθdagΛωC9š8΅ 8ΚΨΩ%]¬ L@VN6Nzz #==Ην9ΒΖ@Z €­‰€DΘΞήΓΡΒΜάωoΊ’Q88Ψ¨°1q΄02°Κ8››Ψό­hd` T²3²0qφψ_)ȹ͝ν9ιθάάάh lœhνΝx)¨nΞζ@E'GWcΰ?#e lLώ3-€¨lnατo‡’©³›£ π―ΑΪΒΘΔΦιoˆ‹­±‰#πou ’„4PΞήΔφί`騁٠-Γ§ϋOτ?‰,ll`ddgco`λaak4΅°6Κ‰JΣ:»;S lX;Ωύ7p5°°60ό ψWλ@Q Αί 3Ÿ“‘£…½³­“…υ?3ύ“ζο6‹Ψ ΩΩؘΨ:;ώιOΨΒΡΔθοΎ{Πύηp­lνάl½ώkejaklϊΟΖ.φt*Ά.&ΒΑό5ώΗffβ d‘§§gcεš8M܍Μιώ) μaoς/'Γ?ζΏ3ψxΩΫΩMŽaβcajςχΰεdΰjtvt1ρρϊΏ;ώχ +ΐΐ4Ά0rš˜YΨώ'ϋ_³‰ιΏΧΟίΡΒ¨E—~ @ϊ~}§σ—aΖvΆΦΧΣ) I‰ Qύgδv + +ΪΉ½hX˜€4Œ, @&  =Πηη‘7°ψOτ+akjδψw»χιΏZvύΘ# +ΰΞ%kχ—Ή&@ς!Ί6= ½Ρί ΓoΊ+δεdω$ϊΩ‘¨‹΅υΏόδό?ό6ΦAόe‹σ_ΘΨύΥ‚ν U3ω·tν¬OŸ„³Α_-ؚYχ6Z8‰ZΈ›Λ[8™›.Ά«ό#4k [y;'‹-@zϊΓχW]FVN9ω/—Ι_ρόο’"ΆFvΖ¨Œ‘…hΰθhΰ K%F Γ_9›Έ‹Ε@:Z[;ηΏ!ΐΏΓωMνœ(ύ²ώWn#GΗΏ +ϋΧι-ό_λΙΩΔΔέΔ°ΆlgΔlΩάυ\'€νFs8Ν³@r¨–NAγ΅ζΨνς +™BQϋ3pΫρQ elac_„όΓλΌ½ 2¬#I‘σΝϋ]/Aqξ°°:‹φk¦θ\ qχ2‘χ‡ƒ·j€ΥχvΠ^I’<v8ωδg·!1χΖαŠί“‘Λ‡ +G΅¬RΠοσ41*ΡΪ₯‹$ω†ΩK„Ξ4ΈP”HΧξπ‹ HΉ3_ψ’ TŸ?1LΕ^š;Œ±/Kž›UʌN}˜Δ˜šΈί&ηH½OR%ΡWΌΚJβ$C£JLqu—[y»α ˜λ zΧΈ+&fv'0Nυ„*ΰχηBγŒφHΗN­•r{0ςΩJη?τ +N +=ΗhžQΥ‹?ƒψ~Y9_Θ3†e &Zγ?σAτœ„€°bόζπ'kJόςγOXe4ΜwΦκχ2p‡½θωΖφ|γ33#f •£u―¬qΑ«< ‚3Fd0Ξ`NΛ‘‘£6ϊ«ιˊ»­mψΰ}}‹€š²iOΚ©ίxLUΗΎϋβΕnθAž‡ +œί–Ζ ;ρα–w§h“ηPά;*Π +]1*°Θ3‘Έ5Ή#YW3Y€~/f6‚.PyΎ#ŠuηΣƒRτω9jT%ErΤ’Œ€’ Β–η³I¨.|€/ζ©}΅g)κΐ6δΧΤΜΒ΅ρ6B· « ”ΠΆ • υ΅ΝH>;Ό€yΜΓ€ΧΖό0•†Ιr[]Kι +ϊς…ZߜΊˆΝͺξƒύΞL>NΆ~„ΦoZϋNWl€–$œ>ςYΖ4ώͺψ‹\Ύ,ΈcΉlhmΎ7σΨώ9‰`–w㎣1ZΘΓ¦yλSώs#^‡κάΣ$CR+cό„M3Mεφ~ίs +ρZ²ͺ°&MmjMΖ’ΪΊTͺ}s-ΜeA[KΎ―²ΛSρυ²άy#jBΑKŽ―A)sQΠ1xτιΖ|ο0$5OΥD…ͺ(ΊΡΣ0Sέζ»Υ0΄ω=π λNΤc[¨ψmƒΜΓζΐΦGΩΎΨΕ^ΞλόR­ β©\cΞY #Τε Λ‘7ΖύIΗιΈ;ΞBŒƒΖ’Ρםκž)ߝO£<€ύe;Θ¦zΌςg΅>P’EεΥΌΰ«GιΘ)-鐇PΜI]Έμž&ίT ¨$1.ΐ_0­1aΈy©Ε}kt+GAUxk}‘Ά|)ΐ8ΠSΕriPτ‚©fΓ Ÿ»βm2-Ž^¦³@λSΚάσjV^NnρΝΔN±οΙk‚ΪœΔC٘ΛίΟe–…³v`'$Ώp”Ές˜‹τι©“ςˆ•}N‡$ηΐ*=F7fYζ.ΨRgοdA&ˆ»†QΚ‚¨Α“|Œ­μiEΠ^]ΤQΟGΑθHΕkͺΙz_³¦l[œΔζΤΐi¦αΕ±c{Li“ξxkYΥΜςm±~xy¨ŠDκ4Ξ`(šβ€ xšP£ŒΊ7|bςᦩ!9˜T½€=) +1ΦΛηb5Ν-{°l \ψί@=¨v*o`η%›RόΞΑ!~Ή>Y9Ξ.­ Δ•G `3]¦’³WΧ wφ²€jΕ)js›η mΉ₯wγίΌ1Λ '»™φϊ‰Ώ0Yˆψ6kΑΫόρ|JQD}&Ζ]x%’Ημ…2dο©H§φr +Gσ£3 Μ@Oό¨9ΙΖ<©ΒΔτ–¬«ΝXΛ!«΅Š6{YˆήG₯ΰ χO7iΕαojΉn4„T̍Δ[ΕΌσνEιbͺ¬n­ θ»Δ6 ‘άžš +ΨΊKΗτ²gŠ™Ώ)ΩΖ\€εΊ¦y ύ¦|/ίυ6%|Jπq³άƒC+Γx:•6]ψΈHλ§Κ¬ϊWτ²εHυ,@Ώ|:ξΌ=έdχFyέήΖyηΉκSΰ Π +‡βΰ0ε―₯H§`’žόΘθR3ω5d¦Ξo: f £wξ,(~E$(ΓΆψOΥCV2ώI" xχΦ–iBTβ_½VˆιZΚv4½+Υ}Χfa K!Ϊ_sEΥ +јi’@=F“:P*ΦΕiς΅mΟ¨–ΑR£f'-d3€ύeLό™Ά™­λL§†°κ1ΣλΧ;„Q +uWΕά¦ εE±Ζ3’&άeΜΫi‡erΆ™eΏ–‚eLcΟ/Ÿ(ύeη]υHdNΘΪ+'/―>±νiW’«j4δσpAA‚.½*e*ΗΏY°·$—]lR‰―₯i2ωκvτˍΎuQ¬lχˆVΆ²Fγν ΫiΏX€Ω☠€I¦xι}s©τΉP‘X7?07ŸΆy–I;…%‘=+KBhq‚WΉNc½‡Eσ©EΘ>GΙ RvΐΦΙ‚[\]J›ή™Š.•ςA‚L&Ϋ!LOκLδmΐξ ž9λΨVΙ8-ˆP¦2ΕF&YOήrΥ‰;’5w[mΔ /±ΏϋDk§°j9&Jb™[&ί`Έμτ4d«©Wσί’‘²‘j n5bJMΈAΗδ[8TΛυφ˜ΚV@ΗΦ_Ν3aGs¬Hβy΄c›Š#)Ψ‚Γo{ϊΕ*@:`ή‡’(ΚQ—«Ό γO^@š~MΌοΗ9qoή‹υτυ'σeΩw Y– ³Šύ‡$^τΊjπΧzΈΨή¨kk ŸΎηΞ9§€‰½β―Ι–1ψgΏ=ެJθΌ‰Mn'-ΫΡ΄–κϋ‘­o±ηP( ;=»ΛAμgΔ―½D„&P³χ}|μΙ‰ +iΊΐ/ςυ—^³SVΞ/₯τμ\jη2 Πϊ›ΉIΗΟv’¦[Q&Μw3„βάβ­(ŸΥίe£LΉΒV£ζϊ1ΪΉŽ.ΖΌΫ7\OνΨz=—¨;Q½Ή #«Ω¬Ε¨g|[ύ‚WΧΤ% +ΖΜ”ί‰ΔD°¬/s^ŸWPο•Η{5-Ϊ’*Τp‘{ύ­/$ξΰZfM‹!–¬ΎΕHBΠ‹¨„‰‰ΊVΦB Wπ$8Ÿψ³$Ά-jK’?εθ@Pd2<ͺύΖP}ο4Ί/ΚΧρ-$ +BˆKr…&”³(έƒ*†uή*ŽS)ξρ-rAA™5³RUAul#T@Φεή’όkΊ(Μ@,>Ϋ·$4ί$U~εi™Φ)«³-Ρb……‘E@©€…S?™³]±Λ0Ϝαόπ<αΰܟlά΅ό΄qt}Θ’Ώ%‚εMΜƒ2τwΏνάΧ{κ,™lΚ…φ¨ ­H +ͺ»ŸaŸSδ§σ2=WΊρ©4οƒ]yτ]έwl<%¨ύΞάω‘;Πm*_hΧzαί+³@₯KFwΙͺGχ[ΝΛ|ujμjŒ.b.σlςΈQΙgώσB2ΜzΞ)4r©ε rwΩύGΩV£―g–΄—ƒ€6ˆβΌ91E§§¬’€τ¦Ž₯nΞPςH"n3hό/<ŠyεYίY)Νο¨―ςτΰq’ςhΎδΌΎ{€ƒΕ[ύ{Ί_„PAΰ=έaκ,ΗΨ<ζίeη dVΚIbΐχ–]!6Μ\ωŽύ™YΩΔΠΞ >οšeο?ϊ- φδ˜qΛΊQΦz 7Ώ‹Ϊc@σEo=nnH―Όξά΄¨4#% ΪTν»όΠWϋ6ΆΤΫ–2Ά"¦06ΠeŠbΈ΅*`ΥMp΅μ™Ο²!θe…_ΩR‘œΨ:CE‚Γθ²|ώ’¨‘WΗd+΄V}^άς£i|‡Δt†ϋ$FŠM‘»Ά]`”Aϋ·™ω((μ‘ψ²π.zΚΩg™ -0ψ€—”ΰ&E²Ί€₯.ή/ΑT_ΛτεΝ„ΠœΠ ρΖθJΧΥ•¬5*nr„"+o~Έ³Χ·η<’qƒάΏπσΰη΄CάιQ@χ? 34½Όcο·πiƒ1`.Ψ'FΘZ44υOι˜ΓϊvϊΕξŸH©-Vž«δy†ή‚>DΛf‡Q―Υ +jΤǟGΕd2jή λsœBMώ:}΄ ‘¨ΊKj8VΜuο[Μ“-Ζ‘r―ηΆΙΓ#―{½FœΘΏδΆΞξκ#Hί©Ϊ‰IΪ$E@Kο|JQU`zύ°΄xΗ–k=g›)γv¨ΞύΪ{ Άχ9ΏνzΛ ƒ‘¨sœΣ{bΆ‹u+…Yό₯xΡΚρΖW"D$ΆΨζ~ρ&Σ΅:’֐z@+Σ½"0`wA#„Ώ5}ΗG½΅μŸi‚SΏX€M(ΞWJο(½G‘Hμ@@uΘY'RxΛΆ§‰ΰΗ2½e€Λ6ψoΟ{ΓτΊΐτ:μσJGΉ‹’ΩΘΊά’»9L3(ΫQ,uRσ©pΦψUΉžγ»ΧE΅iΖzέuŒηxP8O±Ϊ‹w …lήόtεΗ‡‡i·žαο­™ώ#– «Ψ1ΰcτ\0ο}ΤXύ2Ύε!Ά]]Δ€ώηEHΘ‘‰R§ήI΅΅B92λ„…]Yp@’(wc<ΣbΖ»ζχΟΝ=Y΄-]·iŒΝ ¨A―,R‘κhΞ+ |Δ)φM&Ž`Y}«„wq + Χwi=«ΦTͺάΒΚ½wιΩoΣά„^`z]kτ΅Ή°(ϋ΅ξ;vŠΧŸ;₯G¬Ώk–VUΠ‚ι|φλ p΄ šSL½³L΅ΔQ+¬7B™~Œkm‡yώ‚¦B”^i|”v¦RŸΟ…iίυ²ΪΕ€_Τ.φ΄c‰ °ƒˆ-i!Μ#Γ™6•zπϋ³©kεΑ DBž#©‘±vJ +”½‰=H]Aϋ +αΖ4νωδΑξϋƒΙH5B ΄jr>ζaφτΓ&$Ψω Lֈ*δoqF)E£%΅Κ›+A’M­»ΆdͺZ4‚²Wb¬_5±κθ>%_KLvU%Ήΐ‡Pf*rχoίξ΅^q~?χΞx†"­~ ]kVώ‘ "-ΛΡ(ζΛCe0έmOχΊΪIήψτ~_ψrδ¨0o›=Βέ+΄νž'–ςεޘpOίsρ0%ΊQ0BgŸ΅}!NΥ+ ¨ξψt PΡκ—1>Ύ8WθΙψά2mŠ€œmvγάά$š€#Υ9‘ιΰΚ’leMxxίΣ•˜χx3nΖ)e%h Α“Ϊ²ίI Ξά·΅οGΓ“φU¦ςv7Vc­iπ`€4¬DTg—+n Ίd›;fZ’qDC@H`&΄…H•Ό[CΒ†HΩΕGε’υ–ZΨ° Τ,™ε*t3tΏλ:G "xP£³Iού…&²ΣΕΰ|…†"„ξͺ`Š’YΊ¦e}Zy2Οξ!hRς“Ψ‘Vn+A|eτ€1‡B†¬X¬₯m Χ1¬quδκήe‰Έ―ίλπŒ‹Ηξ8•;ψΩάnƒΠ5{Ξ”λ@N%‡Λ]vœ±rOqž₯ƒςα›Ε’ +O!mh)£«eΪζ?•2hp€Φ†Q8δCwτ[rΜνΕzŸŽP0’Ζš<ΉEΙΥω(εvXWUΐ¦βΔΰ€ζJ§Ά†FG6deωͺ%VΓήlPΗF:τ£ͺι€xφΖΦiεδξiΥ:|ζLΒΘ%¬€Ύ) +H½«—I u>Yβ¨­\μνͺ̊žq€~!s'gƒΛP#Ϋmό\¦? BK5Ήι|ΆΉjύ’χΡ[δUΰ«Sh«q[4vΰ£I›ΡwR½šγ뜢€ γ“βΜ³$? +υLυzCfΚΑDNŽπωDρΔ«όγφΑ{΅z΄η“όάΜώΦϊψΕƒ¬`5o‘Ξ¬=R³8„ΕΓχIΞρkpH•ΟŸ.š€#3’$Φ`<‚ΙλtΫΆo'Ααb^‘$1Nκ8₯€ =0τσ;’ν¨:'/rL²xΦ9:υ‡ΛY˜Έ3¨R²SΏΜͺJy]πΊμ‘ΑSΰFΜ·yξPFφ˜‡Œk{Εί0α0 zu}+¬ΰCΝpAY:#ΜΡbρqO‹ B~"“„”kΨ[5I7˜Η~΄ΦeMDδ£.Ξ6Γ鍂=(„£α Ώ™΄ +ο™CΚ(Φ+γύh3ͺgƒhcτŒšd ˆκ°G->ΊΔNΥωLύ’’Υ3`<Ο·‡¬yΣISΤΚtσt—χM9‘H`Σ¬Β “ς«Ο\η°τ3Ά―΅ Ϊο Δ ¬IRUl›Ώ0pΗdf}8θ6·(G$›‹Κ`GΛ‹+=0άeάDΠ†5ƒ"Βps8φν=@Θμˆ™}©²:ΞJ˜§–μ· 0·τXŠr»=ΥΪPΗ—Ž$¦wΚ Χ>oP +ΥCm₯|מ§[φXF x ν|TX έ,5Q)λ ůtMcκ`ΪΣg„itdfΈΐίοδO$׊ΰΞφc»’ͺGγσξβ—αξΟ–©·ΏΘgJδw\―¨ΊΉΛοazώ¦ΐ7ˆ?Ίϊν>8qHAnΆ9T }½«γς˜ξνόF ε76ήWžͺt‹[Κ΅ζgΰ{‡&œ|ΆZ-4oŒγΪ₯ζƒνœ½f ͺνξžs‚­πa€Χ0ͺTΟπ/U9Ϊ8/pρ[‰˜‹)Ις·CβWχ Ό¨μ‹ +1%>fœ‡:š@Z HZΊ•tΓoΈο΅ά5‰TA; ½_ζ—š/ΊΌDβν™~ˆrΛAΆc] •£ Be9co=gΞΉfλ?q–R·žό͌D+M{)zrZΞΧΜ9ˆY5ݏ.vƒ ¨iaωN ³οyΰ<β8ώ£Ό§(>Vpψ—ω>Φ됷<σhuώ NqxC,8‡i΅IQΡφE•`Η9ηK%β3Ι,hFβdisΤhΔ3Υ Η5€r*ζ£ττ˜ΫFO90Ξo”I2gjUαFiy)HoξΕΗδΎj }±M™‰ζξ’ήυg` Ϊ§N„βΞE3(ΦΥΤοˆ8’GŒΰƒk˜άύdΙΘ'Ύ3rUΗΦϊώq–«΅{ku¦7]쉦™΄RrFHΥ*bΔ£΄νn›E%#՞θ•*~Χ°rMaKΰ™]p₯Zε!Δpω¦γ4Ξk§,}+¦€*ώv5lουTi€™ILPLύ[šz¦Ž +ψ½ ˆ6@ŒU#£¦₯ΧΟΪ—•Ό«Τ1ϋΰυXVγV»B:@Ν΄^Lš†vχε-;I»kUΫΗΟ―YΕw–$Κ―¬šυAΝ7²$›nΠ€>w OuΏσ²·šοΪΚΓν»[ν]?ΠAMDο·—%œ–rιρݚ̌(ŒΔh#ŽqR”d?2<χ;Š―Sz— +υ&TΪΏb©ΘŒ…;qϋœ ¬DBIsβ+€%njHώΊάΏ·±Ζώe ψn΅§,ϋ§σ~‹]7½@‰ιΚΚR{%ΆηTΝ”0ϊNΐσ$Όύψw΅ήBC—.!:4ξΘ6ΧΏ”ͺ”jΆ–‘ŠζΉ^"ΉζοCWj9Vιzΐ“0d²Ζ/Ωot~Ι !SRΣίDp=›mθX°΄‡1­ά%Ό +α€ώΑT—¬<Ρ.Ŧԟθi―³&‘Pς„ΕΫψεΣ*Ί‘ΗcsΉ„Φf;g>2 +vgfQϊkά_ubiΔ(H‰€Έ`ςν<:š@nS‡σΒk6s*’ 3φ‡Lˆ”«Aν+όΟΈb9‘1‹kP θ! 2ZK(EΏ³}ΙχμΉΆίκqΞςτ‘.ˆMŒ’e[`ρίDρFΙαβΧkA‰XΠεσG7ΪΆš—μ@:ƒ2O –.Δ‚³L9ˆμ―\X56fμhχX3'_‰όŒ«.ικ¨Hυ,θ:% +α@έu„½E«ΒkžO΄*eœύ>˜|₯b=Ί(ψš΄"·)_Kψ΅:°;rΉ"φ¬kʐW0φ"ˆυ2?Μb«Τ9ήΟI­{ŠήΞE[ΕEΪ»”ΘM°β:·6£Μ_‰ΧŽ!φgQ‚ε™γ3½”~Ψ j*τCψπc"υ[† €°K½2Σ€n”ς•VχΠΐ%!•KΟΑ±nδSσιΡA[|ΚpY‘Ζfαζ;jžrjz}€:D΄-Ψμρΰ&' +ƒί:yΩhΚείΧΙθ° –fΖθ~FͺΜόTƒσƒtέFa썒3lΉ# ¦ς†ΌΨΆυΣUσ¦νAV_Κ’™i3/[e½ƒΐgρV‡πrΏiΏαΛ!²ΚτΉX`μ£*‘Ύ’|&ηΫ('lFΜX¬#ν΅4\«ηV7!ƒNQˆs†ΟŸ#hž·‡ž +‰x{©%Κ,ω«ω’xι³ΑYwΚΆ8ΗΡ¦·5φςY΄4Ρ(T~΄ΡδŽdΥƒΦhτ(²¬‚K ‘Ω'N‘(¬¦ΧMmxcJπ²ΰFΨrν;ΗγΏΰ,bΨ,2Ις^Ud'œ?vΐλ!δF›9žk6δ?ΘΨΉW?Qώ¬x^B\ςΉΟrρ–ΡcNKkΨwADμ[hrΝ\—Σšb&?(zχœπIqλ+4B­Bl,”γd[Ν=BΞ^3-~}—ŒœaΛ^¨0;!Ιόκ±E†}ό±PΗ(ΗBςpΒ€YΫ4AΰΨΜZσΫ7ϊT/:t(Rͺ$w}uυl‰-†[Ιޝφ΄EάΜ ςω˜"ΐ§BEωΆ0ά›^–L³sΩzƒ—/Ξ‚kTqCƒ2LŽΊμŝaOϊ‡/ ώ΄VΒ°M­YIk™$bλτ ―#ϊ’’e}!am]ΊNYεn°ΡnŠC™rξF!―¦££H₯Ϋωι CωΟθΐoY"#Œ¦‡ϊΰŒο•ΊBuW΅ώΣ+γΥ9.eΈœ‰ηš~_uω\JqX7©‡>ςDŸ~“RRθ#ΌCN«Ϋφ’ΡbfΟ"χΧ4‚υŠ6fŸ4°)ιξΊ‹(©υΑ€ζCŸϊξGπ‘ήεΚλ+¬9ξI*ΰM 8uρa=siQϊٝ•<Ώp6σ犱m°Τnξ>:F”όBw8κO+=…ξτŒbα«Wι"1_·€PΟ>‹žtΰΊ`\?mςΫάΓΒ–άuV€¦©b•ƒPhC—Š[Ωλ ε^±Ϊ'›L Δ;);κ•Β(ϊ^z\Πuήeγ•’uΞmΒa…‰DΚΉΧT ΄ίYHύ‹"ΣZdΌzΜΉ”΄fάͺ¨,=“ξmβ₯κIQκ ½™όΦχTκ¦Ώ&n ΟΣӍ’75:ؘ"ΌDJT@oRΦΙΤ2.Π|8Ήψ Φ#hs€w¨+.ΜαwΟ?ί,­¦‘6V'RΆy1ΜεUlϋ±ΧΥg"# +(ί'Ϊώ΄’ΡX±_h™Ή[Πε«@HuMΤA“Π*ΎE©Λ‘Όrϊ+XλF––Οq―O°ΡC€ω'Χy`CΗ2ΟΨJ=ζΌΏ©mDμάπώΌYGΛβ=zIQώ#Λv%‰Pše€―P`,“Mκρσ p]8&[gΠΓμGG²ΐ Σόή«NμΈν›y· +ύ±ŸŒ|Ιw*XΪΕb˜tvζ8ΝΩ”!ΧhρBr]u!rθa‚eY=G-υΛΟ½σΟ£…Ύ•ιύU &]Σ“ξΒRBC!HH%E]¨θxrΥXΆ"q‡§¬˜Y;K9]"zΩρΕΞΛΘΔ?FιιΠε‹>A9?„Φν7Μψ+ΠvΡ~MOrυ2D^䎣…m{¦υξμ€™ΪΝ&Φ6‚απΑΡ7o!>ΏΒ¦D:£¨λM//†ŽψJŸNNNO-ͺmΰΝƒGK~λ/“?Ψ‰[ω'ίω-ξˆ1ςŒ2ξώtoA!j–+F¬ΡaίqΛ69ζ~Γ"ΗJr]βV/σ!½ξωd-Ž-s…W§±o, ξ5Ώ§–I2Ι yŒ°”! KZύϋιŽ*…Υβhώ5•ͺ=mΏ›α^k¨ψ;Wo*‡oŠjΔοOW΅Qͺ­C>r.§ˆ[ΥΘ ΧLωΚ°ΚήZ‰E²»oB,|ΦΣΓ7”ΜϊΦˆyo¬JXdιv(§ξϋΤώ‚Y₯JEO»ψΒ©ά&ζrνΎuL?noΘΚρ‹–½Ίφ€kΜaΖγ˜‡*ΊρϞKP+ΞΆj |”UoΦΌ8ϊδ›²ωΟ^ΘεΟ΄!?šoδ/υš ΧΊ YsΥRŸΔ:π¨Y2…Βq?β·ω³σ•ζ:5K}Θč;Ψαώ€JŸ½EŒ€z Q°•#ΧΧ5l’Θ9ώ‘Ž_k”„‹Ž+>l+‰σΠ€w'5QΘΞΎΖΨ“ύ°-Ο‡8Vhι½eζU^`ΜiΟ°χ$Μ‘ερΫͺςέ5ŠΎq*Q ιΎπ:w„…Θ>μΣ5O†ΐ@!8|dλh`Μ’!Κ/s™}ω£/ό’Xtεv•Έ€ΨΥύ—wPύ¨>^R>{ηjz]Ώ…5(tI·a`_”£twο Ά―FCAb₯²%Ηι[λλΐ R™˜Ί3΄J2Α³³γˆ₯§₯πΤ¬1tšΓ Άnρέ;j¦Ω^xِ\„Σ)tgΘp–󣦭Τι2―·K-ς+Ξ†s7‘aͺ†WψMŽμgΜ+Uθ,_šλ€―ξΐγφΗάR³„Φ€χΘΓο80ήƒ.Gqx­”Ϊj‘€-ζ―αθ σ*q*Q}Ž μ=ΞΉDD“ϋNΕO*€jzl€Δξ'ώLUzsB³ΈΜ€8ή"cQO|6[[Fo―ίΌŠDG=_Tb”ΤώJ§y ΤΌψS(­(Ο&γMΛbš|'¬όm°m}Υυ4χ1Η4T–KΧ‹vQ„ƒ1‡$ZE₯οήFjnS +bΫά'Γ·Γ„£ŽψV7Β².5—Γp 2σΈ΅ΛsΊR5xL΄±Λϊ΄@£Jlmx«ͺLΩWFίΓβ=0`P€‹XQΗHμTn¦xdΚΟ€e‘O,΅Ζšž*›7’δ_‹•θΦυR FΨ1’€+΅³ήΐlŒG.ά78₯jSΝΪ‰ZݘhY3ΉŽ0³λή³q*DGηRFΒξ<ͺ# Σ֝FڎĽΉΓH/ͺP‘ΤOΈΠŠ΅UΊGx‘ާb>ΊΛ‘#~I©Δ.›©Eε_τ“Ειzχ”¬ΕwjΝΩΜ G‹„λI z©εσ:C―°g―ϋ+©ηBiggΓ«ϊ]T‘ύ:(&+δ2!—Λƒ~"^½Δ·Σ-BΦeԈϨY2,y»ΫKODO!’}RnΧ—BdEΒ!@ΕΜˆzS€φμ”½ΆQdŸZΓGΞψΥˆκ£ k)Q+Ε6ΙZ(*j+λ,§ω›KkNtŒο™λ§ΈΜ―†tήΊ¦{cψSϊτ&θϋΠχKPύ6ͺΧζNZ§‹’½ΪΠ+ΖΎ&vjjJΆυQ˜3€/ζ‡/Χ>ηAg©\*΅Γ(μžύΊι ½\ΔHωLbx_ίΤ!μ%4μεδ†=¨ΘΙ#Eς±}Τ~Ξ’γayϋ΄Zws ap +>7€± +!ŸΣ'5rκ“ί_wΤ6BTœo?ο_g υΑC²ύr?ς₯x+Ξζaχ&mDœΔΣdKΎ± ŠΖƒNuπVΪ΅YŽ/ήHΧω%Οb﷊ΕdXlHΖh²Ιθ|Ciδ!ΝΗsέᝠ+…K΄έ;°K*hј/:pmβΟlŒ· ‰‚Fm>3 dΆ°mE,€ΦΣfΟΘϋΕ۞8…kg1 +Ocλ‘±=ώ“pX*³ήΠ€΅=q‡|U%#‡Πšwxo„†k^ΓχΤM•?ή5 ς΄)nFz$g5ϋ χχ₯ψ ¨"N:Έ +•XR<]Η»_u­Q‘N‘6#ΐ'Ύ£:η‹"Μ‹y%<‚ΓDΕ ΗΜΪά‰GXσš»>z.P*;Ί§‚1jΌΖ ^ψ=O½½”ίΣ<˜ΦRϋLΑ+ϊ£ΖJsCΚ―ξ\§‚ͺ T!±IV“ϋ21ΰΟ–ΉΰΔχηηo„IkΊSΉτxΜΏcΝlΐ€uA Y¬%―—+Έ’7aίIt[w}αV DΣLd έM¨rςy>`‡%πίH0ƒ1#ΈQ^ Ν\wAρXaΣΘ‰λC$Ι`7ρuœλ]Š΄'ω—ή–C2X²]νlˆεR6I1i›<Ρ-έ„=" 9g;ε©ΠV‘S” Q1Ud q«">_€t‘šΙ½σ{ŽPΦΐ\£[΅Άά„¬ξˆ0„Η“₯XοΝoΥ&…±ΪΩδ+‚η9_¨†ΤΔUnzHζKwtaΊmeCWΧF.Ψ¦ΊV²ρM OV%WΕ2yDDmίGxΟdό©ΛιM6κxρo{κ] mΠΪω―4ΠδΆ*―<»Ώο0γ4 +n@ οΈ5Ώ―rδσ4’‹±μ;TΫ£αjvΡqU/τuκ%o°qκj_ΡiβπvPωΚΘ.¬œη4ΓW½Xκ›?|Υ_^ΩΣ`\·­#cS?₯΄νOΑ³Bγ[ ΒuΞu·8lΟΡ#Φ‚χέ3^DM‘Mς™]gΜΝ9{ΑWQΨΧfeΧΓ·Μ1ν4›ρ“μb‘y&iω~π\Ϊg4­Z‰­Λ:/+ΙWgωso qfς΄fRε#aŸ‚?9Oζ2΄ϋ:δi —Ί΄&#†›ϊ½šώ>ΈC@l€™σΥ "’>iA‚dΟ3ΚOjݞ|žζ0‹#ύυ}BΔ2‰k~j¬βœ”ΩEζVC΄©Mg7O ~+ΰXό¦ ΰχΕ?6 ιwLΩ°pα‚Μ¦†b,“9ΥΛΩ-²0Βτ—‘˜Υ†1O²E 8ιZjŠjV₯.΅žΪ“πK7†+fqrΩ-r$ι-–೑"ύP ΅ιitN―Φ!Aφ»¨W4†θηV E*φ^!gC¦LUΕ5­yIF¨>v '?ί½ +>‘ON±Ξ2ΉœQmΞί„#«Ϋοζ“„ώθͺ¦ρ‚-„ωΓ·Rό@]H7Ռιvόΰ1xύ‘6ςΗ¬:tV›6΄•0;˜œΠ©9FˆΥ±χΣ£Σ&Γ^±PΡ~‚«Α4δΠ―/ͺ‘“λΎ²W}\Q~1ψiνQJΌaRΊ³ξλϊOΥ™ν2ƒΛg•­Q·ΘBF4|φEΒθΕyYL{⸹܌;C.+Ύϊ½~`©:Σ]Ω=#[bm[^Ώ3!ν'δςϊη4ƒΞ3 z”_wΦ¦θΠ…c|­‰)šώΓ;m±{:Δƒ• Rυh¬ΰv7MΠ,=ί4E4™εxΧ^+”Υ&<’Όα—ηώω:„%ƒ]Ε}ΦΙ“ί±iωoƒ”eαύnχzI‚²(·›aRpφNώ!«Œϊοh‹œ–Η1€eM Ύv΅)λ³ΣΑQHρ¬·THH`ϊ΅βA¨?`ψ@@V*ϋΈΘ.τJjΨρ1‰5›‹ˆΗ`ιC‚)†a~αν +½ϋΒQΌωψžnΪbο₯@_‹ω=; +I,Γh#ή¦±X˜Ή’έw°kl^Tt‘ϋζ‘‹°kλέͺŽ#οΪ4ζ™žTΎ―­·Έ9z–U‘ώ}{ό9λk€ήϊΜΏwLάYρeηΞ₯γ+ŠgΩHςΦο!–ž¨σ¦qiΰDO-™rMΖz‡H³φt² Ηβ_Ω +Χ5[x²Q€!ΞήηM¬xA†Ω}NΘTzΌ&Β³~7ψβςΣήŸλ™°δ–¨IV7k ΏΩcέy2’ +G¦j’ΠΣΈ…dΤ‹GĊ$\}wΫP­7DΕΒ_’!ψ3§g„ ‡•¨yΌT?NS›G‰0SΉΚ£IY§Σ5‘0―€ΚA›\ζωœθ‡° ±V¨ͺΞωjζLΛ‡‰1ΚuΪγiΗFό9Ωt κ2ͺκ*(pΧΞβΖeόjL¨χύ%Μ S„Αˆjc‡—( Š?ν€7'Ε?'oqΕDΊƒ8ξδϊέͺˆ‹-•IqΰLmρ;-ΓYMc¦EgJΊMδ Ωμ$νΗΈg…fΡfϋ»ωι Dξό²]j?ΉŒθ1Y&ΙvύχΑΘso4Ά“ΔνŸ}›‡„Px&ŒΠ3“D…†γΫb τmtLŸ/0Cσpβ3γGQ#σζοߌζpώϊ0ΆmŠ_zΪ»£άC©ΰ/Ζ(½κΣ(kbi“‰βqG^ήΒR„―Tď;5ω°HρϊκΆn;J’ί~žIΓaCςG::2΅ΉDn· +‰@š?Άη¬Ξώ8Οί‹”.(Ν­›Ω6iTQyξ»`μοkk!|<**•€Ί4MΎŠΠδ8 –˜m(Ηιlt±X ; +Μ° υΎMΜρ|Οκfy¦ΕnplΨgτbQ•Z’εί^“ΰηΌ™]uΓk㱦€w~„τΪ@ΒΆKσά +@’(]ΘiA?™ΪyΗtmGnΤ†$Κ'*αBL/ΧΩΕ5rΐ―'έΎ\ρ‡Tβ`Κ©7ͺΊΟRλ/³-iΓΦq@ΦeŸz„ώΦ9nΉ3ObUnσU)ώ’· °P4ΟkσBσ©ύER₯Α―ΰϊ [[ά:ητ‘B—»Θ} T`”H]Ύσ€G΄@X}† p#W°6I±ΪŒR8cγMαy+΄ύ;}αHλ•Β(Γ’~yŒ1€ΏUυ6δό|G‡­Ύ_Ο ΞρνcΐΧλΠO[Ψ<«ͺωωs35ΨY—ΔΤR2₯₯Ά…Ά―4΄PπuηB-’ΦΆ ρ|Όf{υυm²Ρξ5‘ΐoa€0?ŒΡΣ…ψΗW‘—u_5Λ;zti+ŠΔC•«χus#}=’ΪΟX8Ÿˆm•„aH-$1h-χUυδξc?°Ύ_o²€'Ÿ}H¨ ΝώΒΰΖ#ΫΰΤτz ’[k₯‘Ί@fΙmλ΅/Χw™’d¦ίΉIη>j‘7 +­ͺyž%«©ά”Β$Jψχ8Hi¬δΘυπΟθWo jραOΑήfΝΌ<Υ¬Βd00Λ3I~vοΈXΟ$"_\kΗ2 ΰΥV‘›ΟΌ}Yοφ»Έ!ͺ™Η~%½Όϊκι€μBνσ[wƒw±Ϋe$Βw³—,/gό*ΗO< ‹΅‘?x]eμ©Έ)Οu•q9θυ!*‚·ΠAU )zμ³ ϊ]τRτΫL³Σ=uρϋBόΜm½4CAέ!$νL‡κCγοP·&ύ°νyma­ς4©*ŽεΤ3τň@E­ψs“ +ηϊ‘εώ6―¨6‡e¦:μΕ‡Άπ€mΧμPgβ;²ς·’ρΐ»€± +{―I"z]ΊΙοΪ{g¦"b<"\0Ζ±¨’•υΟ8ŸΑjβKιΖ@I`?ŠΐNcŒcύφΗΦο†ΫΦσΊNšΊ7ˆΈΒ.–uφ²ZO#Tfx Δ1ν7κ3Fηqy˜'˜©vUρΜιΧυS5ν{°°+C½c΅νωίΕ©Λ\1䏨ϋΫιo²fϊάP -Δα]˜yσQ~o,cκsΓΒ/ +([HYšxΓφΟ©}<ǁΤY/ε:E™HkX3,nηIVKήνI₯Σ^hςπYD‘x3QŠΛΔAͺΏ%­-όвΘτ kVΠΏ/!ΐ`EΞʎΐ«¦λΪΛΞ+ΘͺΘΑωS7|iιF— ΞCs?§–!Ÿ―ω\ WϋNˆΞ‘7_n!Ϋλn‘&Lu΅΄οHœ\•JI禧‡κϊΛ+Œ³ω¦[ΫЎ=Ώ/YΓ{§ΐ˜‘kuͺΈ_0˜Ε Σνs©4\δ¨2`i·Σ½λ2+‰ρŸχΧέβ―»e§ο0>ωΆc–aE*S 0y6›‰ssΰΠe֏6!ώΠώZΟ2‡—'ρ½Ολƒ]ΨΫρ(XUΓ“’Θ+#"Β5ž¨ϝκ²W~“ͺZ$BΡbΝ/F)ZœƒΎΝ¬Λ Je 4,dΫšΖ§™=C‘|*ο‚kΜγ`ψύ΅–ΦAš–˜΅Άƒϊ²HG?ΊnύziξmdŠ΄8G)SΝh̏?;@zuΖΛ5Βλλδͺϊά( + Βψψ^gž†0_ss½Ξ§kEWŸμςΚ±†Wασ. Nξή_χœΪSrKAWy%ZF|s^…ρCdŠ!S>λο㱫ΌΠ™ q£]pw") +εsœ`έS«¦VT~˜vPTd*­΅žΆx™ͺAM—q²Ϊπ¬1¨@4ζawΦ!ΩΉ¦£x”π1Θ8HpσJ7Yd]‘€wb©ΘΙ­φ$>ια=Ρυρξ«yΘf’PuV΄PNΊΎπ\HωΓl>܁Գ@ύ9vλCΰχϊρ$b™ hσ›ΰΐΊ†ζ²ι2? πά'Ώβ‚Œ<ϊb3`‘±ςύ§Œ-³iHΒxNη Ζs§Ψ*μ±Xίβ«© Uώόπ₯‰o (Β•`ΣΣφ3Mςέ,P‚‚’g)κŸG©₯ŒέχBξ©h±ξυX]€Ηs₯λΞπΌΝYIR0Ž‘Ξ#nΏkrg%Έ·—¦δS[χΗIΥ`”ŽL›6ŠΆΡ'‘EL „%VR{5<>ή.}§\YN?™«Ζƒ& +>^FnA-FΔЈΊ( (ΐ±π›Ηq«(EuQΥf$™εЍΫΦXΠY!€b₯Z«ˆ…κzαMŽ,Ζθ―>Ϊ‹'­Ν)y‘7QžΉl`\δΧvZΠ‚φΆ¦;9ΎυeD珂½ΙŽ™z…Λe|'ŽΑ^EΞ—kI-'.'Ζο6«‹ΔSιŠeTž€‡UŽ΅ηώ‚aπ^b·‹mꏁόY6Έ 8ί 7ž›ΰ³’VrΫe‘I*Ρ₯vΠ»[*9!ŽιΛ(έGνδ:@܏% z¦ΰRΥΡΥγ_f,wλ*SpΆ6†Δζρl‹V΄ο8΄x«FΞDTxZ–WŸ:X¨U –WGΟψˆ.=BGz$i:ξώΘΗb‘)iΕ†Oδ…0Ω¬tίΐ˜Žd’9©Ψο’qίΉ/XšA#»ΙΓΛ3ο]{ΩΆ#€Sš1'i΅xŒ-έ`₯ΛθτγENj!τΈκͺFΚu ZHΒή#А‚Ή•‹;Ά/Ϊs‘ΟN±¨Fwp’Xm—ΩT₯λΠ>)pΑeŸ_βΡΎΜυ_ +θ2τfQsιfώeυ“Μ +π‰cNϋ•ΪKM·{,ΰ\€‘¦όfxρ!΅[h³'γ2° Ÿ ‰_u3Σp‹Ž—]Ήσγ‡pSεtθάvΎΰ=ψ^¬e•G/eΛΦHK‚mwΖΈyrφσU³™V},Ιρ±ΎSg~r†ϋ΄Μ5£:koqpž 7”#VΙ8 ^B! ©ώ ²ΪG#+T)²heό ‘P\Ψ•¨΅Α-[,TS‚}E2υτ=κτF—~Ι,ΐδΫ<¦ΕΞέ*ύdQΓ£eEΝ½ͺζε–±έ=•JώTηωξΜ£«zpΖΰΕ°pκ¬ξπ η{ΛάW/‘l…gS=žLAQ{…ƒμ][)ΠQ‘"AΉΑ£΄"e&Υ·|Œz<:žwN‡ή"CAJ΄pχyΦΓ9聫k6vЙځ‘”@΄Γa!~Σμ}ΏˆJΨ8?Φ©‘S{ψή60zο'8ύψs™:A£ϊ`]ψDϊW/c3« 1žXχώͺYβ‘=κύ‡ΰLη˜2€@ΣψΞ|”j\ϊŸDΡ쀑DΠ\ι";&<ΠSωΊψxͺ5攃ΐ¨{ͺρώ²7ρ¨Lαυ)ίBέ}=wXHEφΰP)fnidIΏA4O“Y§ |CZYΏpπ˜/8΄phΕ†/U:κΓFhήsΆ_o.vΆέ“nωܟΝ+ŽΤθ₯`;ΏŸY@ψΜΩοSKι½WΕΝ'£wρθlp[BUŚθ5-­]n””ΨτΎV₯>Δ Δvpg&fͺϊί[ά™„kJ™έ¨*ešΔ³——?œώ™ΛΚΰΑ˜ΡJj| €ΨηN:Aΐl0FœO·M +‘=ΝωJψCJ`§nέpPαυΘ@}eΣό ˜°Xμc«››‰Ι@«]τB¬“Ω@ΗsYπΏ2C²ωψΉ”6C J1s²cž"ΐ;εumνGΫΪη\Ζg'‰¬_i΄Q}ƒ`¨…vΩsΤX&颏ˆΕγ³οϋ½&‰8aMΗw—‹gQ¦ st(M{5ΚοιπAΊ±ε@!β·°Αn€ύΡ °˜}€ωͺE‘‚UΓΑώΉTΉωθϊσ‘ΨPm<ΈΏΘυηgχ²§Œ•2Ιb!l n"Bw³ƒ_8ώΊ(λΏΩ€λbΛ?:&Ϊpεz§$'+χώ‹Έρ Εklπ?N'#zΦM#†Kd +νnX˜>Œώ”,α™‹jʏ Pρ'²»’>ZΫIg»FWΜNRΚ­œϊ·™t6ΟP£ESS&&-η΅.‰»τ0˜Qδ­c+•LέΒ|ςfέΔάϋ_x(ίͺ\ΑεXt0M‚Nœ†…Rζ +73.²P’:RiΥ EΝƒCƒ/tώA1₯‰―™ήΜ3φιήΔOβ“ε9£Ϊθ€j!λyΪƒjήΙ±…#gœŠƒτeEέZτŒ€X⑎Xε”ƒΔ’š΄΅y!ζπA*λ’θO“Mš ϊa‡7³ΑΦ‹€Φ1¦μ¬PόξξϋΚ‹Cς;7na…ΛΣ™―“έΗ sc’€B͝ 6Ζoδ5’±„u-‘˜¦S_ »‘`”Ή”4’fΘ| ό‹¨ϋϊ›Χΰ[Ÿ+ΉθMΰΈVΏΩΌΚ;κ +bb§Z Δ-!‘»Ϋ£žMΫ₯ |ά 06Te#β,uηyb ΆΤ•ξΣ½LίFΰμt;ρΛ £Y›! ΚVή ΫΊPά +?τΌ^'Tέg-#m\Z 9΅)»R8! YPϋ54Έƒh%Ξκ Jκ%Ι~‰άζΩ†‚˜4·d¬|iβK™PΒa‡(Κ\<·φΕΩz8ήΦo<ΘyR7 DΖ³yςοgώ„V³X“ŽK‰5†Ύ5ikοM™τ`=τλxηΆͺ3©hϋΐΡ―ά—δΚΝ¬'–ή΅›ΝΐΚοΌο”4J΍jΖγ~ΧcH€‚ύƒA'mš’―*j3βͺz…½Š›jϊa§φf +%γc ±Ω? ‚œ`YΥPΗάO#Ob4šjΥΛ΅·vΎ‡. *Ζ’03kϊ€Χ§2 +Ϊ*ύ~λΌ3sw‡Ηζ¦―Ζd|ΔsLϊ„1Ώ&ΆuΩ΄Ζ$B;hƒi©V‚-μFΩt<έΦm”ΌιβπoIΜ š#.\«tό©^Pτ9±ΜXϋΌ 4oΞGζ±$`<ή[fCƒΕ —ΨΩε―(ZΜ <τϋŠμΆhVάϬꯜœauήfΪˆKH½/L葆-ώ-φ-ΑΤ¨ΖέXηυ–ˆΟν εΧ+‡ v +ρ*DͺUΖ#n7εKjX·»(J 2ζρZŽσYχ?³?šH”X*ψ(•;η†qqlΔI΅ΐ’¬灨-8,“‡<ιΗ)μQ™Σφ‘σ«η’$ϊΐy4$›ΎN 4υΜZΫ‘Β’.ΚMΎN’‘ΨCδα£(α›xδΙ£„V.οώ—:g¬ςπx[—¦bEF―'»b큒_ΏτΓΙμΟW†AD+ζb„Jj‹f­q’# q gxVΕaY‰MneA.τLΒ 9Ω?–’"·LhΖ.ͺ»ΈUΚ=ΞŽdΘm ͺpμπ)†G|ςΰ,‹pύC•M&8γΌΆπ?Έ]Ž.?8Q‘nόˆŒw\=‘Bα gd—‡ϋΑc{νΒςu`y˜‘ΪŒ3%F£Wθf?‹έWͺd8mGοš`jσ@„βέx΄†Ψ&leqΏ#eβ¬U΄*›λaγ±ϊœυ&παΣ·-ΫwžτU­Ρή ¦_•0§KηΉ’ύB’Tλ}ΑrE-Ÿ+›CνΛΓ–τΖ¬ƒ€Τ‡I7ˆ7€‘ρϋ3†šΟ‹Ζ›ΤF u ,ζIϊ!˜4GκΉRΣGΝμŽƒMΪΰΓ*3“Ε ˜EΓ–AνεΨb–g½¬ή§Ω•‚})0ΓΌΒ Wy—«³/IŒUs'X¨B’ŒιvΤΖe:Fia’"δ}ͺαF±ά{=δ}ΗΟΣc¬œ¬9ύθπ­3š•vς —Μ‚f~s6 +’&ΨT™ΊφŸ+σl›3…ηuη>σFsξ΅rUk(― λpKΤΙlπHκ³­Μ?›―• yTβ·1βΊκ&ΉΕ3n‰dΔw"H9'Yχ%ŠΥV˜γΦμ$VΘ׌ΦYmήB•θ]€Mj§…ΊI›‘OιάPOΔ”‘ΏNvNρέJΡpu&8?δ§²cpœPΔώ™Ÿβσω§έF’ΗTaί„²ΤΦλsߎž|}―‚˜ms‹υuτΊaα +kG cδs2› aVπ©σΰλ[ εΟ?e@2š8@ΐœ2^βΉ/‘5›M ¬1œ`άΪgd½&φΕ%%Šγ™ͺ>¨Υ©΄dΞ,5ζ|0ŽΓϋουͺσK„ΆΫ4ω gƒΟq9˜QΘΈ=ιΧ›‰΅1}SEλΡ^ν†Ηφ€ΡnΣEhυeFD*ιušBΛ`υ+Ί™]@xρu’οh|³vχΡΝωvφΘέΊm/$—ήpϊ”?ω©υθΥ³JTuxΌo‚HI­K σ¨…ϊ’©b¦°ίΐH+άR5eʞΏ»‹zٝXO£Ηš%I‚,01ˆpόŒY¦ϊιΩs7·F¬DPΛO‚“bΙχΥ*|U@”V₯u1WΧζYeW xθ^ωΓpΝE”°—0ΗρdύΈXάΫ6ΜwAΆ5χuΌζ£ƒ0…4—„9Τ\#ΩY Εza“ΨίςuΊυ;mͺŸ;μR°»¦“Ψ#gz‘ΣRχψoUΚ,Δ/sBΏ}\Œ|DξΆ&?-¬ΘΐaO‚9"ސp·z-σ}™₯ύ‹γΥ/M οΤΣ»ΐ$yΗζky‘"}0Ρ,TψI°nCL“ωVά €F+Ύ•SλζP0h½dβ„Ο^#}cAͺv‡tά₯š.:JΡ{©΅€‹4ΊΗΠ‚* …—š«$RBΑΨJ[ͺΓ/=˜ž=NΆ=Šx9wmκŽo„‚σr†=¬£‚—.o‚F(ήU>swΝδΉύ_!τ &BwwΓ«“J—;·eh)Ο x“σϋU7ιow!ώ¦‘/ξτCΕR)QgF έžcŒ£‰73%mB‘- ΠαVKH"ώˆΞ(μ‹s:φν8Œ―œ  :Th―ΗΘ]„,uσΤ֐άΕmWλιΌmυg]ΤΣhΨx7$ρ λ:…α‹\.Z‘“'`?+*v€Rfκ–(ab[§Cξ-Ϋʎe?QΚ+χ+}JΫΒg’I+•šΡΟ·fd³§·pKΚ]`¦Z r’.ΐIiΐRXΓώR™/ΈεΞs7(,Ÿοϋ§ϋyy€boΟJ@ΓIξf3Ψ»$BP,0ηΘ›”—> ©J…·sVΆ¦­·hƒΔόžΦq +K΅ΫœνΪΖ…Ο«—T7΄Υ-iέΠ°ΠΙψf€ωξ,žrœ z‡\ι―‚_sΖη>DI§±\―;’&Ε«ΣVΐ6Ρ¨ +;ΌE]ξ[΅Ϋ’UΤτŽζ\IqCC‡:@£Ξ¦Ynœή²T¬λ½#Χc#†ΉγͺJL q,σrΔ›Ή³―»†ΕQ₯Φσ23Οχsω•ώύzβg› Ω#υ9yπ—ˌw˜ΛζήeΕήΚκR³©λv‚ Ά"e‹y€αγϋ+ωΝΔfSS0θafCΏο―«ΫaβηŸ₯αΥΉDΈͺK΄4’ίξ\ϊƒGΔ¦νNΠ0”&‡Κ,{©σΖφέ€s…Γ³1Χj*Y‹ŠυDΉΤˆΆθδ_ΜG}/>>nOtšΝpŽΑ€Xg‚Φ˜CΕD’ΘœƒzΩT5(…ΛΓ>BόΜqWTX„_2t«κtb_Π[iλΪ‡Ξ퐦€:‘ ¬?ͺISΜMρ¦@œ\%pΠd~ΚΏa‹½ͺΆΓj„"ο¬&ΕΪwζΰC°1Δ +Θ*ŒAGi— +9P₯uΥ ˆ«šh§ͺΘ³β¨JS8=qŽ_!Γϊ ρ'‘G˜›θpo©]b$xΏΛξπχjΆmφ˜ +ΰ]αΨxΤ—!Vζ7’οΚ•Ό‹5Μ9%ΌSBNl4ΝR§†ρ§m*λOA L„ώ-Λ‚”΄–h›qΪBN³lωΪF™–^G‹¬QΡed³Τd©iβξΗ|vˆ~Λ\νhΐ€Γ^J š€εB€Β+™F•&ΆΠδ©ή ̍αΠ%/sžV˜€”’L·ΊN΅R +ίΝς_ϊΠk΄©Vν -™ κoRμ Ձφ˜)jEqwŸΛ/OΑΈlτ%ؐΘώ••Š‘v― •JΡ%N$)[Y΅κχM₯Φ†υpY0Βζ+¨²‘χoΠ"Ζoa³ ΒΓCFΧ-]=Θ€έ_)«Ζr|vΣ4IW{-Υψl!~΄ΑFξΕΩΕ…Ύd:1c«UyνWžˆ}Ώ€[jP‰VΙ9½©ΆŸ~ Ί*LΨΫ΅Q1ιφ£¦ˆ£ά/&jϊμC_ωZ„G„€_“›’6k­>~=?[]Τ”zπ°κzύ±Ρ.v/„βο‘;ωΪ.S6δ₯E‰Ύy¦nl)n $r‰[`D•ΧS²°qΔμj‡uΊΚΡΰ‘ΌϊϊΧiR;Ψν‘ί<`’p[AΘ[ίΔ£)ΎFλςŒθ³,ZJε2β?΅Œ•€΄ως)^dZ·σ8·9}°[Ga<6ž’ΗF€Bj–ζ/‘˜±}gD…nhš»φ±nΤρΰC‘€qΨƒH5"θRτq έχ{Ήmϋ†8eΝλ 87*φaϊX,‘ΞDΩ‹@’υFέΖM64šοaĝηNͺVb·Ψz]Ί)½ύ„₯1BώYβΤ²EBšΒz2Η‘ϋ•ˆγ΄Υjγ3ε)"χŽ:Ε0 ΞYRAΌΉ Αbτu4:a ˆ;mε&―£A ŒΚe‹= 4ΤjΪ€€χŽUΰπθ½ί΅ί~σΔ§ž©Os—‰ΨSτ™H'{λ1θ~‘ oά—uίρSHU‚FΓ&Q1ΰ κfχʟs£Ή1±ch΅=„8W΅{™?πWkΏ±`Š>ς: ’aΌO²Ρ£mΡΈξ g”β,Ɲ12šΎ2 r9.’‰{eί”Χa½¦γξ—Π΄ά>”C βσκ„{§bαΞωTΝΔιΕβΆb½F+ΌVά›Ω­ά0Λzm…Εξ–·˜θ§fΠv5zm‚σζT’Vd`ž}€J}lρΨΒQ—Ϋ֎XuJυψ–eΨχSSΏ ³ηDπΎΜf +fε`γωΘq‚ύ’λο-‡₯ΝCyX +‚―]Œ­+ύR4uΗz}vκG‘(ηΰΰ%Β1ίPPC°ρ³ϊΚ!K¨Šηή6ΐ)!£@ξΰβ6h3χΗΓςΪE₯4ηRt~Γ΄ΪJδL”˜r(h΅4Hp›‘½OdŠHGδπ€γ^φϋ―£Y]XqΗ‘„(#‚ϋTβ1ΫΎ›Z/O΄ϋE1Z#}¨Αίk΄2q†W`ΖRκdSšˆ’ο{93Ό‚†ΪΘ”mHKΜT ΜΗζσ'j˜£SnŒ~§ΫΤ«z<ΝΠ²jNq#0ΰ/˜ώi:³άΗβδqΠvH'€6ΓΏ[n’YNa"‹Ρ‘H}_κίΏάώςy ΠmΌΒΊ±n$.ž9y„큄l‚΄ώ9ηί _πΰψ¨Y|΅Γχ'BΈHΡ>QκωΉ­8…ζΛ yάΤμm―:.΅ψ₯«5YJLιΒSˆΎΊU +Ϊ'ΨB£ζ +~\΅;UΞl³Œtn„ςI•εΈbM,'G?εzo…χIQ F$KΨ^κνGa|$:Ϊ?ͺ:΄ύΑ&ΏΊςε'7=―H=­σΗχp\eŸ™2α¨e@ή3τΈ‘₯ˆ‰€χτ<ΊΤŸBCξtw’έέΫn"(~¦“eGpΫ– AΌ‘‰«νβ‡Εt‰€“ΎƒA£—Ϋiτ*ƒ—λJ#+σ-FX²ΰo“ͺΤΛ€ό‘­œœ&–ƒ=9Έ_ΰυψσVκ7›_LΉ1ŽΊqΠYγŒt%)ο¬2σξ2ΉKυ,ώ5η/6ΏΞ7c£¨OηΊ;‡ψΪ+)sϊ¨S6&zŒr`LΣB1‡1χHI—ε’β%σݍf¦‘ΰσ3 †<πΩθ'|³Kψž₯–W:JχΪςYžςqŒ…oχ§¦„<ψπ^,SD@nαH4 Ά8ΉQ ΙKχyάηοο5UHΎ2r@Σ–»)ς”šn 4ψPRu}7v ΄]aBlxsηΔQ7μלΧι@ ήλ9ϊ§·WΟΆ>$υnDeMί€˜—AΗDSυ€i$S΅\σΤ1«ΡΕVώΟu„„χΑΝ):₯¦Ψϊ7βΛέφš9FΥδΤ&‘ο³ͺ˜?π²,Š™y{¦«Δό'n:‘ŒzHη-™ΤΙ€* .κρ₯ŒuˆΊ²ε  αΕ1(½r’Ί!o‘φ-ͺΈ—Œ€qξ]1{žIλ‚Κ₯[s€žΰΐ„³-q‘Cs}Q^ΗWf¬€ΗkxLV:ΖW¨UΜiφ$b8Eσώ.ψhKβΙ⁨:άΙ(ZdζΨ[ΰ/χ¬Αœ1J>‡Κ³ tFΎˆMI_w†D© &nC2.¬;F©χΠ\΅3X©r%2α'ΉhΙΠ/tDί`˜Η?;o›»κυIΏEe&υ›Κ°c +Ν™±ͺ”9ΕΉΌq?Χ/“«V'…υŽ―“ >u|©Šι„Ί=ΡV)eφΝ·v²()Δh΅ύc¦N»ύyλS6D9νtΧ[}ruIJωaήRΕyΡΛ†λ7°¨²,0‘΄(Ν‰±F^ΞNΕτGΊΣβ#λΦpΘvΎ ­Λ4 "κcΚEF£κο&ΆΨνβj‘ΐ^¦HίWπΕUŸEwπSfˆα`ε!€}ήβ‹_lξϋǏj,f ‘5ŸŸγ?€ °¦9χΉδm'Σ₯ΆΡ.&„€Χ’b…˜“;τcίδh»πχΆqRS`y@aι»΅“aΈWGqŽˆ\04ψ…’™TŐ1κ`ω:^Νzή―ŽItθ`NšΎF±“G†ΛC„%ZJώ1PŸ¨aΰΨ3Ε·΄Ϋ:άΤMδM±/‰|SˆϋύB '±_Ώ’„'hP<’A$w_q¦Ϋ•8ΪH~β3y΄π+DΏ₯·υΓΠ SŸφ)gs%ŽcbGΉi0‡θ"qt·iOΊ½Ο°³€€―aγΡJ†J‚|-)Εύ@qΣη„}τŽ­ZόoσT=εΒδQJ.R™£ΖΝΪƒ¬ +κ?©%¨<ψYρOΪΚ$!…΅H „ν~6WK°ΆΔvέVTA9΅©ΧY—βzŽVVӈg’><`‡lΔ°>Q΅/΅?4ŽΙ•xpΔφτ©ςs5Δ½œ;ͺGΝ΅·&K₯Ο¬εžΪ”ΣZc¦¬ΖΔS₯†yn9MεeXd+)’`τG£”3OΛ7κ|Xεl»z±œdϊΚρπa΅t·—€_WΑ(fξYzŸλεΆύΛ…Υρο&Ό/•S‘ΠhRϋΞa0έ&$Ϋ<{Jή3|‹·φ„ΈˆgεZڐΩ?ΊYϋΆ±¨ [“–{x‡ͺΉψm΄ΡΡ¨Έ…quˆ +€XfUCύYIyšδ˜ωγQL¦‹Ά{m εβw„ξ&ΰ[ώŽοφ½ZΤτ³ u“±Ί5ϋ„HζΒψΒ½=Z-Π'Ρ€‘Ϋ|ψρΑ¨TNc+ι$ N']†άό`™FHM@r—P½’:w΅Cc1ΏAΕM/&±ΩšͺΙξβ•žVsCG?­ψ―1Τw8;ι„|Š?β™—³œgθŠ;ΐ[Ώ?ΰΈŒ΄ob„Ώ€v‚N"3Κ]δΚ:‡Ϊί«6ΐ υ1¦ϋ Ήυ!… ]ΰ^Φ£υŽΣ›‘πWΥΉ₯΅ΩDVL)”|}'ΪωXΫL#΄Ώ¨Kώ¦iύ·ώ&―Ξ-ΖνBMœG2žRΰίσŒΐ΄λ₯)§­)’O€‘}E8¬{ €΅lˆηGŠ( li·₯nΝΐ¦«$džΪ·21Πςώ™ΌMm`xλL  R>?ό"SJΛΜΩΪ/]!‘Ξ·$Τ~rL‘T߈-EvΩL¨˜©ωYd‘ΗsIγηΙ°dQδ“ΉOυ ¦ΣVŠΌ„φ κν„Έβ!f΄’‚_›Z}κ»Ϋœώ.?’FΎ%ςρΆRœ(7 90‚.½LUηkμ(‰/Sf ΈOفzΖΞvέΡ¬ zQ x6₯O’χ,rψ +Ν^Έε8Ε§„ +.<“‚šc6L0gHδ‰έεNC΄VIš•r%θDτŠ|Z냝λ%Ξ)αϋOg[ωιξΐj(Yž“RΚυŽΛ‚₯x·.CΟ<—‡NLς[άu#Ί±xb’€ͺxU1ζε΅#›Χ’d†άTLΈρ1¦ϊ&Ι•Ρν<’‘Ϊ.|² ·^²„™qΫs‡έζρ«˜η©3πUς† #Š“ωq£uΰHι {FN!mQi³4ΰ…κΰRΑξ©€[wOGόAͺU&eƒΌΎ5‹Μ4Ί³-ƒέΐΓΌΞx<…π,4 ϋjυ_ΫFψι04tM‹a ΓΪgŠΫΨπlϋoΪέ”Υ+~Ε‡ŽΒ€xΙ5Ύ<"–Π$)X‹°εΙBYxͺΥxσG}]+ƒb‡Ο—zt{„Κ6ΆΟΈμ|a²ΧΝ±΅w©εΓμPηOUJ"aδζvΫw +ΑΖ˜uρ»ό΄x˜Š<#[{Γo +ϋ$Δ„€•€€ ΕxΒ}Nƒ +Ωύb 74€ιΘΩ~j&£]uOφQvlt}GIuCˆΚ¦@_O ‘΄pΥpΐ>Θΰ=8βv‘{\)Π[Ζ!/?1ΣζŠV˜vœ‰νŠΕS…”k‡΅νiΕηό]ψsωΫ•Υ/7R~yΞmΆŸ$Ÿf“ž‰―k=€Ρz.Ιεm&³©σΠΗl^νVΛuŸΈ'vŠ΄ΟD§Κ…„μύX>h%IžM—ΑˆθbναήKI=>Τk}S/kƒ¦Ξ–€E}4ΐY’sΖαvοζAsEXbŽ-as΅r•5χ–Q^‰%Ρά“Έ 4dgAΚ`Τ.άώ‘4τθ~NyΖ.IΎο·!=\_©YsJ?°=θcηΔ‰JœΧ°ŠO˜7fIΤ‡Šξ­QZλ–,žNγH7¬I•ϊ| ‘y~ωvΘ}ςΎ,σVΎΊ1Θ+ΒΎom―ήƝ#αίь„€ „„¬Ϋέu™?ƒΏ†ζ3ύΔΉν&&ƒ“8)BMΖ |€Œ5m†^½ξδr+θxoŽ…¦"¦*xΖ²ΨΠπΒ *€η +)D»&p’­‚k*]¨*ጠ0€ΡΪύ—‚²“·VΰΊ %>2mΥΎφάT‰,X`|•ŸΠi¨šR˜–ΞΤα+Ύ1ΜΖFν<&0o6 %b3¦ηYp +E΄·dO­‡}R7ζR:zTΡΥαJBδƒ7―‘cν<Žj’C§Y§,m~ZΊ έ‹˜Ψ’Α›ΦP0δˆ‚B™@-Ϋ€Τη„Š%LB$γ49ΧT©G0Ψ”ώΒwIz?"}>ϋΙλ^ρŽ&&β4r…•ΨκpΝ4ΏλIΦ·«ΒΏ™κ’n•Ιιxχ/SBv +Σ²)Ε¨ T{$yJ;Y–›°/©“Ψ‹₯4eέHΗU wz_Wβi>μV{qpΤ°»Q†Lj–_ήή[½ζΏbχœ‘h#½Σ LŽΩPΩΒΟVφίK(#^7Ρι Fυό³}Κ›ύΑ78<}?€*2BCΆΟο»°ΐ9ΝoΜŸ£ι’Goηρš ša†‡ώ"Š·κ+βΓ_V½Νϊ#Τe²h-;ρ;σΚ§$|±Τ[€ύΧמ‰A)?Ά"‚„@ψ&)νξg˜'pZ£}z η{PΎυ·3¦e±}a›qςΪαŸ2κ„ž„›β„― +endstream +endobj +854 0 obj +<< /Filter /FlateDecode /Length1 1373 /Length2 26888 /Length3 0 /Length 27856 >> +stream +xڌ·p₯]·5ΫιX;NΗΆmΫv²cΫ6;ΫΆm§;Ά;Ά;Ώ8ηόίΉ·κήΪU{?kΜ1ηšs­1žͺMN¬¨B'd +46Ϊ»Π1Ρ3rDδT΄ΈŒŒ,τŒŒΜpδδͺV.ΆfΒpδκfNΞV@{ξ‹ βdfδς5rωΓ“Ϊ€]mL,&vn&nFF3##ׁNάQ#7+S€=@hoζ G.tπt²²°tω³Ν=¨LΎ˜ΈΈ8hNΩ™9Y™ΩδŒ\,Νμώμhbd PšX™ΉxώG *^Knwwwz#;gz “WZ€»•‹%@ΩΜΩΜΙΝΜπΧΐy#;³&£‡#¨ZZ9ƒ«Ν]܍œΜ[+3{η?φ¦fN€?›T€d +fφe!Πώ==Σ—ϋ7ϋ―BVφ'™˜νŒμ=­μ-ζVΆfqYzZ€‘½ι_D#[gΰŸ|#7#+[#γ?„Ώ;7ˆ )Œώ ψοxΞ&NV.ΞτΞVΆΘπW™?§,fo*΄³3³wq†ϋ«?Q+'3“?ΗξΙπΟΝΪΨέν½]˜[Ω›š5„©«ƒš½•£«™”θΏ”?ά`f.6F.vv6F€™#ΐΜΓΔ’α―ςͺžf™ώ‚Lΰλνt˜ΒΜΧΚάμΟœ·³‘›ΐΕΙΥΜΧϋόη +މ `jeβ06³°²‡ϋŸκ`3σΦ.ίΙΚ ΓψG{LΖΏ>ύ€χG^¦@{[Ο‘}Ώ ²Zͺ"J²4Lόί1aa ΐ›Ž™ @ΗΕΖ `bbeppp|³Œ’‘ΥΏm0ώO”½9ΐυO·ŽιΏ:vϋWTšγ+ΰ?kΙ¨Φ @υ?"Χedc4ωσΕτ[κ§όΏ)ό―*_"ί ‰»ΪΪώ¦ϊ;ώΩYΩzώKψ#ZW—?ώ±ύ¦j˜ύcZ93S+W»•r1ϊc!{‹?b¦cb₯gdύ·r·ς03U΄r1±όG2ΰjYΝΦΚήLθlυΧ»εO#γŠύρ—‰ΝŸχ‡σ]ώ2ϋcŸάWΜήhϊ—Ο˜ΩΨFNNFžpŒδΔΜΖπfϊcHS3Ώ• ` ·ΊόIό™Ρ`t‚ϋλZ‡ϋΪ&NN<φ·ώlό_λΏ mfζaf·ϊhΒbέυ\'„ηNw8 ΅½Ϋ™ 5ΑζB±œηm) ύMbΑQΨΠ΄{2]q₯κU’²‘„e²ΖΟχPŽ–γš£’ηcm3€ δ…8 Ο%V(§η ,Z‹˜lΧeBK%ΝI'ΓUUJmφ,Ϊ*ξIΈ+ηΕs_'.sbssμϋΘp:ΛmΜ`-{u:Ξ/iβœΙ€QŒΔο­Ξ Βl"ε© $8Εva[ΰ†[o‹#‰£,Νς"π +~S-ΆQYμτΎ@Α6t)L ‘ά^²²Ψe€ϊΠlvŽs’3Yg{²ϋψγO·ƒΦΥέVIl—£~pNMΐΚb/ΊΏ\dϊεjD§"‰πRQ΅=Ω{1ͺΰί4άmΎ}οz֞:•l_“/λΠκ±c+-G(Ψ[Ϋ杬νu¬RHQΞηξ₯œ#χϋτΈΫΟ ':}ΠΠ…sΌ&Τ"6ςύεPΒsΈχ³·'π ―°εΥJ>ΧΑΐλι$•4·Q…8k.γͺΜ° „"3šNΧ}ΓbΒγfw[ΞΓΗ5pβ?'ϋlξ/zΉ,mϋ†τK&ψΌ v4Υ‡‘ΐDωGu7Z§!’‘¨ yσ/ŠΪ똍/qγό6χ=Š>˜uΛ α:+ΝO}³ΈhΆΌ7uςπρ_*6ͺk΄Β TλL›φθ8L?“»g#‹Ž@t%u}υ%Ξ}½Ι aΠ ͺ²lρυ²ΘˆιδW±\Υ|°Μ}ΈκuiΝΚu€ +E QLžμSΙΉ}—ςΐνκF”&Γς§xΫDNŒΥχ‡΄ίΦ…:’Ε$Β']Εhϋ€'z›1SωeΝΜψΏ\7™#%SΒΡ’8^»r¬Ό°»²:$'͟€(ΞCίκΟ”θνR,ΕΡζKe)Δ‹΄­b³J'+ΦιWu\ΩΊΝ²/UΆHc •έ –G·ͺ|Ι1νd•›ψ š$/>+εζ›κΘ₯cΣΡΠΩ’9πBIgς·,ΉΫˆΪ°ΛXLζ·βλΤK‘ujP—ΙFτΞ—7`}Ώ²¨˜;xΚ}ξkŒEΏΓ§,PJ0«΄5PΫΓ\«UΰδΔ%KOΖ`χ¬‘WΝ|€|ϋb%JXΎpͺ­τ1€‹t]Iυ•UX §°60:[­ΥͺΜ’•­ ¬…6©OΞkΔ}> n΅ΚΆ,`pΕΏ»CΞ§Κ%*~εp―PŠοξ€Φ‡P‹("\91γ"»M —ΚPΈ|Η²U‘VφœΌž™ty½†˜Ο3g4›Ϋόs—‘–=£'†M―νή/(^o[’βX5JgεQΖUλŽρ €dϋ»M;DxΤ³„οwΝΦ”|lχŽ±Χ€Œ©<ρο,95žpΥM=ό—ωάTZ-fςfl#ΨΕ#ϋπ ΩΥ―i"¨‚ˆΓΥCΗ£ϊΜΌθβ‡έλ§vβΆ"nŸ·Rk7f‹ˆ mG―F4gD±zuŒΒ…³]ΞξΞAVΏηSŠoρ §„Y§ΈΏ›ύmlώ˜Ϋ&{’ƒ#O³Άsψ™AZ}*Η α‡…ηφέ΄ήΆ―»τ·λ%εӎ\-ΏS'”X)Δ=ΌΫβ7­­χ@ΆΟyέ²ΫνŸσ`:‹uί‘*[Θχ{zΡΒ ε²Y§έsϞξ±ΨI°νm’©™ΐ­WΈf5ΩΉ:-άα69ΡHβόb|ρ ;w$φ-Žΰΰώσ©Ϋ ιγ“1Ώͺ MΝ`5xaa’φ’E Ψg™ι2ΜP6I{).‹ϊQ’,Ξ²•/―Γt©Ω²ϋ;ρ6c₯蟱–«j²γ)[ΑοΟεβ\f>Η8kŠΩΌφV₯bγΓιF»LŸθι½ΐΙj]1—5α\d8aΑώβ#J„μ1΄M„ΪZŽϋŽΜΒ1-Oi“œΎˆlΘ Bι³―|Ά*•Ζ/%B*I“·&Š|Z’VκΥζΟ½³θ;§|AΦ7\š)© ΤƒF`ζfΪhz`Λ,£KR±\‚Ν(ŠΟxύΙΚ}ϊ6 2Ξ· p$gͺ~Ά«·ίΎέ‡oΌZΰR6ςzf‘’)€χΆJ―ηp~5•\&`aΘ‚―`“ζIΐC!p¦Ζθ3pΦDžn:–¦ΣgΓ€σ…«ͺΤΨfv1dύi6qεn ά}ΙDša_œt +.τΫ~HώΙt Bϊε€$>Κ{|Κεϊ\ΚΒΪ)*)―|ΆΚξ­Ιl΄Τ:‚rΤ%>γŒΏlFχk5ΩΆ%ߟw“=cΣޞˆ“SMζGTšIΪ`½G‘έIf䦇˜[ΎΎΦYKl­/ƒ£sαψ²l£+ς€αA·™N_W&©8*‹Κ<Δε,kλ«ΎSG\W€#ζ.‰β +ΐSΰχL_W₯=γ gCΧςΞντƒ]aΚX½2ΊšZ“ΛΒK”Ε8w‡Š>u’€Ό '<9+Κ·1kξ~Ζ"Uδ—τηΫ[ΤίoQwEˆΟžTa.πΕ:σ]Ӟtΰ–ξQ0³ξίΧI½~ΠFΈΜ”κς?‘?TΞφ‘«η₯«νΔΨό*Ο²ύΩx³NUbΠζΘ; Μβλ%Ϊ,ΘmΰšRwœΈEπBζΪqΏXοwƒBδ—EχIψ΅aΜ„_尐Xέμ?TEΞ*Bne|aψmΗw 2ρtl>“xqχŽΒeΒφOˆΜQ0πΘ8^Ό‰Μ³KL%IΆ4­ω}Ω²β–λξP(…LΣ^#“ΧΏΩΪΆ6‘ϊhYNYTƒθ”p ›φ:/vc+·žΥUΒ€‘s…βΙ.H‘aΰϊW_ 4sƒΔpηHέq°ΟXyœ pfEΑγ%Β‚Ζ#χ=“Ζ&B°τ€,0/. -‘'ώl{ψΩυ’$£ρΤEπ+/€έ|ΙR“ΈcέIΝX;ys‹–%9‰b—₯΅Gθp0πNfύ›Ž`‘}}φxiΓQD†WHΉ%Ήξ…Y•n‘ΨΈƒΧ88΄kε:ί” Y2wb ‰MΎ«Ύν΄)gΨΊ}})€u™œ9­ 8ΠWμžΠΔ§?0Β’ψUΕ›ηzοS Τl {0¬j&Q³Ύ<}š0{\!QνItPdaέƒQγpŠ*f1N–/Υπ ,9«ςšuΊš’5gL †Τh”€ζkc™bhcg¨7+ψAi6’£―ΏΌwΧ κW#gπΘuΰ[Ɛh?Hψy Μ§γ9‹b|“Ξ䔃P/%³Yϋ΅ + +7Xύ0?„½Μ …Υ%ŽRq‚°«λlj‹] IτΊ΅+°j]ο€σX|mΥ©uξœK-pΥΣζ£!-¨±Ÿ·ZPY\€"'r\„–<€F„/A<ΞοΟ{ŠκmΒΫξΛHμN€’Mε«•ϊ! μυΫη$ Zο:*“Δ:,―ρNΟ'ψyα]΄ΘLq©u‹ ΙITύ΅ζήΗ―eΑ±λ$,›ΣΕz­ _βόέΚΉΉ9Ϋ“Ζ™·–"^Wl6φ―2φNθC―Η^€­›.ΏdΩlοO7#&DξΑ=¬CΎβ[‚G(©πΠπŽQ’@׌L3BΙΉ²ϋ/{γ-ΓBΰΓ»’Ο|€RT>œλ{s[s'hζ6Ypσœ,Ύν/5fn—BcΡκ_βpkTLΞR\f8~³ά_ μ[:!Οƒ1Q&Ίώ„DΗό…qεέψ΅‹Iρ`"½¬Ξ$.Wy(™φ^Osζ Ϊ%ΖΡ$™Η‹λ°·‚…›ΒAŠριβ°ϋHYέxΐΎSΎά—7-„η°άRςΨΜ.Etχ~T"cQΔ›MΟKΰM­ ΑΨπσV’κ§δsάΔ—Χ_Ύο‘YρΉ)ε +u—Ρ ¨ύά,4ΚλNΒͺ z6οσ‘ζΜWθ˜d*ˆή:–k¨?ΪmΌϊX‚Έή +­b֊*_ω…’Ο₯―6ΨSοM½έ;₯ϋ…§b‹ψ»‰o$4Z‘;ΆΉ+Ž\΅O)οα³Γϊσœ>:ηΠΤΙ6=ης%ŠΨΉDχ šΩQΖGΠ =¦\έ»Ώ»™œΫύ,‹ Ί„^<}~iβ—BiΡnO*€ζxI1RΤI†~ύ kΦ<¦«βα~΅ΘΔΐΪRϋ!4Ogs:HΔΏx[‚(₯DWzς}αΕΔωZ$™λΘΆ)‘i<ω*΅+PΎ M^_}ΉK€]υ†uδλύ“Υυ…eϊ%Μ(Ώ=W€§Tβ™°K8:»Ί†πϋΉ«fn‹λ΅ηa0++³:|/Ώ*'*,G―ΜΟόζΝ¬ιh‰έέ0 lΤΈIαΌωfžζ[`5uτL―Ρκ«^νψΩ¬—ΚfsT±$ίbH(άΰIGΓ&υ#ΑL₯ŠΝ°”±2ΞbΕΧΜr2]^p›’y †ƒK[ρ˜@Ά[ εζ™@†ΔŠςSΗp‰)τ<"#*X_'ιΊυχZ +Vβ―…½Rέ­·‘¦Žbεε]₯ηΈ³wβlς·žq •c ¬N”ηΦ†™ ˜/~€ττGρΟυ„ΞZc¨Wl“]OIΣo/½Θϋ’u?S‘ ¨{¦ΡƒψΨΰ+ΰΌe 3…€ιKωuNCς›SEŠ( x8ΛΔ‘em=ŒAΙȎγQ*!¬Z܏ΗTH1ε’υŒ‘@ςι*FΊ_ΩD ¨<Y«OtΗ+0Ψ€―£Τ~Ή0›Π-Qt#›ΛA~ς%ζ»ΏQΕ* ³ΰ‰]0— –ς£œΩ0¦ΦΥ‰ >ύBxγμ ά\ΰJšν2­₯(₯Σε©ά©5=+Ύδ‰€*‰XξqΧr™Ύ²=ΡΆβΥe…Rb(εcο=°Y£(}\CΗi<0²βžWS +ΚΎ„_Μαh+ΐ‹©۝6:«Ζ =Ÿ]–Ύ5*b(w F/£ά’bͺUuŠ›{l4βB@v{ΞΞΤͺUσ–ύΚ§oΒ’Ξ.xP}e’RO  ¬Ρm‹,ΨγΕ†ΠΑJמz3±Ώ}λӟ`,Φ^•οΞ’κ}γΓΏKΰKR†ƒϋΠsy†~"Ά:Nwρϋό¨Ασ―&Oς1}ΚV³(Ζτ}£Δ†΅AIΓJ2%XΈ―8’Λw½ΈΎχ憱"†!»πΉGAϊ•4m–˜PηψtΡETL†½,gΤZΈ†₯Fy”_VHύN +~˜ƒV ˆ±αΒsσ˜bR#l7rgΣΰŒ²vƒ΄>Ύ”wΤΈΗ3Šω–Θ=C<ό”Κf\\EΡΠζUƒf{Υ‚υiέέjd†Υώ’€•U§κtΕrΈEocŽχώ +Β£αΜOl«1ΡώΧu@Ζ§=~Υ$wo:“”=^u•>ξΜl1Δm9„O6ΰς π"»;Ύ‹>ƒλΜ +ˆΊσλ};q +ήφkr£Λ§"U|Ηρκ‰Σ: 7 *Zw θz-ΐEŠn»3EsΈ‡xW“ι‹•ήΥ7?j— +™)]3Zr—!ΐQ·Π…œcezεΣ—md«ΐ’……τ; a±)e['wΪΎFΞ J p1­|ΊΠ‰1筞]X4="eAŽu†eώ|»J/η>(1ή.χ¨§³Sξδ[~9ΑΘ―Ύ4yΰdvK½ωύ ¬Ρ;l™ŒM .’ +f t³nŽS«œuwϋ)eœφqžLτ–€Ρ0ΕJ υ +39„© )β™σF¨τΖOθ/zαwrΈ_Ϋ0„"bΗiͺŒyus§ψ†ν‡­ίQ¨A=]k‚‹»­«SžιŸΎ”—§* ~`ˆΠ<Θ}½SD•]<Ή7­ β`υͺΆ”-Ž –­Kό„ +Φ=l7M2Œ=N/¦«έ Γ$>‘δ©δΔχχtέsG<`±«ν_0œ–c<ώϋ2”m‚dOΤDς}}3b•iΨ΅ €f:?ψΔ η·δhά¨όyΙ³”:­tہTpφ6F³α₯ιέH– ³Z¨|½ƒ ŽNƎ”*μφ(‡žηβ7G΅Ύ}ύ„Χ‚ˆ ZLfΑΌ8νNΥ΅ψΨίϋ\ΔΫϋ5Έ«οBŒ'ΝγγqbŽ-¬ŸΝŠ5Ϊ£a‹€[ΐαQ“z°;ΏηόέVXΗhr$ϋ‡Ύ?JebBΤΆvφΕύœ ·Ό€ψόAΚyi +Ž΅ ΥΔ»;y[ ^Vνfвπα‹+,νh©"Ή@άή©].q‚!ςt`U-3Ά[ ψBύ„Ιyw΄>ίΘΦύM(nΛy€αΠ$6Sœ§y“6 +,αηƜΝ8ZΖ5χ(€œ«o½ΤίΎJΝίΥΠ+χ?&†aΞHΨ’52ΠOU£Χ”sKSVH]‹ΰ82…Ρ άϋfT“Ί YWUΏ*MΩr‡,.{οIμΗgT<ϊΤIygŸ*­_GQ4§Yκ‹ˆ16I^υ„#|Ή;χη^^ϋΊERA4ι!Œ~ˆ,Y/‘"ŸeΣψι‹bx ŠYRΤ ΜΰϊΑͺ8Gτ½rΗ*g€ΉKΖΐ“šα7n"α‘«Ν[Ζχτ;=Θp„g­Θ +#/οVcΎlΨVjΌ¦nuT κοΣوχLА2ΏpΤΌ€6‚$ϋσk?υ"N~ObMγKΓ΅Π€Θ(돔4Θ½Cψγr9θ{…Z– υ#ΡO1>jd/kϊε’@ή’·˜š>6/ «Άlγ½³βu0Ψ{ς2)±P@Ήέrbk Πk<°G°_g;?5¬§’ϊ€qΆ"Z}χkZX—A°ŸΘΦΩ›;§© Ο΅%΅ΡrQ@ίχήɚ@<γz΅’„{Θ¦ Ar'ηλ­©uO2"‹E›ίqηX’SzΣ„}-½{ՁοΝξ— =§ςƒ]Ή’šΖ˜ί( +ύhΑ +΄Φφ,GΤ΅S±eχΦΎΙμΣ^q)ιύ$u!Β°N α:ε‘ …;‘“i°i˜Αφ0`Vˆ)5 ”ΗAέ,\ψY,Ÿ4»‚jζ›Ϊφ“VB?ŽCΕι˜pΌ™5m o#D+’‡0‘ΑžΦ6^~Ζ$'ePΓt’ΔpΧβ―₯πGΫƒ"ŸΨ+γ“!φkϊσ‹α₯ρŽΤΡͺ§Τ;Θ…ς>ΎσΥ›Μ +“6•JΪΒ4;OL.όœDΣ€‘uOγšΔ™YO]†ΩB5ΧeΙ‹―ρqv/'ΰi³_—wκαbbΫ!/Ίώ¦e‘ƒΒζΖ€ωO―εΏ“a’ψ6W{σέάωKH™δ°ͺͺΏήiX6&+π8|ώ §Γ½©i›ZœCωθŠ’egλςzo7˜Q™/<#JΈΓΪ΄μpwF«r…ΤΠ―NœξΌΤ.Δ/νΑiœΥO00+gi¦kεΰ«‹ŒΨ6ψ₯0Ί£&8΄ {d1‘«-^ΕTAΏ/F šLg \\a…2ώ +‹R„ΔήχDh#ΔΣ­Œ‘βUa0 eƒώ ίp—ΦN+γfΪf’χ/C.ΰF„Κ{—€ίΫ‚Μ¦·c½7zœλΥΜγΡξ5,"sξfA›B]Θ­ψGΏ0 L$ζίEςXƒc’ Ζ‚‘АnΖήΈdUP·€d9 cΣ}e|φΆ8ήΨΚΥYž­ϋω…d-(€RLnlυΏ#˜JX‘_‘‹J(ϋC±t9Μ$RώΈŸB€‘κπ=Ξ₯τHN7˜4hjς΅ςOΎέ²Έ!.„ΓΈ)P‡/Ώγ™Š™›%iHΜΤΕ½pA1Ύq’Έm₯f³ό€›Ώ±ΉfE@Αε> ΌaάκΈ¦0 ϊ³q…< +ν°ΠN·>*‚L2k-$S˜|xΩ³LΓNiρ +¦ψ½Ÿ„†Uώ˜›ΜLΉͺŽς²Ζ²Τ»}:=JyΊ&ρ– ΚE"VΊΨNHόΌNu‚ˆΧΈΠοAΜΜeΰF_|nΜ@ΓηΠ%~$‘ˆ=FΌŠΞΓη%΅pϋΨt +cσ Q’J©_ε©O˜2 <»ōΩp³ŠGν`Ύ χ^6πϋŸτ7՝”Žͺ?Ν,ψ˜ακΖgΊΎ―‘$ezΎ‘ςCΔΈ¬f,…d©N-τ$βF’ΜFNVT7— Tΐ`Μ§Μ²›Δ„ Κ΄»Χ¨σ–Ώο½―’¬eρ^ήϋΧΖ'ϊΟmΏRNsΉ:_䀐ΨΠtΟ4›LεΛw(yΘR©ο5αA‚Ω&[PŒi‘θπ€mm ςUΠ^—6"]0φ3yλ’¬οΦ¨a6Η!D @υΐ»υγBkΕτ<dMSZσΧ¦έΣξΖξ Ο·V­Ώoͺ΄Ηiy ΅»'j{7!^ΙrͺTΑ]xΧΕYz€°˜ύ/=ώrβ0ιιω$τυ,$T[ {yŸρ+ϋͺN#/’žr°$™±I«ͺ.Ÿψο.΄Oς€#ˆχ-eήα9ε΄cg?ŽNwŽςͺ‚qiΑη~ΐΫΊζI—Οx¨‡›·|41?OύEˆΈ+E1ϋšΎ²ΙΗO7˜e…ψC ±’cβeΠσΑ\="’.₯ŠyRΐμZdΌ|•/ ΩΈΟ„Y`l†,‘vΣΔ)Lf;γ¬CϋΝκGiΛΛα‘ MV+OŒcε‡T¬W³/υΨιζ~??ρZ² ™9%"ΜT$έ:i$—{?•ϋN™Λ(νx¨‚εWOΌ(wΔ°Ϋ¬½f“·SΫ#£”'όȊ‡μΧ^μ„₯hθΉ{Ή‹Α[Σό σwΈ¨M£ϋd‹fΡ“sλo\Νsά œμ^λιL$ΐΉ₯κΎλ‘ψVZ—ρTbΥ@&Ζ€Χ“Vͺ-η5Ωd:;MUAθ$d€Ž)8εp@!Lˆ‘dβB«±ŽΜ6ά…‰ΩμκΓϋΤπΛJ²ρ­s•ψ(νQʌQγυν νœ“¬T)4%Σzqn»΅δώD)` ‚ΉΧθξ{V~-Ξ`ŒζFΉ·]ΎΒ,.όΰΈ +ΡnKfΉ‡~  +uΒ‘jΤυ©`¨ΏήBšk•Α(2nηξ'ατ§Φκw™ί™ΚΊbaSŸ—ΰρ£ΎC2ρͺXr°gΆΑΚΦ5ΐŽM 9“.ξοDyΠ!=9°€ΝΉί-Ι¨’{+Ά/‡œšxoΉΥΆ[Μ9³ŠLθΎήb: \½έΰ£ΛMPY0Εm ΥΊόV_Χζ‰Α˜§!cΫΈ{­pzP˜;D™;N|‰πL‰υΆ‘–!},yΏ¨Ω?©ΏώμΤΡΈδβΎΖ₯iς©ΏMΌόΈg1 ΈD»(Ός7 ]€$εΰŽ–‘¬άŸ§ΰΙΉ5 ΛmΩΙr”TSί뎁 +χΌθM§«‹¦Q8ΙΤΨ—ζUlΗb`‹{[_'ΆcZΑb²νXtF·ε#Œ2o=Z±ζσS|P#ύe Nω`•LF4+¬΄@ζyγœ›Ϋ”β0ύDU*Œ(’O7Ÿ­`|zEχ£mCM³ΌΞΔ{̐ƒ>’tο˜ ζsΏ«ύ76¦i[˜‘aΓΡΗb ν{xaΐεjjdΘΉΞ­Œ#€Ζ͞¦v°"Lbσ”Ύ qpηS―Ή[GG±3UՁΉQ`¦ΙVΧ&8ϊΓϊ‡ΏšαDξΙ;ΧΪbWΌ~› =kIP|½ΐΡ…ΝWΠ„ΠΛ`2ό I„2„ε•©ΰƒAμΔFζκZ–gwϊ`t46ε v‘ƒήŠ©a h» {B\[β +bOΰσklsIχΤC΄1]φlΥ)ϋmwΊŽ‹Aω¦=QΒkΫH½ύ0”iXLΎ†ΡgΓb‡Ζk^N PW―ϊ(|ϋ1"Α}y©ΘAτcΉž +§Ϋ:_HώΖBWΪCφϋ©C$ζ3."ΞΞν +t’1Y‚:†£Ψ€Ο$N“m?}υ·ϊ₯U₯rεˆ8&ȞN1vŒ–ŽGΪiΕsΟh0₯ν7!|Σ}ή¨οϋm―Μ6 ΗρΚ w1œG±˜ˆ©-‰hβ’Φ<ώ€!2 £β9Ucς”5sΦΛ‡Όoeμ‘6ܐ‡Yd,΅’«ŸE“?οΆk$Οο]aFΣ΅ΎSόb{g-οŽέΉο4퐳^-?"|Tζ—ς2D’κ[ οΟ '}B,T”‹yΩ°π₯oᐳˆŽΒΛχNœ'Ήϋ ί£ €ύ2½v>QzΎjx DΔΝ0‡%§iFόξυniη²ΘzN„ώ5ϋpŒΘρΟΐ₯αΗ !CΖΗ)/Ϊ2qqΠZ9U‘1Šξ-₯„_]`g{jαι© °lΟG…:ŒSΏ .•fό ΏG%Ω1bϋήE&H ”wu#_›’sV5ε΅Φe@Υ‡νΎG4E+ ώV―3Ζ»½Λ‡\œΤ# χ2nΰ»κΫΫD‡Κέπ}Πuέ'ΊΒŒΦ–ΙΖ e05œ^ΥZΜη`Ό‰Au΄+Š$‰4yϊ Jχ—Ρ'‘žρήψq§¬8'7Srθ”…%V1½ Ηλε€χ·—lC0ό7‚%\ν8±n―έuπwWdƒ SΞ{έΉέƒα8>!4PπΪΕ¦*vœΖ‹WZ!»ΙΦ‚ΜaΤ†Θ›υSw½23ΔΖ.Ϊ…ο½Ε SŒόΕŽBŠ *θ·rχ¨„m_p|ΓtΒ;«Ε3SΟΘΣ +άS¨ œγ8DJΙΜMΣ[AάΟZσΝσDtmb‡ρ)Ž}{ύβΌw\Οήk«..Ίr(—ΌψLQ|Fr «€9Kμv–s„βρIY+…—ψΘ·’ƒŠΑ‡Β4d³18PޏLNΡ‡gΙu`Ύ][Ε +Ρ?ΔpΥ ²85ζaW<Ž_/ύ1(­Θχ·ίΒ™Ώl—1Ÿ―±€AP:DuΆΖ # λ:U‘eN»SΛ,­†γρΩ<ϋPΨ>―I΄›ώσ¦άœΰΑλΧ9σYE„[>kΒ6€oΧέÚDΧ¬lsšhZ±Mε»‘΅5ΰP’Fc#αΝ‰ςΉΔ¨{3£3σH#ΩΖtŒ/zΩuΕ­Έ4ΔΪ·<τ\e ·~Œβ%iTm4I¦ΒΥŠ}ΐyžΚ–sMZρ̟Α˜ ­7»K« χ›’ωρ*Ηz±~…l ρ^¬vτ%ρ½πhω=ΌΥετrεΐ‹7i5QΞ₯r™ΓXαγ½qΝ|™α0¨U°«“»HI&Ψ4›«} ΨΒυωTΥ<^Έ."Ÿ“jίϊ$0εw’F›σ¬'’α!χ ϋgA>d»έš'μΆ4†αf +Π)-]\Φ£Τ3γ‘F‰ŠoαΣ–οΨnΣ»9 dχ}φE Όηšt½ΞΉSκ>Ζ°Bόpω+Dγajv¦ά¬ψΑ`Θέ”k5ψ–ι"F‚»φ¦₯T—yς4΄ΨΒ©M-ŽpΞ’tύζdΘΧl暒zi9‰"€Φ°b»% AW”’ω˜xςτpIc/|ψ¨Σ~E¨{θn‹!rΐχyνK5΄ͺ^χθhχ¨œφΔm»Ι(Ϋa‹ΰΩ›‘P]© ‘V¦Ρ΅½˜`sτΫz » #”‘zσb1*ϊξ΄5O“ΰκ$6sϋ¨bΚdΎͺ[Ι¦QΫζ‰1&Ργќaχ«(X€΄σΙ¦‘(–BνcXRΙ©τv&‹—SΜ­pΙH˜Ά)ͺ!–b%Τ±ŠφUk€`ψ‹ ‚©%θm―ρ€Τ‹q‰!›ίΪ¬©αΪΖ KA HΚG&XŽWՍw*™re2_zͺ-·qψ‘”λžu1ν6΅L¨kψΡP¨e|Θ―Υ£‡ψίڟ‘ʐ›ωlιHI· ›ΖͺΪdθrw|τ«½'’)+™Χ»1R2²JΩ*=FΊ¦ξΏΟjENΨΰ cAψbΊ84Λ{.{ ΜΪΕ*σΐς<τε{DΓΕ\~Žξ5€ΑΝπ0”oΣχ»xxώ +9ls8Ίςέ΅Ϊ‘εŽoΝΰe ͺν[‡TόξΩόδ λŸKίg’υφ υΡΥrUŒQΥ¨ŽΥΏ¬Ϋ‹·ωπψέoΰY8₯όMθ+°ϊ‡‘Y’ΣΜ„½ž£ΑάG΄ξΠφτ7{ΧF …mL2|MώD|j!š΅ς‘uίFsel₯g­Οςxr|Θ荑Ά 5ΨρˆOΚξΘΙοBI š±XΜFܟ9•2Α=Ο©ι3«.-ίω9½hϋΞxPΪΛ$xoƒυΑ€”ΌGά+ΚΫ0k̅ލZlΉμ‹Ξb™}Έ+gŒœΟr€σΏIatοQmZΦφ"ZZrƒ? #>³z …NœKακΰ,V!g7JŽς  +eZmΪ_76Ϋ–ϊΪ\μ4X_Q“ΨŽ‹φΊ2h Β§ΠΠΊ‰LΚ@Ο*E±B‘eς 3ί'ΧFτ&ΦΕBΡ£(ύ^)ΨθΖΰS‡³T€o•R<°‰,OMΩ]+^P*Λ₯vΪLRός΄{‘*‚³Υ0ŸzάΘφ +γ¦\―zωπ@ί2e¦`£™# +[Ÿ‚¦Κκά‘ ΓέL@=!!`·ΐ³Ϊ,λ‰ 6ΫΎ²ΟΞΎœ'B‹/ͺl Ϋ…π†‘]γ<ϋ$0ΣΟ#»q=n»I¨žkεψ&#q!΅΄;J=ΏpwS}Όξ€K<‘‘΄Px\œv`q°υfweΗΠθΘ.τΑ‰VΓ§ +₯ΔΝςΖ›FΚΏ¨( JŠd΄αζ2'Ϋ'υβ`ςd/ΕΎΟΗβc›ΊΛΡ¨bΧqj”πOJnΗ8ΎRΔΆΔWψZ:l2¦a>,½Ό™ΗλX4•_H'“€‹thΨ(¦‰ ‰°T“ΏV Μόn2Xz7E'°«ͺ­\p"vβFΡŽt²γi(ΏN$w Ӂ­¦‹1Z|„΄rϊGp‡Y ΨΟ/Ό±bsX%»-&t&—Gϋ(ηΣόvπ­ΣE.%ΕT•œ=&Iγ>J'cγ tΡhΟμWΉj«3f"Ϊ½ υ ₯Κ>A-4αpΈ|L’BπΫφsρSΫ.'>l¬+ϋ€@πsŒ4•A(_š ZŸb,Ξؐ„h±K6ιμ ρˆGiX:ΟG ιKΠ£ι‘3:Σ Βδp\Β>ΒμDVοPΌˆ&ζ€―QΎ ο‹Ldχ:꬚ dh8Φ/=9&'ξQ¬αΌƒ‚ νζUΪ›ΗQbΝΏ=±π΅$Ό‹σΫ-D ¦%Ÿ ήάε£aψ75η&σδˆΞ’tuβVΨ +2OΦ2>F‡p‰YίΙκj UόhΡΡ›’{qζ μ x²¨A―χ­~CΎ…|a©ΑŠ3ξܐ—ˆk=ΌΉζ„†±ŸΌ`oΕo,\V ?AΩ™’BδΟp°›€ιŸφ\l~ž-6πέ•*u;1ιΦ1ό°/¦h?kɈŒŽ³q«lΆ$αgΥmm‡}­ΞԍwΚξ-eƒ³ 7υ}Wΰ1ϋ™ψš?°K5mό”W’ Ώ8:ΕJ-ŠσΖήX7°‘Ωπ΄ψ£τx!Β잴•ΓληΠTΎ*Π@)ρ؜I…€^& ŸOιΊ‘kzbΪ„žΥ°ϋ#ίA\Φw½―²ΊήΘ―ˆœΨsQΥΪϋW:π‹μΨϊ$‘ζkJ9―2Ψ,!φΕ7‘|2…—τ’έ#n₯ίFηυv 2 O―΅Α›Ό_Zύπ!z―„°΄Aςμg,‚ΡLp₯‡^ET lξɁΥχ£Œ‘ς|Bb|Œγ½«Pl­>Ά2Η„ΧτKdš"(ΉSI²T‘錚žD7FΓ’ ΦRC‘'\Bίv¦|ηL½Ϊd6œΕf½0€§513τ·rΓυ―ά­6Έ»0 ’+Λ΄ρ w·:» ξς“½U~β‡Υ”~ ʏ4,eί0ψ3²σώ£ψh˜|βM+€¨PίTΠdƒ<ΩŒbΛ=ΧιbζΙΩ'π"φmζϊηΝθŠwΕc"Ω‘ΔΪuμSδMaΪVaζ'ξ„ΎI%‘—1ΎΏ¬΅ˆΕbΨ›kwδSœƒSS?Ύ™°Rσ²N‘*£§μΧC`C?g.P€β.“υBΊδ]ώ±O`–_’ SBΑgβ”ΥC&r’”ν΅ —τu%ΒρΪ–X’Έή=&qsiλΈaΰ½ςΧ » rG˜gˆzΓ4ΠόRšρβj>\Δ|§άƒ‡ΡςDΡΣEzVέMEή/†ιΙd‹ΐτvΆΰό6@©ŒΜR*Ž~Pp…Ήlν“+Ν‚RZ/gΛ-ΏN+Η¦!’b‘Αd€Δ¦­Ο5ξgš}Ά u­"C€ΧiJΟ( ˆηΫܐWΓ‘v„‘,υΊέ +½?cψφΒξDFb>֐\ΜηΧβŽΧκ½ΕΉ"[’Δ"£‚\ΖωdY₯γXiaτ…™ψ¨Άvv€JΉΓή‰Υχ§3ΔκM{VUɍ:Ν ήΫK +}Ο.ΰιπzœΘχj£,Y¨˜έ_ωJζQ‘ ›_ƒί¨;wjv~Ι ωβΣ*Y?ΕyvoHUI(-œέCfΉΙVξ}(Τφ1λ’ŸOςr«fμρDrΛf3ε―.$€&τΫ› +Οω_»LΐD†°f­ Z$°1­Ktς%'>Ψyχ–fρ‰ξ™IωΒ<9AzΊαΔλΤvς¦¬Χ”YγσKLa¬Ίœ†²"‡φ=δkA€»Vƒ+χ]$?• *Kž―OdϋΓcCδ – }6ͺδυωψJ²βώδR…‰ŸQ‡7¬ ΅―Aφ*wkΤΒ1^œΥƒυeΒlwΓΟQKρ-ΆΗ(B7Rˆ―,͝ηĊ°ˆΧ«˜Y1 ς½ίΕ$[#τѝΰ–ό˜μ¬:ΰ#yβΖ>”0aΞJΎ§χλ»tŠ·Ή ,iΫBΰhU~PqŠ@0­ΪŠ‘|΄ΩԚbϋwO–΅P }Œ£2:xxΚVnσΈΘΥD€«΅Ζyг²$₯’‘Δ―½³Ύ+NŽwU0½Cq\ηξÜύpaΈwnŸ‘Ž…ΒbC5ξ ‹…³Σbd₯°ΏTΥo  Β14@Δ¬Γί—νζΐΆ¬HaΔ¦ v6lEςτUy­0X9†ΞΆπΉ²9#?φά7šΒ6{Q”'• ‚Π$> Οw;ͺΖ†{‹ :sED£Α―c‹’·7€θΓ;&~ε…ΦΡZWq‚^_B1€Φ +Š*«Έ9Ε₯­ΒτΒ ΅7'‰6…«]{t¦0ΒkW¨ ΖΜλθ“&hγνWS>Υe$UTc)Αu(•ž°ΞmίρZ?Ύ"ˆΤΥ–J ]B,—BηΔ»-ΚΆΘ/°g`Ynδ³€.Ί&¦`$aE]7%9ΝΛ'Ύ‚ΥvymΛΪ€§ΪX=7χ5bJ΅JNghθηXQ­ cμ­†Νt~“Ό…jϋφr€%˜²/ϊIY3WbάΆnνaρb¨AtιΝ3άΡA‚Ό5JΊaUl=Ϋ=O2ω³uΚI #†?/pu/zŒφ3”Η|IΜX·Κ[HΥ °$2ΏqŸqπ28/nšΓ£3ӌšΦ„L!$^εL³ΓRIe‡‚{\1Saaiγ OXAby™τ·%ξzFN―4k,q=•Tηi{œhΑιcΪώγ‘σ΄ Δ†2;8ΕD‚#\zœšvΐωζon­Λσšn‘4.€V²²ΡŒ™οa  E΅ƒ/Ε‚zΥΟn£’£–…’πΑ9šχ6ρ΄₯ΎŸΤEΡλψxπ±g&U’+•C:ϋ‹π8ϊ +ΜΉ‚ΥL=ΗFΦΗο4vς?Ύf.ja)x—Pδ=³τΰΫ1ϊ―Nλ @f†ύΐŠnς*ςω6ςfC — +ΗήdΗΡS AB[ΊV‡ƒ}~Fl&qt[Η+ΌΗD‰“ΑuhF£-Ј‘caE†ϋΜ+3Τ"ήΎCͺZ<bzl‡v—Κͺc;kΏo9Gύ­ŠHpΕθ§p^k .±½ΕHL£Lαιyάδͺ€ͺΚ¬6_|iYΓW" ΰYptΚT#έ4yξW/Σση‹΅¦?e§2τ s˜9:# N²/\ΦT;ι”n_χqύl7J‘²ΊM)…xΣƒ}ώδ>ϊW8ΰifο»|QςZېOκamΑύΗG–߲䚼3Ζ Εα‡Šω X₯awœ&Ια"’Χ/tΤr~κ«=χ:«Ώ¨‡“εΏδΜW(»I +‘qL!©μ@xΘΜώT]@ΘӁ0NΡMςV€=<γ‚YΔ]uΕMύŸ’FΦ°>§ΐT% Μ›Z?+djGΆώdΪ¬ŠΜξ«O»ύМƒ>EΩCtQ”˜’:4A,½Ώ;¨G[tτ ½.ωΥ~ζΕBε™τ3±cΦΑνκG;ΰ5^ίωή.š˜xμ}˜Ι±Ϊϋ‘Μο‘Ψ+‚F:?{β{DΗwώΊ‰³„—_x€‹˜Κ±Λ:aKξ$cςxœœεtΪ›4€ΪžΡ,ј᧩¦Φz!Χμ—ψ«Σ!qM0M«βΤΥΝ¬TBΨΪ]ςBn=Θ–d u<=Ί¦‡±gυΓή<Γ†u½Ζ"CΆE˜VkqW©ωKNΩ2£b€>ό χ*mcmΘήΧi&n€ƒΗα Θΐ¦eΐ» ιΡ…7”Κp9UΧκ1Bούοδ7'ΖΨd©( ΤJo(όΨ +}Εg’€Ή-ζ…„ •„ZΫ]Μ f€ Λ³φ3dMΦ?-ύφ’*ξ5H]ΐ[ Φ΄„¦P"z―O σO·Χ ,7΅άjξφ°2Ψ―/BΌ{Ν|³ΈΚgil>);/,3&–ΓΉ$ΰ#h5^Ω0P«6Ύ§ί#"»IB>ΓC‚ΛqΗޞ όΰ₯»;ώχΈ˜ԞT‚}Χ€ξ#Ml™DYo™(\³H“&J9dΐό¦YVΦ +`Ρ_*σ,ΐoΝΐΗ}Ϋ~αfz/ §Υp)–Œ!RZO†ή~s»|f―9D.v]ΰs•ξ‹μˆ!ζΡ₯”— ˜έμΟy—T?}5ξη\4ψŠt-ƒWςιδΩΧ,^±tΚ°oJwO.Κ¨ΔΊ)Χ›[ι›ΰ7ω‹€Ρ ΑY‘Έ™ΰkŒr\†Ί,!#›$Λ=qζ‡άϊ«π |‰ϊdΒwŒGΕ* ·Qiˆ[…ω+‚έΉH~i§ΓD6ѝ ψ𹧝ώ&ι(fd­Ύ=­Ζυχ™u4ψf‡ˆ Arr+κ‘vτ―]VΐtS0·`qΪ Τ a[ΤήUŽfBžngφpΡαCΚ ίlΩl•ΖZΥ΅W1ΊMlk%γΝμΰΦ?:λΙb„F8ΉμRλ!ƒAπVΫlaœώ}ΜL +< *ς :UbG +¬Ά0λ?–4 †Ώ“?;αϋ& Ω£-‹ +n )›9YBhΓL˜xdOi΅Ύ±ίjaYΊ{Έ‚Ί+―εZΛ”»«„ QΩM€ΐŠμL ΅Gγ±BΤϋ»‘cΓ\Ι₯‘\M-ΖμΓΧ7ŸΔ7MΕφSΌ•s^iΝ^Μ;Ϋ^¬,™‡.κ’!7Zg˜« ’(Χ\(ωά»ΑZ”C)υˆόLΕ$ξ…OΐΗjUΆiяOVΗ&<―ΏšV]r"kώκά―=ΩHuδyv +†ΦθynσkƒΈ?N₯Ώ„ω,»<(!zSυ˜Σ.e‰:ˆ¨Ν^d^UJ½eG'ύύεΖωώ’Π₯_^Ϋ5ςδοώΤ“ηΙΔH·N΄Θρ@;YΊΘNΐ΄$y’†*6 ·’³βIα’Ί°ͺΕYΗδύ–Ÿέ3’ΛOΤm€¬ψbCz2)lžσ Φυ0+Þ’Ιψ Ζή֜b›ΌMιςΉΪvε½”WγΞ‰η›| ˜¨ΩεΛ2 +BΝψ΄€χˆ[_”h»hΏ=ύκ‘’σ]ƒX„U[άΞ o–³Υc·œHζ:V-zŒ—ιJ&pάί‡IΝγ† δ̊LA ΩΣh‚3ςÈεΙOΌœ΅žgo@ΑςΕ°fRξ#ΛXž^‡ζq.ΆΜΌ’€44άFι8JΘ&;2Ί,ΑήRK9bΤƒ| ߚπJEgT{yŸΡ·^-jQ(ΓΚΒn›Βԁo’2 +hEͺήΞ”n{Š$¬Ίm–+‡ϋ}wΔ>Ξ¦<.}Ώ5Ι%¦5?ŸZyu*ύZ“ΉΙL·„·wΡ§8ήxλι‰YΡrœžu^œE}cΔSζΎ,£ξG›sDΤΨ\υIΫWf‘qfΐυΆΨŸΥb"Š–μVξZυΘΜu·FŸTkOwόJLF☟i‹0ςeΒϋδIΦq"7eΙ ΘΩΦο +HΛ³² Α±νΐ5ΨkŽO3(σPŠΏΌ]9uΉ~e°= +}D"Dό4‘•Tα‡ΝGŠmJY3ψΈJ*Χnšš©$+Έ)$BV*CfsπXa/‚ Rλs CΛ1¬Β—χDCtž„Ρ‹=Œ/x²Wρ Ÿ:αςQ`₯ʝ†KΧ0ϋiγ4ω$MψkΥWKmΟ»λ Τ·ϊ: αΙΗΖ=H5v„Fϊe§υ ρ™bhšQψ…ΓpεLͺˆΒVΛδ˜m°ωŽΜ4Dn)¦λ‘ub†Ώ—Ό#<04£os&ΫSι€Χ΄6¦Ώ\ˆ“ΔWZώp#φŸΊΣt―©Ύtc  PQ j–ε6Υ4‘ˆΕάξΖx[―H%Ž[΅ˆέͺl-ω k¨ύ0aΎΎf·„!Ε!'ΔsΟ?¦©‹l³π2™ϊ0}ΉΪ΅όίD^Γc/Θ\O±εA‰1Ί8°O +’” ΚΆNNμ ΅^ŠD²ϋC‰+[š%tΟ?ϊΦk\l9₯uS11φ’ϋx»ς#¦$J5uΐW7)Ϊ`“Q‡λ”Ρ£΄WΣ½a~*mύΝΏHΠ°ΟΔ΄“ηŽ σ«#Č₯j| aρvW­ΉΠYqΰ·Ύ·IφŽ·ώυΤu΄;mƒΆΤ›ς­.ηc,λ’Π¬•!‘3Υπ*’’ —ͺΕ:S7"šΗbKž=™žŸŸΚ)Ί6Q"/?™e’ςμ9K‘Μιs.Κκχ’5ΩE¬#Xύ HHf…’βΌΘ°άC{X΅€€Y(ί·h $ί πLD―£l έK <Ώί–s=±’”Ÿ9s1Χ+ύΙOαDl·j,Θ±hV½Œ7x‘Ό4&‘Ε3»D&―“έΖԜ ΙzNΥζ€ϋ/^†2ΌΤD@Ϋf*Ž}iU.τΪΪF―u³œ‹#…ΨΟρΘjξΘ́Πzύηδβ~Na•^${ΆσσlmBΙόOνζdŠ©ιΒbΚΓμ»a ”ΆJ?τ?KΤ€†¨xG.κ©£ ŠΤaΪ‹°°Ί†·ΦŸM­ρΎPˆ8ΛbŒ"Ϊ3~Ι1›ŒΚν³°FΘ¬]`Έ +fplN?gcN3–KSoΤ)φΟί1Μ"@§φ‘ exΌΝW#Jh€ρ$ωΉΝ8μ}“zP[@Α;?F–c©η aϊœ™Ώ–G/—εΥ_0aΑ?]>C ;%p +eαŒψ{yφ/O‘!3͞aΩZFΔ‡θ‰0β_>—7μ5ΫKT΄χ`±―u^£ Lƒr„š€€SΣφ6Έ5» @κ?[}κO[Λm©­sκόΦδη8 ;Ι"―έd¬=²ϊEΞ;Λ.4¨™@2aB:ξρyH5אU‚o„ΙQ±χΫžG§kΰ Φq54“πP?Όό8AuX¬PΨ€ΦOkΩ2Δ5ζ[?Œ…g.&Œά•o-„BΗ·AM θ ΥαJ£·™ωŒΜΔ€ΗyXΣSjΓ6o=ωδlθ排Β0XЊGζΐ«0ϊΑ8k› ₯ΨιΧXΝηΥurλ#™„3o4Ώ/ΧΒΖ/H…yλΥVjβρή 0L[wΊ$TPΡΜ*(ΤπšτψF’=‘39΅ƒ[w΅dXip1'ˆ<+=Ωόz ξ€ˆŽ:β•rPΘX9—₯ΙGΤε-ι·1Ψ6Έ _QΖ4KΖυγ™Κd~I„$uba")›fΔ½ε-f|:šι»ϋoφ gVζΐΦO«o”εΓΰWώЁ±y+ρσS€ρ«ΚRΣ**nρ3’λǏΏ{τψ”ΧγTHΟ1Ϋƒ!y£θγϋοΐ(·ι9¬j‡υαA‚ΧnOΏ-MΕy‹KΛΜΜ›ΰ‡nΌΊΜ^Ξμϋ¬Y#Kφœ2²‰PC|•WιϊΨ½Y[‹ΰσ‡2ω¦Fšˆρςω\Π~ξ+ αΉΡζ<Ν ϊΑκφ]HL»MžΖvyω”Νρ-‹ΈzHʘ…Φ”φgΒ +eη6\»/έΆςΣi½iε©ΧSόΔι…ζ˜ΊΓΑ™Π7AV]3V&@τιΘν΅vε #Ά…‘₯$4΅ΰΜtΆΜMB§½bΠ¦WΔ%³Έι'G˜σ.}g8ΤF¬ ― γψ8l!Ίbu?ΉΧ‘Š{ƒc‘œ‰+MVPž’2q”p†"c§χΞUΟ_«γ +€ι–IX1Σσ£ΗςΔΖs0έ½^‚ΞΥ2Ύψώ§HBC‘ΏΫIΘ‰ο8€κ?ōt½ΊΚΎ+C£V,_–€8&έΖηΠο[«J™άuΝ!{rCŠ‘€œhό*ΊV€.΅A1²f4Ω±¦*θOόӊ3©ν`»QΦ”Χ;L€"=MdΘτκW£FΚ²Ρ£5?Lƒlœπω‘g\iOΤϊ)Ρ‚Cx`Ες¨s}Υ— 5›’€Sΐ₯β…Ύ?«TΕ#­ŽΘԞ΅Ι.Rm•&cBp€΅έέͺ£M£ƒfΙr{Žψu₯{ K==Ίόk[n³ο‹ηΝφρU=˜*ρĝ± •tγŠ"£KV0EΝ’w’?‡D"οͺ=όeΙ ΄2 ΐΐͺά^ Ύ}7σί0Πov"GΔΈ0žyΎ’ZQ$c–«€^MC-γΚ ΓΒ«˜Ο/Y$•μ½<)‡"ά–ΏΚžzό·o<Φeτκώ·ΚDqCRΎλ aεϊΩH „—ΔKωΉsΠo ύΤήw€τd́FK‡C·š„A ύYh9KnbŠAdP§ρhχ +ή΄•"c «πLΪ0ΠԍX™ρyΨ’&)Q‡u;ο$ΔqEO°Ν›OSΧgg°^fί;Š!₯ƒΓ«±(ςί‰}v%] 1λΙΒfшR£OιάΪ f!͚Οjυ©žpΦHmβŠm΄ΛPR0τ6>¨”†ŒZΧφ>:ήΥΐ­7`ύ«ΐ,Δ,epγXcoyΖψiπΨ hiί€ύΝ€g‡€εΦ&XΣZ% Ίlvε;Z|™ιΐ—!¨Μ° ΕΦbΖ%Hes»ΙΗT KδΏ¬=l§sᄏ(˜ j_.1YD$Reˆέ,ΎΉc+Ï °o YΌ³\ψΡ–Œλ'   !—ρ8UΚΈm‡ΎRμ^d[°i\ΈWΧΡA‘Šu$ƒ`τΑS‘’&‘„[ h―²ΛžnwψΙΕ!8 FG^]GM-w‘X1D„ΛαI8₯.Œ Y˜AŸκ‚5x<ΓίWΠhuSW@ŸΩsίP`Τ τ›ωϊ›’q +λˆξwqpω/²‹Q—_‘ξA[φNims½ίMφ&ΉΕηkΆ/P#ο£·Lθ'”k%Ύ aNrϊήυψΥΜ-r!KΓχ˜K‹ΜμJΚσ˜ŠGfPIΟόvSΰU€„¦ Ε<9ςΝθZNΆez3y!­šП‚’c―ΫRŸ\Ι;Κ΄‘:œβ›೜Φxαθj?Ά*>š'–*yΤX³ΑΠΜ^CΙHΏΊq•†"•(―εΌω^[ČΠPFήKЧYω έ05ΘF]³eOl‰Ώ±Ι¬6€τdGξΘ8.•KΓq>”“0ΪΏπ%Ι…a±@o―VFœς―‘·_τƒWx\^j³\NqΞžφ• ,:TςqwΎΙΑψςμν ֎Τj]0YΕwΡo Χ†ΊιΫk-Σ@8·˜/zλ@ "RI°KόΕνΩVΤι5_ωβH}“ΗΊxI ±!K‚Eœk:ξŠήσ˜©lUq 5θΓΈeΑ +,Ω.;n•œΡW “˜qΏc―2,r.1x₯ΐ +昀 Ό+ϋ‘/i9θώeΚ=cΙ Λ$‚EόhΞ%Gh1Ί·Ÿ™O¬νŸρ*7ΧoB—\»/ΗY[ρ‰ΪfD§’οτd?Αώͺ;cg"Nγ!…2…₯γ“Œ«αι%ςπ³4Ό' +Ε"YΧί­»$o₯·Ξh$ϊW3ΘΟZξ0Ζ”‘(zxοmO±ΞF&,τΰ+‚Ν·ιώ΄ +ήΠ +Θ5aΈ^3™/} εvΰu·Zθ +€†J'‘Θ0SWpμšρΐ»o4π-[KΖΖ†Ÿεdώe",R ­³@cDH΄ς—ςΦ™21g—W½V*=-?ΜΈƒͺ“‡8-k›ύ+Ώͺ‚8χ✫3K§°Ny-ΓLΞC8_ψ“Μ1,υeVžΜ!NaθΡΰ‘q$UQK½ŒΓS‚P‘Κ_0E_r!ΟDˆŸΛ:˜Ε«DθυS ΈKiΰzΑ]Iš?lΗή|‰7ϋμϊίI¬cιζίLψϋ₯³ΰkαΪiέ<ΕrŽ'œiΖ‘~Sφh±Ή—™τ~΅–šŒ…TβέαΞΉfύ©²ΆK"ο6h3qQm£«ZMά7ߏNΖΏ•ͺ’Eœ8ˆ +τ›¨³ΟpΦκ2„ΆδDM$…˜ƒ=H$8—S&5"r¨41΄ξfΣι~h2pΎΑu£M +n—"Rƒ%ΫbΚυFMF„θ3άί‘/&<–β3Ή\zX§ξTe]!­ƒΦjBˆSαΕ3ΊYibνjπŠ•*€Ώ–f²Ÿ9υΎZθn…0ΣR Mώ#Δυl³uωΝΩΤHLιzπfg¬­ ΨumΚ_?Ψ±φWœλΞ™χμ’ΪΎtΙΤ'‚‚ο‰­ ΙGh΅8‰3.ηDΓ’6Ιώͺ*ŒϊΈΣW°fjΈ-!œ0|μ6šπD1{%ά"Δ(M‰…@_cd$(”—Œ>[‚\ΣΣωΊ±MtΑ C{νw7—’6A^,Ž ΥiΡ*ΗGŒθ(mΕΊϊ `r–h4D¬`Ι$•κJ‚όΟΉ:C&σcΟIγƒbЈ-ΩlϊD7”ΟpχφπUΈ’ž»βχs&έ9ίPξ²Q’¨`&iΘ]κ2eΒΫΘΈ¨·όςέώΡςξm@¬_ΈΌdUT#zπεR–xv‚`Ϋ% /œΝςΰn|θt.ŽόSŸρVΝ8=\ T{6±6ΙΫΗΠ\π0xρΫm:‰J©$_ώΫτwΔJ³x‰’8ι€KDpθ‚sπ,3‹%‡Ώ9«ͺ‚mBwQ-ΤxbKj…G ˎŒ°u”Ϋ΅'¬:ύ@ν‹Ϊ6΄„ ρͺžκ +ΪId 瀑³£2Xž¬_ !ρθβ0ΫˆaΏ€ΡDDL*[1β_+L“7Ϊ"ΚK›˜"λμβ/@ί§±θζΛγζ@:λx‹e*Ιι^=•oηα#-δR‘/•υŽj!8œΟά_Γ`[b\ή3Ό”dΎϋ¨;TƒΧΡjZDΫ*%gϋp©A$nl|{jΦ|hh£πD>(Ό„γΈΚ/y`δ †Έβž[Saυδ₯ΩcE Ή¨χΦ#šI‹1¦v#{ωVx >SQθ#•ΎvwQo„o{ΜυGΛ +Ο]ƒ¨^Ω„"©LΠ‚›¦ΪσΟƒί]₯…ΰœfΚΨΊE θdΑ썼ήδq` +yd.~šδω_*g ―V₯‘£ΘΈZτ RY3…ΒlΊƒ²χφEjν< x»PύΞβΫzSαΞ)L)œn0o‡‘΄τ.‹0„_ Gώ΅ί …PK”Y9.μ±’¬j4+‚–/ψΦ‡cΐwΐΑΓΩN’·:Ιd; +ηQ•%ΤϊΕ ωΒkηU•ϊcNZφmpπ‚Ύg—ζnhω‘’œΌ"E=x‰^Ά@€Ξ.ŸόΙΔ¬$rk*ρ6NηΪ½Υ=x”{LI~ λΑκί–'£ι°zΕιρ}έA –waΩ ²W켞Βr€‘;b,μVΩω° FX£ωΐ.­/—zaE=Ώ{d4“όΨO™'λ—6ή³ΆNΫsd"6τΉΌ¨Λ£sl#]oK$pq†ι_Ίs€²„@ψκύ/Η‘ήτ|ξ0#0fόΊƒξ«VνμTgQΎK"zŸ ²ΟHq\ίΧώžHΡΙS―+‘ϋDμ.•@Ž₯λΗ<s ζώŽHΞΉxtρ΅ϊοΠ = W»ψhϋϊλ©άΎμ©λtG\lK;€΅¦1Y}hμΫa₯gtH,ΩΊEχΒoVkΎ†Δ…˜rίΫ*0Β[ƒϊR„Y±VβΧ4ΜζqgF3ΗpΗΘ^^α Ώ΄€ίDBϋΏ‘yβω¬•ΐβ6x%Υi¦Ζ²Ξ1ςδ%§ΆΛ…s5pωΠw Q‰l °1΄ME&„˜κ%₯ž.Ύ€oλΖ!›KH_V9€YRΑαήέ σί]-₯)8Λ`ΈΞtb’«]r墐υ3o+W; fJ%‚δ$ <–3wl7xa“ΊLκR84¦³gq 9ΖωζfΒεΰ»ΧNBSλΐ +(ΚΒHzfΑwznήn˜h²ˆήͺ9cƒσ²£Τ ‰89ϊ3u k—|ΎμόJ€β"ς ΈΡ³PMω‹Χή“ƒ έsΜs°Άψ_ρ~9Ξη²%’Aœ_τ@XvxCΙ ΝKΪӘqbZkBιxο³bΊY ΆšͺvΞγΒ$c’ΫGΨ*„Γ/θν[‘ύ›[VR•ΤβN½Ÿίο ΝβfŽ­γeΞώ’#ΠΓΤ!ώ…ή,£pΥ7ξα€/ΰf£ΛUf¬]δpΌDΛ3AΪμ<Ÿ’=»ͺ˜xˆ™Ξω V;xl„±ΚW}{tw{˜BQ\ψx–ΙZΐY:›ξ’ξE›ΰφ¬#{}Vy‘νσ# q’’lŽ +ώeωΏψκ$Ν£“.Χ:މ„V[0iŽp«‹1ίοU‰m†Ήφz₯šΩ? Ω:γaΙίK±N%OJτmϊ’Ί.ίPᲦHύΌxΖΓ‘‹„–4suρOΝˆμθϊwθ ¬E\Λ= Όˆ@ίΈόI†ν΄|™›EΘg` -¨φI’β,ΗΖbB)> Β΅”Κ#{Δ©•…θrj5“M%Vα0}ΦόSw΄eE³ΫέψUԘ*₯ƒ©mn’Ϋ’6gν+ύ}άέ”υΌΆ_Λ’PΈ™η“ΰμυ]ΛaμΡ…»FVU;άΈΗZΊ.F§ΰr_„£ΟzT;‰N’™ƒωZξΘ@ƒ%α°”Τς.ό33Dαά,«©QαYzwoρελŠE‹H ž$c£ˆ,·aΝ©$Ϊώ΅ς‘V\}3Rϋ³œ˜Ή €O‹kύƒͺ4"@Š5CΡ.U€Ν\!A4Ά <{εΈ»™˜Ά•ƒ‘…τŸ@ΒQ=½Ξ–“‘%ΐ‹0ό/³Ϊσi’rζeDτ‰R†& +‘Ώbd±ξX^ER'=ΩΗ¨‰0Ϊ˜’jΙηn"+1(Œd^—ΔΖ,κ^ngώƒ²‹Δ γlšK•ίΡW\•Ul½ΐ_½—;·‘{Θ}~zeGWd‰qΎ’( έgšΕlΧOWίz^ qšΩΰŒŒpt§ 0w³ +dq·žτς±η€υrΊDŒΈ ΖΊfφpŒ~€Wϊ3ŸLŽUθ!Οkξ ƒΜ-ˆ}. Ώ¨ΨΜΰίH&Υ³R²ΰΣξ³~πcƒRκ'9Π0κfSΓ,¦k]‰»S™Ξ!χMΧ’;c­6»0eV€Τ©HπvΎύb Υ}τοΒhθΒ^ωkqΪo>@³nœΔΎ²ZY†˜˜bάΗQSκ¨0xF¬·aρΛ–ϊ\IkKζο""Λ`:‰gκϊUμέ+7Άχ6#ιέApAA‘έ}5qD¬-Rυοφ· +ϋ +b§Κ€ΐ#¬ͺP{k +]ΘΊp#+κμΨmSω8χ«Πγ"½O‡Α aΟxί `“3Γ@ž‘dE©/$¨JΥ₯~Βχ3k…μS»qa°ˆΡ‡07Kv< +Π‹%ΫΔK]ͺ‘βY‡΄Ϊ9~ЏL™»αα2Ÿk,Vν4fγ₯%.nς[zΚ6—™—ύ +&ή"hmΎYΦκδ![)Ϗ½,ι¨]PUŒΔΔρ Ι―u`$^Q‘zq ~›haC„™~λ‘ξ"ΒύΩ0hΧ– Ά=$sή™γοځutŸΏ1žαϋCkH^XΡ:_Lz4ΎqΞΔ/αp•3€i‡δρωτxσΦ»ς$N_mΆΔ`Œ9ησγnόŒw¦`ύŒΊρτ¦ƒη%AϋΎE* © ’°TnΜδkΧ€0Κ«Κ_π9«;šVl»&e«·!eΉ«{ύŸΗ8ηχVIvχ\tΥΫvΰ@A +(“£Ό:ˆHΥξ$T‹±Ζΐ­Δh₯θ%’(%‚΅-·š‹g†ηδΨ²«ΪnpVξ¬9‡ Ύ-nεb₯QΆΆΌAΜό,ZGœ˜0βνފπ1¬1ΠίΡη‘rΟtΛ@zq_HΌ²tγ–bΥΘ‰„ί―2@ŽYyGϋU@€Š~-oY»ρ~ |«Αv£‡ϊΒD3._#T„κ.žΥ’Μ$D|ο +endstream +endobj +855 0 obj +<< /Filter /FlateDecode /Length1 1366 /Length2 27424 /Length3 0 /Length 28388 >> +stream +xڌ·Tœί²=ˆC Έ{γξξΑέ΅ΖιΖ]ƒ ά]‚»»»»[\'Ώ{ο{oήf­™Υku§jWͺS{Ÿυ5 …š&‹„%Δ$ »±p°² €”%υμμ\¬μμœΘ44ZΆn ˜‘it@.Ά°ΰ εΊύ΅Iέώβ”!`€‚»€ƒ ΐΑ+ΘΑ'ΘΞΰdgψ/ ΔE τ°΅(³ `+2ΔΙΫΕΦΪΖνο6υ ·`pπ1+ αr±΅‚Κ@7γί-€Mˆ…-ΘΝϋ₯ Άqssdcσττd:Ί²B\¬E˜žΆn6 +ΘΕd ψ§a€ +ΠτοΞX‘iZ6ΆΆkB¬ά<. ΐ_ƒƒ­μϊ7Βl rόέ )―Pu Vϊ7€πŸ³p°rόwΊD“Θό―` …ΔΡ φΆ[¬l@UY%V7/7flωθΰ +ωτΪ:ΝώU9 +‘ώmπ?νΉZΈΨ:ΉΉ²ΊΪ:όΣ"Ϋ?iώž² ΨR +βθ»Ή"SŸ΄­ Θβο±{³ύ{²φ`ˆ'Ψχ? +[°₯Υ?MXΊ;±iƒmέAςό5!Νδΰaggηγε€œ / Άky;ώεδψΗό·_'ˆΐκo [+Πίd_W ΰζβςχύΏ;ώχ +™ƒ`ikα0YΫ‚‘'ϋ_3ΘκίλΏΓw±υ²ε€ύŸΟ?₯—%μΰύ?πΝ—MW]S]W›ιί·ORβπeαα°pς°88x|όw5 νΚ`ŸXy° ποjΣUμρΠG €Kς—΅ ύάˆ‡έβοΗoͺ+δαdω"ωY¬»ƒΓΏάτς?ά@G[οώ’Φέν―”!eώ?‘Ί ‹Vβ`ωϊδέ€e ΆvψοC΄u•΅υYͺΩΊYΨό›+Άk£1[0H βjϋΟ₯`α`g?|…eaχβpύKΘΉ@uσΏ·”[@,'/θβτFώ;βΏ+€/Η_%Z‚ΌώEa+βφ7π·9€ΔωŸy²/;ςΚmαξβςW\šύߍkύ/%ƒ@^ δ΅eˆ…P¨]}hηC­±'ΛΡ΄ΘΝ‘n‹οšK—ϋb +CMΦη—;‰”±~ŒΝϊ[ρuςWߟmˆνIκΟ~/¦ sGΘ«³xΓ3E?%†H?°h‰ϋ½:ϋιΫΓΆAχ(Πδ9»σ£©`?xΚy5 UlL†/©Χπ*’ΌTΜ³ΔjΗ—.δ›g/P"Έ±"1bύςB_Ό½[ΐʝy'WH`Bφ?ε*φ5ΨεόϊΈδ³U₯ΕιΪKHMh@@ +{‹59Gλ+yϊMΕ·¬$N!<ΊΔŠΤdΉE΄ θΜ]GΡ³&\11³7AπΓ4Xͺύ`.<ΞB}ŸFr쇃fn7V^_ιό+‡i!²«zχΙ+žOtι%θχ¨b1Ότ uΜγk‘8~Ω υl?Ε/Μώ{'°ˆ’jΘ;Ό<Λ/ΙDsf–ΔΪΌL†F!άλ_β!φJ.‘xg.wΔ‘($•Δΐϋα*'ψξ‘KrU…/GΜ@/ΐ{½Ακλm—M8Μ!Τa‚Π8τt‡2HΑ*ΚB}szΉΨθ_§€χX)̜˜ςF{yye@E`b}yυ]FRν΅KΗλΩ,ɝP"―»Z‹ƒ°|/ΝJ¬U‘–#šΏΨQ‰>ρ™ΡδΕψ-RLO4+(©MyךCqiZ–Hλψl(™Γ…W–Œψ]—6#Ψš‹ΰyΫE&ίe\εˆ₯ Ρ―s›\ˆ‘ΓΤ )a‚ΟΞΏΐ›Νr―¨§ „†‘°~;>5Ϋ"Cγ‚ΨΚΆ;—gBΕA7 Eϊ(σ`ξv¬±'αύ–Αxyύ©φΌ<’‡yΜ8œ1¨Žϊ^tš6nŒΫΈ +3Ÿο€,iα`˜σΠzΡ`€Ζΰ>œ¬eΔnί +~<5'―“Zdσ΅ψ€±―πŽ ~x‡>ΪΟ2Y”³Ώ·V?ŸbΧΟ€ ”Χ”’¨šΰJψ)π1ΘζΛ%œΨV ,f2ΎωΈ‚v0Ο–η‡o_·†IμΎ9)HN“kλϋ‚¨Φd=‘\ΕΫ]φδΏi:™θCn ώψ5Uς ηΪdsφ0…ΚΤLΰ‘Ip“θ6‹>ϊ ν, OοͺI#Œδ·‰λͺ«ŸŽb‘φvλ½ΏS» ZςΑDj—8xB+Nς2υp’7―/Kώδ₯`ξΦ0ƒ[tΜK^Γ$·Έωοςf|ΎNˆΚη&:βLςοηκI!¬E(ΙΆ]·Ηξ” EΑ}΅Ώ±¦³ΚBαoΓ,ψˆ9ƒΈΔͺ~οε°Πrθ(6:³eŠrΧΒΠΟύ +Ζ;_x㉆wκϋΒ|Œ PΏ΅εΒkΥβτμb1cd©dΪr%ζdiσ7bμK3–‰ε6ݚPUΉ8 ,¬1iςήζ’£dpdγd7)ιΤ8₯tuB*M+ΞMS?|Ι°m\1V.‹o,.nz‘›£χ±–°„°&)KΘω,ύ‰i¨Ε§ώ&₯š‘ωjΞ›ο…lΡ΅β?αn‰w§Κω#uψoΩ‘'Ω]·  SEΝb‚Σ Œ3DXΟFη…΄†+:ΦΘτχ°nϋ0‘zνπ"67ϊPΐ˜+sm•<$ίhY{dc‘ԌMZ’ψd!κ7‘\Ϋ‚!œ0ί±ΟΆ\>iF£ραR$jζΝσ ΅ΘηdN‘΄C·kzΘ± ‰ΰYΕ±™Ϋ;\ +XγgΗ|ΌcΡJ©"|ΐr¨˜’dՈƒ)xGύ μ!ξyTA~§ ―VŒxδέλpΕΜ2&hχMƒΐΣBbzζΤΔ€κ₯Ψ”‚‰|›δ·aΣ—}βъΈF kθρ;}›Ψ |ΰΒp|*v]OW§- φό—;†Β6G³,ιι³…lUn‘ξL½rΤΈ¦(Lρ!nœύ‹-©«άΦiξCY ?™T–ΨΧ‡ž²ͺD ‘\Aχ€ίTˆ+}!ΥCf˜$@}mIΧΨ<XΧΝ8[šzƒ}¦„]‘•76JχΞι_™B7GY“°σCΦςh(^ε”Δ.žcCψ6Θ•$1ΥγQx-;Š6q…‘g&γάάέS{‘|ΚΉ8Λ‰_ν—•f/N‡o£{rα‘cΡ¬y¨:―i1…BRcaΪ= FL·N½7ψŽFs֞k₯ yωέ]¦ίψγmsvοX,> –ΧΤUαQI!…)S‘Ψπ“9ΥεΗηόΦF’τΔfΥ…‡Ρ$ΟΙ:ˆP΅@‰G’"ΘLάΒ€ωtlr‘MHxχΒ–Ι,]ΩΉ΅w@' ©MήEγυτΆI5€ξ°κε^_œγrLcΊ,ŽB1Z­xqΚΪ₯ͺπxPΞ]ͺ°$œ_ηDί@Ε²&ϊH\žb§±fΐμ•Υ<8λ2-Λ―#L/rcΆέ€8’ΫsΒfyΦ"^IuK›V›Ό#&ι k貚Ύ¦<$¬€+ΕHFxh½£6BAψO&™ͺ‡oMΔ‹Οςν#μFΉ­;D&jeν£·•xŠ»qo‹rE–Δ9ΗΛσ–_ς±ΔΤE|qhœΤeƒw1Cˆ‹Λ]4 +p k±”D‘§Β‚ψO}2pυ,)"O™Cά cο=§ΤεŝWΛ”‹$ξ<ΐ9ΰY*"ε+ϋ‘ωΘ%‚œΊΙ.ε˜jΪjgΘB«ά:;Ά,dkΆ~i:ΥΥ0dBβ£@ω n”…χl™ηGΈψ0['K‡ψΒV~hΖΛήd™/ΠQ£λΆ²γ»i/ŠV―–‘Β@ύς#ΧSΤxηΤ·³ζΓƒ©‚Βƒ‘_aνπ*MαdΆBJΨρ1[&}χΑμbΠjoαθ°μ–0O>,oΤ½gήσl†τӞ$χRfΨΠqŠλ€°?Ά…n0o#’‚=,˜ΐRzH.νcω ~Hΐw™ςχϊΊ} Ε…ΆD/Š)_bΓΙyyόζ–α¦I |$bs6ϊ•ΘξuΉηό@AχΉ +uή ΎVρΐH™ν?ΏΡ˜Σ šŽ¬Bζ$zΪɁ4[^&αδΈΨ―Θ}²ΙiŒ+Ζͺ&kIτšβΐΫg~ _>ͺ΅gœŽ7NΉώ˜ɐs+ξΦχ9‰y©ϊD«ό -Ε\’Φ½ Κ@1c«t\cg΅Χix_(ˆκφχΜθN7?\ΓΦ=f쀨]ΏΞJλΪ ητrΨΆp•孏…”ώˆύTτˆJ$ζ~–HR +²Tgi¨₯;Œeλ Όhg3ΫmΊPœ'{ώšΙΫ’&«ΆlfΎχΎ‚ζzYžΚ£—’£‰•‘ E 5|/Η]ΔBt'ŒΊξφaύ’eW. b·°Ω +|‰·`―lή*ΊaMŸςΨQΈv’Ε*’4Ϊ3”gυ­RiΞ#§/$²ΛlZL½ΨW>˜ΐ»‚Žϊύχ•<Ϊ·x8qgηI¦]ϋ\(ί΅|„ ₯E\ηΏ@.ΝY —Xm¬‰―&„!‡=ΰ:!υΡh…―‹Υπ¦£υκ~·lς„!o.β§θ…ύt΅’λ.*›vL6y'ΐ“lΨsg/ϊ§"šjŠg|Έ€K?ό-ήŸίz›]οΉΌŒcdωΖ».ϊ§!ψ=ΈUT@Y˜#qηΈP‚ 0nΆ%>ΞΛΜΪ#‡x u… <–΄'2€ †ζ¦wd’xdψ,&Φ¨d‡a5­Nδ?W܌ρϋ ―JsΗΗ΅w‰Ž»:“;δΤήό†=“‹Γv¬šp;ΑEΖ2˜­Γ>U¬{H„G7KγžoΠ]SB΅dπ6( Δ·6rœ\Μ;o‰&w +g_³CnΆœϋZqƒΩYHΗ_) ΟhΊ£VΊ6ΏΑ2ΎlΞDUD^RσFc:32Œ‹x}`XU€Λ―$Œ.e€k ίώHm +Φžλͺ³Œ>j3ή”†ΰΈ7“8 ξΒpχG–ΨΞ]ωI*6ΣΤ^‚Q#ϊΆv]°ΣΝ«WλšT…»Uτ—‰G—± +\«$Βά₯ϋ8ŒXT‚Σ‚:ΞsaΫ/qƒŠ$M„—σίޞK—φ&ςSͺΧ)ζ=AρφFlΙ΅ ΈLž&Ψό(J`¨{^·ριΊG$κJΜ>ΜJw-Γj[†trέί7Ό£ƒP› ΉΑ.ΐջž…θΩΚ)—ΛΙΨ”Φ^‘y }!vk B­y‰qΤ₯q~ +™?¬f$¨ϊσ…'{ρέužτ>CΎ‘·£Α‚§“n ‰Ÿ:Λ―ω±Οτ„+³;ά£±Θηπˁ–jŸ»FX΅›aέ›ycIDn28Ι¦¬γΛΜ5ω­>Ї%_fνΠ!ƒΩ€—K@F“k[©ΫδRŠH₯Θ%·9§Ά#[Y¨‚R§=α™h."V@ύΎΙ5Cί•αΞυhjA~‹Ω»Ϊί<?ψΗC/™€μаbΨ¬·΄:i±©ί ¨¬4ŠbεžΩ[cNΨΥ?\fμiΞφ9eŽ W˜uΛΚΈΝt±j9Θx;P™0$!Ω<βΏYΡϋa|Ip2λ8S‘j ψœεjmΗ ΅ͺ±Rš*~ΕΫ%νϋ$Γ:ύΪ’s17;”£|@θβy*=œλ—‰ϊͺ_ΘΈuΒbE#– +3Ν‡™ΟƒdΒιηδΩνΠ :ΟTϋe»ί.σ3υ'Β]ΑάT’§k8ά‘¦Υ΄–™4tJ]τh“MP™ŠY5°ςΧκ‰TφόΘ“xŸžΛΒZΨκΐ8xή4ˆlΌρZyπΌΡπΣτΑ&"‹7•;τσΞ›NyJΎ·Υw/DCήΆώRξΎΙ–%f|Ό‚>V`Ÿό2‹tξ”HύO„ƒ/Φ Hruκ–]ηά3χ.6­,W{ΆpΦ=š­N±]WσhJή°™œ„!­#3<‘;α1ZeJ›:Υ +D7*ΤβuήI™ΗΆΡm΄ΛzΟήHΆΝςυΥ‚/§„ŠšΉΕt" +Y}otΣκ'°ψ+9~+³X κŸίnυkβˎ§’,S“ΟT0z6& {5ρ‡›Ξoι²CgƒψΩ X‚έw±‘υ@!£ˆχ+΄Ϊ.)ΪO?ιWΙtνήΧ{ƒ“4‘Ϋ?ν ¨bRwR=Ω$;z‚>₯γQ(ŸKώ1―DϊΩ/ΣqΦΤCαpGSΞR›ͺα’)‹UΪg&!Ϊ°8(ύ§«ω·ΒœmRЁͺ_΄ώ“ $½σŽA£’ΆKA₯#Vwͺ`»nχS¬,6,Šϋό dΚΗ…’ͺ]T5‡Χϋ΅t$’Α=h>φωήeΗΪ,ϊΨ½μ-%‡υ€‘Wc 2~Ψό—ήΩΛ+‰-(}…ζ>ΕΩ cLc—†ϋo5 ¨Xˆ‚fζ"ό +TFΒΩΚ/γŸRB¨ψWiQˆΰή,# Tχ!<£‘Ί9μ_ŸDΰ'GOšxXFωσ…f7@ΏTν9l_ίϋ<δθ–BΟ³8 τΏ3!7p­eΗ§•θNβ„Φ¨Y>ΪΠ'½—Γ *B6>2aY Ζ8οU.Μϊl^}g§`›ŽkCaΣ1@Ÿ*TθΨω–c#,(°ΔΜ•y»†²Άψ’ |d™ή@Ν3§vGಈ.d'Š:tmΝKt7…ΉΑ½½‘±©ε ωβƒp3ϋ›9L7ΓUIςΪoD—U…ΟώI g߈ΞΘΎη|s’Η蔣¬ΣΠ^˜όύsa΅IAΌz{ιέ½}6όGΒ‘Y.\”ώLE’Τ=;¬ΒU‚φ©#:#Xb^¦¨$²›=Grs±ψT^3†>lόS7’ƒξαΜ$WŠαξy«¨!okΛ8ν&ϊ9ܐΣΉΤ€bQQώpaΓf€Υ&­J ϋk…±žX“V±S”ΪGύ©»˜ΐnfοϊζ«“Ϊ‚¬ψΉ@n4ν†σXά;DΚ_V•62Ί–ƒvnΉ¦ΧeόήΪΓύŒ~Ώ„4gθΗ«Μ½ƒBΏ-~_>ΐα9ͺj™‹Ζpsλ‚₯¨ˆΎΆρ–m΄ΞVίκ˜Zτλh‰VΞJ0Mμ‚: +ε’C‚`J>”Œ©„ί¬JM₯§όL°Šx!’ΊΕq$‰X6_9δH'*ΠB’Ω/d ?o½Μ_$ΚΦ`Чη2Zyε”7#|ΎΩJίςή΄ΜΏHSΨόƒΟ΅=Δ€ΨΖ UžAη¨mΰtœΥΣ*ΰ|@Isθuρ4m?ρp¦ΜH~ϊ.Ωrπβ΄xr₯8”3ς_ΐΒ )%zζσά4X΅]«/ExsεVx~ω~~}%;ΤBφh«ηd6ͺζCUτˆ9mΑΖφu*s«hΨs'! ϋΧδσΐηd5"Τ?Ψ+t:ω1ΦSΜ€±v2f)ϋ&|ΡωΚr2σVWσύbτρΣΟ2Κ:‘S›œΞΏ€x¬do[αΓjώ"™ƒλ\&ƒAβ₯yeύ¦HχΣ\Ξ`nαώΟθΕ’ %Ώp…#]VT_;Q&Lβ°ΩTθ;CŽe}οΕ‘¨Χa@ΆF=v)ϊ:SΝ¨­άCl#Υq‡ͺβƒΣΣx.FΛ,ήωϊ±B8η$ŸEiΙ@mγζ/ΌΥοœ)€FΪΚύ,wΪΨxΏ€ Έΰ[ΡΣCl˜£TŽΨά:‰lΖ/>δΣ^ΟΝ)Γ°Ίu¬ͺDρ­ί-“³1MΧ%νΌFΚΤΘ2BH°Π|G\Q%ΏΖ&+mkέ²JNΨQK²ΐύτΩΪΧ@ŠpN©‡ΟCŠˆ”8ω^Uέ©κ€yπWŒLΩAoV_dμ/3;=ΡIx@Ž3ξ±bžι‚\χΔύfϊVΆI±ίJ^θΒ,λwΜψ:μϋκώP::€(Ξο Ά~ž +eΗΉKH $ d4}αŒqš!wψͺΘ·ˆ]8™l"§f2¬ +†¨Ά±ΰ”[+}κχLŸΜ0λqό1ι %―„ΪΪTh„η9ΐ‘κˆnˆΪS-甐ρι•§Ίd0’GxΙ†\όωv2–‰ΦkΕ¬ NߏΫΧ‰Ήεΰ†c|πιSΕΟuΣ!‹ƒ±Da©TT! 3ΏŽΑ Œ‘Ξ]x…žΌ-™φq”―ŠIπ 9z±X΅ψ‡ΦnXƒ― ~ΚΚΠ $ΑΠB€ιM1¦'ν[­χEa·T!H±ƒGωKϊι\ΉlEV:―<ΒγΣΠδWv4ŠZ6 +6V(LΦ4œΟψpί_Χu1g”·ΖXCFΉ«Ηy=FNο» 1Ϋl¦>Kκ šAΗΒbΗΌΞ°α"?u ΘΒΆΛρ@N₯l‡ς§¨/D­ϊΚ°jG„Νέ-λt.Ορ\¬¨κ•©θE’•ΑSl’MΩ―Pς ς£yυΔ@UΔGvŠ/D3^Oα-h»bΖΕCr 6Œc°08ELΗPUšΒ΍μZMxΉI\¨qΔεΣχOs *!Ι]άμΑgη΄ΞI"kύQHΊ»*~ΩηΝl€?αε_ΔήΘn·η–y¨)³Ÿ7 ΌiǞV•ΆΔيά\*B"ΰJΆz©@―ΔΜ|u–Β'3$AΚ–ΙNΙEΆ~–h6W2"{BLh€ƒNœgpΪΫκΉ—ʌoατϊι―0λ2ό³ΩKSτ +βω>›ϊό6ΆΤ1iΞν­υƒ… §Υ«ρrΒΫۊγ¦%= OΧ”sNΪC7Ζ©/ ςΡv=;CcW:΄CbΦΥΏ…gγ‡EcψŸξβ ͺθ‡V–C†o„^Ϊ€Όc ΕXGΑδOήηΩ‹Dם/v™]2[ž—H{Ήͺ½«ξuοε}NΊ£Γ2v°Ξ°aΖZ8d0ߎš―+λ3ω€ PβeQω[gτnNž<5­0@Ε†7Ξz€v"Iα^’N蠟FΌͺΗ²‰–›θ ΅Pψm3αΓ©Ό΄›³7ΆΡaop°Vž«εHP”Cž«ZUΆPύ< ωΐc»ϊώχœ€ν„ϊΑXŽ ֜ΫΘqd:Lƒ‹tκωϊΈ8«1ΪD;8υ°ΙNJχέQ7v8XΓ49Φό΅ΣXρΦΫ‡ρνΣψθgήΩΒβR¦ˆzpL!βΎ°[p«σΈ›΄RΰΖ@ ΩYœ` αΐβ¨–C4LKλ + οrˏSOI}?.;εIπHψ(PA”ΧθUlΗ­i5žβ΄Ξ‡‚ΔLvI±™Giš…;ΛWΨ‡”΅1#ύ ΚΠg$ai‡•ηΌ1dκΞϏj“›ΰ«gYΠτoTέ6€ήήΧ׌τΝ¨pΓ2‚ zCγΟ +hι¨0†-ΊΒ4ai5\±ω?š’ΞDΐΔ2c :±ήR3maζΤ4j¨}ZκBJͺmυ}Α―,ΥΙΥιgš7€tv΄.}~zο2„:ΑD΅¬¦πvξΉΧŒ—lAΌοϊ­ΚΉΪŽA²~VCΒ·pύρλμ\½ΎΎYDreWΟΪjŸwΈΎ—Τc_ήχ¨Ι²Yέ€ͺ§ΈαΧo@‘[Ζ«2ΒΞt A Ν’Όν˜τmθ ³ΏwPԘτψφ₯ 6λΊ3 ±%ZqJuΔDΠTαrΜiΰ³…‰'ϋEΐ™k5qΰΖƒPxͺζρ‘ͺΘΒφδ%Ε‚ŸW)OVaߚΧ)ϊ$§4πΆ¨Uϊνρλ†―OΤ7~ž˜ΨH'fΕTα8 Y@GζΦΘ°νίή‹6η—y¬(.ϊቂ>Ϊ³§ ©₯¨Οͺ,ά†Q™ω¦»QΝ7 ŒΡ³„ο,ο·Sγ„ΰ^††―Z(hπzνͺˆ“<έΦΛ*΅ ΄—q=HaHJά¦χίώ‘g}Ÿ#.ŸπVϊ M(ΪSςŽΉJ”χI˜]οrϋθb¬6ŸΫQ$# ‘³ +%iG6‘ο o*GtΪΗ Ύϊ‚»#[7ΏΪ[ή²δΟ%κ;™ωύmk:]Ζ†/n偔ϋ&+|z2ͺϊ­9»ςΈΣ›n €Qέ© —tnqZ!We‡eNŠα]kΧGΠ!μ\²~χ˜Yτ2ξ‚ξ˜ψκ†–ζVhΏ?o’^΄;Α4mθLά`\ι:β­μν“?ς―΅Μ’΄’S¬χ~•wBδηχΌαOJ·χνΒ4€“Ÿ*υϋ6 Ψ›ΉΕE8Ιf+π«+1%₯exνk=ψ™λ~4N”v£Χ1›ζ›0(I†BR+-΄°XiΕ”—7*m©›R²Ήπ^˜ΧήU¬FžA°ΓΔuiZβ/χ| JΩ&šw4ΫίΫ2πR±ϋ7λήΌ_§θμhͺSρ΅*E7•7P΅*’'φϋΜϊ?B‡yΊΜr%Α#ŽτˆKY*ΏwΡώ >-Iqw8}œWŸ›‘ŽΏ₯,ύΜ^ΕΙL)‰Ν³ζﱁςε(e/ΖΡ>r―!xyΥφχ”[c.³Λ~›+ 3¬%υW=„ΗξR<ΤRœ^Λχ€„υfEΈΎσ1¬λΔΥξcZ‚‘χ#ΏΖOS +ƐΥψi³SΝ‚œIy‡+֚Κ)©Νβ₯Yώ2ιzZζΒψ? T»Ό€ΖΈΟb‰ωΓΎ] ΜFΩ쁾’ε£ψ 2ό>"[T‘‹Ρ&oYGΞTDα˜++Wˆ:X#u΄‘„v_0οΛΙE2¦ΚMώωw΅ +G­ :šBηΘζ‚ΤΫO­uΉw΅}X5ΡΩκšΧ­›ΗY¨[%™OOθΣχElΠ?ϋ +/–ϊ”ψι'~κ=£0“@ίx!BŸv#'nνΦ^S/t‘ΠBWα֚Mχ‘OΩ.”o.ο‹μT*€Κ΅³²ή +qD!XΗάύ°\|'ξx’F’²E΅λ­²ίΆŽaνΗĜžJΩΦNΡ_ήsŒSζκ_βΦYj£¨ŒtΛΑ.‡•nw9Ά ˜€™Œβ_e«ύΎ.{mΉ ΏΐπΊΝTΊ΅½¨L­“|οωXΏί8ν«΄π΄1ΪΐS Ά΄»†Δ‘Ρoχ―9έt<:Ÿψ. ΊΘΏvΗC‘>žW žωιΞ(+«‰­žˆQοΚ€š9‰C»n‰L9Ά"«ΒΖ†‰½s0­ή…όήu RίΣή1ή7Ψ€½χΌύφψΖQyxW u₯ΒΓ‡mͺπvΐ/ή2ΎΖ5nΌ―=γ#Αμgλy8‘Π:`ž+€»dτωρσΣτKΛz`Ζ/ΙbAί€ω|N"‰dδι›lΊΝT67o$^Ÿ.И"―ΔάfA9_ΎΈŠ(NηόοΠω΅΅πώλX9²ό3ΙͺΥ&"£ψοωΣ€€7krΗwYX―αΐS²38£΅ϊ~ζ> ΫOΐrŒ\V²\Ό€isσΜf‘ΕψM1 Ο°ΦύιUΪΑF–Uq<‰―+’pΗ4ίcθr²`)T*έVZ&»A’b΅Z]½§D-°q&%ω―Nw.@#|h΅UΉΗΟδό˜kZΫο;o' k$ki€ˆ”ƒΪoJλ7¦+$_6hχΥ}‰Ηϋn{SS6‰ύ™TΖ¨K½‚Η}˜ΰψ·ΰΕ†u›Mš'„ŸΦθšΫχ¬:eᏄ »Ψζg;'GϋwJ6λ΄§ηΓΈτί"‡aωhu{8Ζ8Ε7ύωεdG_9©‰€-tˆΌΤ†)u‰^ό”Γq˜·λm8Ž|ήΎMζ/γCΖε,6ϋα–Θτώe\θ `Ή(5ι@Μ!δ’Δ(b¬NσκΧ\φάJΤԐLψ 9ΛiυΟL/oώŸpnύWϊσB_ΆfŸ‘ύΉ§Δ”υ§έQ9KYαfmmϊ݈Ο*τNΈd:‘X;^ί±Χ·¨">―€­ΡΚό΄HΑϊžQ]YοDλy ‚: ΩΣ-Mώ0­½q0Ž―¬CύΘ‰©ΜB5UΕlͺ΅f+₯₯dvΎ:–awƒΕ―4ηrŒί:^oΊ€N΄Φ5^œύ+‹ιγj=Γς·}0“OΈQ’ZφΰPUΥ-.μτ’&QΠΈ†t†δ¬ŠΨ©NζGΎ%ΎH°ΚγΏγv\SΛΎ½\«΅<ζ:U&eDΩ ϋœoΩΏ Om1ς^3ξΑΗDΈ0]·ͺ/·ŸEfςώθγOΛ5 +8άτΛdj"†ϊΝΞ§Ξ”4:– LWΜΥǟοdΉ­.±3BΡj:ΡΉjΟPβΡ7‘’γνf―7Ε›²cšΧβςfΠ&"Γ<λ@ϊΙZTpB©¦± ˜€…”tδξθΪΜ~ϋτΌ›άdZw:+όβB…j-"pCΙω†ΎaθXΠ•ζή‚L5RQ +ε ‰7Y%Π&„α0Μƒ―…έ–YυIšNˆi°PdώQͺ―Ÿ²OŸχ%Α όdΚΗλOβ’ ^Ι“ΒΧΰ±2ξy-SyKu·Ž*AJΟ!εΌβ;ήύΣ>yYρΞ +πbͺްYNΨδ₯p/Pξ8΅]„OΙδΕZΚξ¨Άί&ΥΟό wς₯Εχ +,W΅“όΰΉZ–=ξ—8ŒNΌ?}Δμή«s…†φ[R1[v—žFδ +'αΙ` “`ΜV'ͺ.2›η₯’ΙŠ•g©Ε~ΗψόΜw…NR Qƒχ₯ί­νΟτ± †ggnaA³kΐšλ˜f‹2Y£ξˆςbFσΚUώ1 ΫΤ‰‚7G€΄u°gνκό½έΰΧ›œΨŸπύWrWK'ΈφγwlΛ\Ο9ρ.δ&£ $Gκ*,Ξ§M‡a~Ng“f3w›œ'λZNΕFξησ·βV¨+%(ΙΟ1ͺ^„aμ³ΚΛ@-g¬dήΧ―]I磏p6ƒR―Γrζϋߊ8CνoΉ‰ΥΊI¨Λ3¨œd@χBίΫ{\ξ33MηfβcΜ{n_zρQb€Ω}Z‚8#z7fk •©’FHεh-ΘHΘϋ>Μόkς…§Zφe}„£ͺ˜y~–)›}Ελ0+뎎μ>>Ή‘F«$4ο1 +Μe-φ‰ί&œ™σ’JS…7xEr †ΎJ—οΈ$‹ψ²%‘}δiπ­gcμ+€ϋ’‚ͺΙΐ»@"~‘!)—??φK±A +u‘%ί3fx(WΝZΩqΣo£ι}5xWγoͺ r|ρ‹Iι|ύΣΎY„Ω/apš +27ζ-X)²²―YšͺnΧ›~Ή/ήηξΘxVH²M¦λ,cZΪγZ P[ωμ¨°`mσΖ*ΡΩwψ’mŠŠΘΫ―2ΑώcdξΞ¨ωTΌ©ϊoFλ^νGnf~bί­0މˆώvJŒg/o"Ÿ±ΨKlZŠΤσ's@cZšP˜ύ|f4vHaœ0Cωίώ€ςbž9„Η²ψ 3` ’ι?cΐv$Όq2g3‘Ι<ή/h'1 F―œΖv‚σtΰ’;‘’c!lμύ„ I—œ‹vΣΟj‚qΗ’ξm]G‹wzcθm:Šί» Χ'’\ίΓσςΗi¦Ϋπx3b8 Π‘{ΚτŒ˜Χ0/2Λ<£kλT“i0―ίΧ\C’DŸJisίι_Η.aIΗ~―¬Νvͺ,'vΡΎE`ž0qν>Aδ΅5 Ύ‘žΤG˜Λ&άw₯hΗ+―V€ςϊ\XΉ,§κffL H|›ηΖΩ +²FΫ€ΉbΉτ‰£― #7΄ͺ£V‹ξ¬Œ*Jq‘™ˆΆyΤΑ)ρΨ³ΖQtˆΖ^Γi# "ΪΕ?ς°3,Ό« .ΣΚΤIω{ Y!%α$”‚e‹ΪώΝpCYOv£]+~:ΣΓ€hrGίχ†"0Γί<δ>ΗδAŒ<ρW^oϊΪ·€`ΐ±\䉋=[m~Η;ΏΒMλ  yn‡L)ϊΧ4-σ=+=°Qύ³;#œν<σO|4l½ˆγ­–,ψZhΒΞΏ3Μ‘LΕ“l2ωͺzUknάEʍΉΎ ¬k?Ά°†‘8“ž¨lΨχ±κ¦?m ’z©9ΐDE8fzžͺ>UΝ¦ήOO ΠHςηωΛj•!›,"n"!ίW7W τ.{&²J’vβT§3"άύpϋ`zCΠ:`‰YŽ8ΌΖBΕσV/2πΎ]€š Ž·ƒνӁͺA³ΚsΩNvs'pΫe1NtΠΗηR'ΧLόΨΗCοΰ΄<Η•»T‰I•£ Aχ™ƒή]¨WΛx9aZ"$*ξ•φ^?―}|‘8δζ­iΝLVΰ΅';k₯“7Ι²”ρ%ŸrΘ8–~ΌΏzζΗ ΅ƒπ-·tuΛνyΥΑ/Α_]+Ά§m:…Ξ£"pί°T’JΈ2ΝL>F½NO)θRJ ΜΘΚ‡Ά}σ€°œpT +@­j"Myφo―‰°yΦxΚ}{~Ίg9θў‘ƒŽζ?ψœο9υκΊ€τ™Ό§]G‰ϋ«eζ©€ ΒKγι€ ΟnΑϋ^U…Q―Ά&Κw6‹YΕΥBσ™˜~S0-Kτͺ΄ςό#‡¦eΓσψσ¦(ξGΙWΣ/MπάjZlΚΚΖΰ}΅Έ6xJΙ•*Ζc‘‰εHmuδSnδ%²Υ ο?Ϊ@q­ίρ)Έu+U­^τw#oώΠOtΆkt'ίb’:˜ά̏ΦZΆ«ΏD(|'π§ψΪΫν(d©Žύύ‡œՈ/ά}$n†cοξcζ’¨%ΦέpXΛ€LΒI联GΒΟ\“Υu +Ε§·ƒν@­κ»–Ιm.ˆΑŒ-ΒB₯y@A>θ³­L΅guο;έΨ*ΩR”r0žF¦! NrΓΒΥ>lœ„KίΟπbςύΔ]ρλ[ΦΑDŠhP QœΏ^¦!8_«εGͺ•†«$JOt,¨‡…J]|*ΡtΣ°‘>:ΧΛΪ1ζ†Ψm<7αΠzCμUŒζͺΚΫΑ„ιΣΰΎΥ΄Ÿυ33ξψΪvΗώA’‘ž%]γ ―R™`ΥϊΐV₯τΒύοΪΧ**ψ(#ΏHWκSUP8ΈAύδ΅_Τ7ΏnTQ'ξό³¦‘δoDß#¦g²ήHΑ’αs+bN£ΑΔ·‚e0V(Γ[ +TΊD[—/VdJπ¨μ§$VλΡ؈ΨπΖβΔ’±ίοΖ79ΡG»Pϊ†_Ξ—zΩp5ΧδeΈΏ] Β ‰FrΖ,©ί’[†0<ΔKh6Аψ·•"τμԟuΌχΏΦŸ`J8QŸeg{²vͺ ©DtYw U™ϋΊ™—8Πlu„ΊΆ˜ε“lΖ JnpwW•ΰ½4dͺΤ:ώY¨ΐ8ηι$Γ₯΄ΠlΚ"—N­Ύ7eρmV©ž‘Ε Γ:\xάΠάKØπͺΆ₯•βΒ«ŸAͺιgΙd{LUP"‘h>žEΌKΙ„›Tκ½g—3£σ'{7ΥOI}@ώƒ‘"φ /ΒΈ©MΔηεψWδ<­μά«‚ρ»'vw(ύ­(τθξdΰ¨Ϋυ™ΤͺrC*œχΥJLjDηςš{ρΡ“[o Ήύ•B½B„ˆ?ZΧ“ιοžδΪ ”…nΘιΘμ|΅šQό6ί2Ύμ%΅y(~ΟΫrΚ›ύŒ›(κlφΕa@6Οξ%• °. fʊ.T‚όΰ6ΝrTg£^HS«LΏ„φ½ Θ²©£ Ω%Σ $(¦΅^Œβ.• Τq’#χ(Λ/ή΄”ά]˜¨ζΪ‚ΩΦ_-UK ŒLζΓγWΣ{›%u8ΝαgόΣΡΣΥΟ؟}ӟ mΙ;e'§ΉnŸ―I»N~αΜ@aθ—¦ώ λΰη­wΦυτπ΄\£ώ»u©φόkωgzΦ²[;yeCω³Bδ— ι]εŸΘc~c UQ`θη€}ΟXώΎCf»ΠζΠ>~£ειpΥp€ΖoP$‰ χώ‘Ξ2ΟjŒf―Lk4"χomΘ_ξΠΉΉiΙ,ΝN%Ωbΐ‰"―¬™xζΫθkυ§ςΏvVΪ¨Σκ5VF‚ DΕϊƒ·j΄Ζ<°M;ρ]ήVwxφ¨?~‡Ϊ|qάI’•ώΤM‡†αƒΆ<έ4šμ—ΜI™™nRƒ[Ο½p…Χ‘x($άΑBiH)HΑDA‘€ /Δ^ͺfΐ.kš·Έ†¦jkr·ώ΄ +β€=›ΨΠz?οŽβsbΠΪοΞ|₯~ΐއΦΙΟGJ IΏzh +`"VέSλ3bZαZ „£Ώ :]j·KτU€ZUΟfD†Ι —τ¬V2hξφ,ιψΙ+0ΕΊKύΉ΅ƒΧ*²gΙγ³=ΞΎy'…‘X£«]ΙkφœI₯LΏjl’bΙγΟ)˜U­D8±a1bΉ:Έ&|οΚυ|Ώmf/I’'Nͺ%εΛέΙΌ'ΧΠI«ΞύχKβΑ΄ΌΖ€+Σ΅8‰ίβλΚ;΅₯ΞΎΓϊτgε3ƒΆ_zώˆ£¬ΤvΦηΒ8 ύqτ–%œΛ‘σiωπΆ(rnŒέz[+Xήσ^ήΡ€ώq^ŽKIg”:Ϊ¦2Ώšθ ¦ήUηt›ΎΔPυuι;΅²ά›wΧEb²(fϊ耄mαœ‰¦Υiyϊaξ Λξπ/ͺχΌj­nϋϊU—+-KΛX{°WΠ(Da§«ƒ0σ<{70 ?eŸ5VήρΫ‡γ-‚£/q’Ϋƒ>Φ<ΪπΪ‹ΜNύκΈ•>σ"η‰°ˆq8δGy&Oω.7SιΙꌷߌ¦¬ϋyρ²M•*hΈΑ}cv_hΞͺ8Weεώαι»΅ xbΙuΊ{@ (λΛ…=Jtn¦F•ώ’@© ‰Ζr„± ΗΉζΐωEc$ΠEώ|‡‘ύ£š‘Xy‹cͺz±½ΕŠσΚΒHM[Ϋs%Ε―ϊGU³ηκ&x",Hͺ»UŠβq(ŠRΉ ŠχFΫIŽ•lΜΕ—iL½·hDΗ§L&@_°#θςA™L4¨LflWΕQΊ₯<”xrnœEξ‘₯y©,h^#ψΊŽτ”Ό_»`5 虝£žeΚdΓYΤ0©ΏcΓ I)¨IsΚΧψβά+ ŒΕ*jSKv~?Lm‘ΩŽ<4rκ˜hΓχ +΄9P‡1ύΤ1# Jπ6ΜΣ”βk°9|9ݚslΖ½Άπ2}γbΡ5hLΊύΝΙ‘ζhRό0έ”π‰­>½FŠ0ΔdΒ[LPCξ”P1ΙύN4¬Ία'š+4–B†|F^Μ«ϋΝζω>ae έ"–ΗΜsiW@‡.ϋ‘¦R©΅ŸΥγ$ΕuΩ‹}΅™Ύkš1\e™Βσqώ7ޟΰ«1Ά“±Ιo& +Gt:Θ6Νͺz>•pΗiŸ΄žσlΗ*mΠŸEΜ‰3πΈo€H’ZΏ“θ0Œ'A 4ƒ•κϋoπ%Qθn$„aλK[ξWc»ͺ σc(Š\Ιo> +hjUL)τŠθ:­!)S©VΤ/Π‘ŒpΙU*ύ#Υ«ξ1J‚‘“hλ7Π "¬ά.›WL‘>dΈΦ?F·j~€³Fε«[hŠ&·„&zvnQ?¦± VKvυzžKς„T½…3‘ZQO?ϋγΜ¬έk7~Γγe=‘ό² +Σ²3G½4—*mΧ½wn7€γiΚ8-γ%j]‹α_˜―JJ”χ ΠC:ζM £V‚~R“δυ³Λ”¨msσΚjΜ#όYwΜOnΚ`3½UP°UΤuSί¨πζΞ-‡σB»xƒΝΛ”1ωQ<^;0“89Γ]LP;j–ΙέΡHςXf, qς½D™Œχΰ©/γΙψƒŸŽdΦ#~ψc‹zŠ7KI υγχ_1―σ‘Φ„NΆ•έΆ…Τθ₯ΫG8;ξ:€nν5|\η€Ψε―Ή0i ΔλΉsτμΰσ―[n’9οω«βBΧlσ•χ“Œμ…Αg9U‘ )‹[—.c7»†­ΰι^ +ΚBwAŽν•–ώAΖοί +[lή[Ye2R2ιή”•5>IΆR°—θ{ίΨNJλ³ύ²ΔP½&Όz»iΦ&šψ:X³XΑ`š +hŠ΅‹sxG™[?VΆ~bΰ‘ hƒ"ώZρ6!ΣWΉPχΥ•­‰ώΒƒύ€;q• FF0πy4~η%ι%Ξ€εžbxΘο9ΠΙw“αύ\7αϊΞ ‘`ˆβցςν~³‘ΘwSΥ…ΨϋY‡σ*Œ*Δ0εŠlμΛ›½ ϊ ΖΉψžKΗQl‡—φϋD‘zΦΈζ x΅rνΆΫΔΦcG†Bε§ ΆέδΗ"kvG³γ2)ηΈP]ΡK&Ο—FΨkϊΫΥe ˜iJβ"ψu/Œtv«+Ι ϋΞ«˜θ3Δ N0OΦ<94IafΣ‘Ε΄―LZέάbDϊΗ62ͺ―…ώ+ΕwάϋMΔDk*βi™’sV§6Y›šR†β˜”`ι鏉3³'όΪθλSjϊθ$› +T²²'w‡Bv¬υUQη5<βσΜD|Ω:ΑαLRΣ€Ε@E5~fKιCH°ΉΓ²Ow§·gωψfMd`{ΧΛ^gΥ^];χΛ¨²²Aν-½μ/ζ#,ΚD4·h¬΅κh\bƒ’ώ‡Ž-πϊέαΆΌ3>1\ΟΜ¦ ΈuΌτ«A‚Λ7MΨΛoΩϋγο·!Pχw-šu㯌sԚ)r=„Ζ₯ z?sŸτΗAžι#²4 H5Ωi―Γή£ύŸΦΣ€Ή zwΈ-πϋ>‰αNσC4―Z#9²°);xcΜv[Ÿ8 ‹7`ͺΟ +Cω.͈Nˆ)%Ž₯­‹ύLƒ@{m‚iEά Ϊ<+…·ͺk—Υ"ΙKkώqθ'΄ΚRNP,φο’,WΦ-)F€ϊ‚ρއZςS:ηQtB}‚ΦŠύ"’ΗΰQ_lΈ2ΗόCΕ^Ξ‹s%–iγΚΓΘ$¬Q>σ ek'9ΑσXÞPΫL"σί^φ€P΅ΜΛόf_©ή—’ιj±9+ωΑ‹@OTΠ‹ŸtrƒdW+hΣ>tœΪO¬ 52qWΠ™•ιΒΆΖ²ήξ Όy$υUؚ|ίκl«~Υ΄Ώœΐΐρ»¬*PοιΐΤΫ{¦ΝςςIυόΨ`₯a°3±QG§ ή·ψαό$ᬻvτm.xDεΡ01 Ψ―c‹a?ω )Š3ΜZŽ5ˆ`—(΄ΪΈΚΕJˆLφ&`(xQΏΜ™S—λ‡π%o6t‰ρšΙ]Pqyy­˜ƒ^Ι Lέr€ΊΛϋ7)?R„Ί–δΐΖΤ1ΓΝΛΞΟGΐŽ= ‘|”Ϊz1š‡lxd(«ΰΥ ΊΦγƒΟžΘzdMώΫ½ +Ε8"‹άͺήΞφήα+3ηα΅‚‘r$ζΗa¨ΪΏ9Ν#sΨ•ϊ^X¦hZ8K„Ο€s"†ΙεΪ­»€²ύtΊβΚ8•²zΘίΏ™6Y™©)©ε%yw2ϊSTq< sͺ ΐΔv•P\δΠ»Γ³iμΚΣJ-ι +ƒφ— •V½ydα†ω7τ§λΛ­{uΕ/ιΐΦΛPsΎbTq=ζ‚œ‘@γCPζTΟvŸN›iε}^PŠΉŠφr­α‰ΕΉp˜ƒής|2oΓEOΌβE|Ι +b9rόάICjžlΨΠ:Ό±‘ν«Α>»ˆΙt'/Ps,\1ܝ|»K£§ ޟ<@·š¨ρ†ŽΣΈ'ίς’Πΰ?ρFuΟΌ†Ε,’Δ7ΗΔμa:ΐn»P4π2hΑίΘQ τ}ΪΝzmyw_%θcύW—αA2KB?“έ ΌΔΒβρ D|{$…©°1•έλvκΉ¨¦&Vς{ͺ½34`Ρ_ •ω‚aΘ—v±0?˜hP:rN™ŸΖ:^wΎSx8cΎΑŽςp`tb›©˜μsΧ5:ΆB #Υ+7 Ή 2MΞψςΆ<Κ'γ±±4 Υ‘wψ@†αj‘’OC·6j„@6"P΅Ίε“ƒΞb’πΗƒΝN|ͺX"‚GΫ|‹χhK“΅ΫcP»9{sίΘ-UΧχ&W\εΓ§9τŠLτ·aΚ6k½νβ©½Α›3ΰOΞ,CriMpγWC_εdϋήΌγ_–‚τ‘E/υΖό2 EΌT‰Hš”m^=’]F8iή'3ϋZ¬’5?/₯“aωί‰}šωΏ§ΠmΉ•Α%νBυns;ΔΪψB)£™6 }χ‡0f…ξV—―g¬μW="Ίλ^ΏΎoΰώ6·Ϋ&Mά#Ξ—TΘ34hŠσΎ΄PΏΒμΨ1O™N…{κΦφκŒ Δέ~%ήΩό\―x~’ό0ζό°»NΊΌτώ"k/₯˜`ρυ-–ΘNσpA„ΖφrΦΆ΅ψθ`U₯ ϋδΪ1©πhΕH«7ь`‡Ξ̞9H“qŽRΞϋΟΣΝv!›™αΣ;pnΆΝbšΔ―ˆCτ}iYνζ&ΏXζ¨αˁ"\»Ξ$E±Bε·$ŽγE tΪX³œΈD½_ΜΉ|λ ΆA‚’f‚»Oj)G]}ΑζΏX Φ,ΘΞΉ‰φ-ΈΖG.‡Δ”c°šW’ΐ "p§’˜—}_ϊ»Fό-4ψ>Ά^Ώ7§œ)HT= +ΔϊέΰPλ\JΏΪm§2‹M°±_Σ­-„P«“ά’k³Ϋ|;€J™ +ry ψB/>‘”€Υ*ε;q―±Wj₯|[―~C2θˆg·/xx™4²X"8?ήΘ;Χj*ΡyΎN–ΈΌ +JqΖΪ‡μζgχu1ωkνΧ#ΈxŌΓAkmΕnοΤ±?Φλƒj«²:Νc'άΘγ,ρPΟρΘζρŸyΡY z7DXηΛδ‚ϋν [ρjΑτ„™±Ήb>'#μ˜@Rλ*πcƒƒŒώδ³Ί(IΕό­P―…utHθ{IDΗεi ₯‚’‚345)+vFy”•Δ`›;ψZ όΖΪ•†|°wΤ‚` ¦¦ §©™ Q ;[ΗtάΩ5ΜΓFs$‚ΚVΕ5ΚΌ\'εʎΤMυ«΅!e.eƒΦάsHΖa@ζs΄ήyr +™Uψ2a”"τpΰΔΆΰ”ΡΙΣ3΄yΝΙ κq΅9KY1Š+(ΟNήΉΨίΪ’ž ζΔ‘Ρ¦EzXΨΜρδ₯œ‰l0Tόeό}έΩγά uQ>έšΰη ό $₯£ "`Β©φάωΨfrOtNZnΎΘΕ£„»Jό’k&’φΆqηιj_ό[“ h§΄ζ‰Σ:f―σ4ަ«>,6~tδ±ΠΞ)ΦwΖ%Ϊί[7lŒ€§,$€σΓr*q,Α₯>ο ΌO"ŽΒώώ}b‹}Τ3u€±€π²’α@_j~oΨΉ€^Ιηι +K%“φ|`Γ:cεi·^ϊί1Ž2Δ2»ΥξS"4Ι;η&<*ΣXwεΑ ΌyΚ?Λm“+ •MβΙ}ε“ΕOγ|έύόI’5Ȋkω4ƒo† +’σtcίηC‰3H“ŒOm„0\·Ο0³΅@__2ό4'…'WTVv+άΉbΉ―γZ jΞγππTΟўԟEέΊ΄Ž1X™:Wn(Yπ ¦Ί3 Bήoώ‹dα―F1m€Bz?_ Ί-ΰήL+‹π‘τš’…ΚΝtJ'±qΑάΈ4ΰ$υB¦ˆ£ωύ€· bΩB ϋKΓ%φ€/„Σjδt―uŸΓ\!%NG5Α^`ΤH²D=Μ‚φΆ(lΣ3€u>ŸNΞτFςΐΑρσ0ό’–- xνTk΅‹rγNeh˜†Εfv&Υgε0¦_’λ<Λ΄ΥΎDήκ«©΄λH‘&Α|™²Όb―bI:*Ω›RρΗΗχ£¦(Σi{Až ²όŠχ’ΥΔ©Β(α )ώš›+H§;pS–Α·~qua5g¬ΗΫ0‡œε˜½ηFΞuΒEƒΗ¨MΠ‹b†A #‡+8ω?_Ό‡#\νd{εΛ7΄†N&Q«’ZΏ΅i\κŽε~§t|-Ϋ…Ν±χΓ”Z¨…/Iš97Oγz6ΛΕxΊ—X=jͺ5‹n¨\_`$mξ¬π›sπΚΦ•W]kή51ΒΙpx¨šμ^(-Ό₯₯;Yγtρ¬uXςΖ7M‘φ9'zmΐ‘J[§+ΐŠŽΧΰ=*­xΧd„%Ϊτ˜ϊτΉF·ή {e™Žέ„ω¬άfl€Š!ύΞΒσ’ŒΫF)κ­°δ}-6€}ΠΤCρxε ιy=N‚”MΞͺ½„£”τxƒnxχυYͺKˆ‚d©Ϋ"“ΖbW„o + §(bwςΑ‹£ZύsxτT\έ νπ°dk GΤ|°p‡K}ͺΊ@ΥαZ~›ΌΆ½‘d>ΪΑIΟ +){}‰W*juψf –=₯ΉrͺΫπNPˆδπΖ₯am†εJπ>ιU1&CζνΌ{ω”Ρ}φŠ‘2€Έ/qώAθΠ₯ό=Τ$ηY"-³‹9™¬>(•-8š“rM"S’Œ{EήC’ ——.nΕzw΅„I.H)Pμ€νΊe, h½Rm°Gύ˜¨ΏJΪΰ3Χ™αšŒt#αŒ­(T2·~xxCΙΐLΘ'ύΔΡ§λώrOŸΤ‹©,^†—#0Xv’'ε?Αζ%ΒσžI­(Ÿ:ŒxΓ(a-sΝ Tα/DΚχ5ύΓr“”ά’a=ί%qΣn½³;(›/ΙdT`8mπ.XΉ)=ΰρ)?.ΖƏœ¨‘ζ±Έ–rΠ²Zχ{¦sΔθdIη!’WμE6 Λ,δ5³˜ιŽZΑ΅GNCλ£±|4"φΧA“Ν{%7φ 1ςxση3lςΆs@χ€jΗjYήΑΊNj!<θπ"ΐό4‘Ko‚aψ‹K…Ξ4Ε w.M„ ušζ`Žv™ΐ-π͚D(Rˆ;›υ¦ ΩrΩκ:βHώ?»M†¨΅m[ήΉβ'θΨƒΓ œαŽφγ˜‘ˆ.ςM~ΏFςŸY&εcΩ1½ϊם΄θQ΅­΄Ε[’:ιo˜’@ΰ%ξ’ΕHq„$¦3?Ζ]NlαΦΗw§PWͺuΕ·)Σ%Ο/’Λ€Νuϋv2Εa―QΠ7bζ+¦]―]EΏνD(yNΘΫ_™·1ύifΎ„²ρF;©νΝ™±ω’B#’ Gͺr-·Dςέΐ0υ‹ŒψOΏ·ϊ +•NρuΕξ5Η;&*T`Ρζ1OnΦρ7ŠO¦!bu/‹3Ι-s4opΟυŠΡkb L°υ^₯§vϊΥέ’Υ[¦5ΓϋΦ=ŠjcCψLœατΕΰ$κ?:Γ#™7P‚S™ζfRί6Kδ΄A¨κWS&šΧΨόR)ŽΆJ@Έ RIΊͺ±²΅£YaΈg&>υΐt$Ν₯λgrοΩl dΤ­ΠbΪςίΑŠυΈ]Vνψxύά- Ϋ—σΖ™ύU}~‚Hͺ‘9ιή? p‘2 ˆίfN$o>;—Bw<@ΜuˆΆάŽ]_θ“a&9”κddγΛόυ„€ΐϊ*}ΦMo"ΔmίQ:₯]_ζynθuΉƒ¨ΉQ±‘Nma!Φρδυ ?nuLΠR#pΏ©xέλM―ΨΤ…‚Ξ*ά¨3;1˜(@ €Œ―XaπΒ‰dDͺ]ύysNN­%ι-C€K§|ρΔyΜ­b_VΊ‰)OΒέv°>uXσΦ`ι-οœ?ό+'π¨B’œφCzKΡ'P€{";ρ<Λά`„t_CͺΣ0vˆ3dz£4ΨrΔ.γΛ0ΝKΡƒΪα Β(˜ž½°yΦΘ™ijͺ’ΧΰQκθ΄“ζρVΠOϋ ¨υ‚"½—Ž#S~­κwo ¬€ΝψΩφρ$γχ\ο2υ±α¨Ύ/i)όωŽT˜ΫxP›ήΆΠ€ χœœ šEϋ SΆV}–ΧώϋDΜΒPBΗs+wΓUj)r&V"””»ω†όQCνhύY=“ι 6€&Μp§·­ΖαΉ‹zρWζ“o°―Δ8‡±>7qγOΘ-ΤΔ…Τς ΑZޜΦ/uφ$©ZςΖωόž΄‘γΗΫŒ'hGοvΫZys<8ήI°άpJrί­’)i/ŸΟΪfV.ψSΐD«zeiSc’…M©mx‘!XξΗsόbΟ'²cV#ΆQ„™+tΟKπKnXξ>&βQ"…‘‰έ d$», ϋό m₯ouδίoιΏeCι8ΈΆ{υΞ8Ε =90lΎFdφbΉ£ +'T=Ω'EŠΘΝΑ°κγΌ'-rX½ΐ―έ¬Γ#εςΣϋPA%°ιАΆΐ‡_…Dχ2Ήέm + m8Μg―G_δ,ήοŸ7b Y}‹Έ!sΠΩ €ΤgΙΨ%’„%u„Υš]iβ&RΠαjCή±ͺlH",_έ¨λYθ†pegYεZN}½˜Š–•΅Λ Πx3υO9) »¦4°ΪΌF€ήH_–ΗΖ.A]μΰΨ*Τa‹iN.·d3Ο<7J4ΠUx«$Ύip¬wCλάηπFh1|·βT«αο€Ϊ&ίΓΐί~q3ψkΉ%k·Sύ’hPoX’a27ΪTωδt•}ΟνyEβΩ.@U[ω2R—3ό8χͺ,2ί܍#nqh&ND§Ί³γkΧI[97Ή*Η”Ήvτ[†V‡zk΅0«Ζ‡Cιw\Š‹Ϋχp½ θC£Η0εα)¨g šο`(Λδόπ8;K;/›ΙΏφ1Ά7<cΧΨ‹J’Όό‡ Mλ’ΜE+tκ2ƒ―(^=O5£IΖχ‘©Υ>$Œϋθ`*3Ήh4š;Η«±³€τ¦©$ `~ξΛ€r­lω―dS/œ=qYΘQγΪƒΕTϊŠRΌS`€ΚUŸπΥ%.HβduωΝΆ<5¬ƒΈ.<&o"φŠSσ&΄SŽι˜8΄^›Δ.gA±ύ†«―δ{&r'iόœ+Σ’TέΖo»?EΙΘiΚ:²IŸt•x1Η6JΫ’,qЍ9W‘™“€ΰΠmrψa…CuΣR/ΟΤ~ +κο³Ϋ%—8Σ,¬Ibk†γό•Ψ²Mυ³uΎScώK˜Y‘kRCG.Θνω‹Πv½~KΊ._‚8φΙΕ»κβM―‹π  ΆžŸSbΪγζςΕ²?I+˜CΉK Αb±Σ‰’,Šu€G΄*τPsnνΠK¢郕1)—τ²^€Ψ©Κy»ΘwΨ0 Ϋκ%'αϊRV½ͺΕΔZΐήΐ†%Ί3ΡF¬QpΥ + Ξ—S‡ŸtτM€Βή"³).†έΔζVkǍy‚xL}ψ‡Ιο(xνθ±od„RLβ\Qkρ·Ο†lγ4w5–€%ΌΘWκ3s=&;ˆ2ώωΦΆ]‘šΡ³"?ΟαoS\ Ή0ΞόQΟ&ˆ‘$8ΫΈφ!De:ρν˜ΰ€+π @aνέ +\uχδ tλHβ¦-А¦ό=τ`ξ8€„ΌD B5Ον\ ΙF#xΞA’ΧΡ•+™…χβΨ&Cg„ΤͺΩΙίδ―ζΒβα_3ŸˆδZq:zΑ/±²Wλ{¬΄RΈ½¨«Ÿ3@;#ί_°@pΚΠqΓ3Ϊ(Θ=bXJՐ½$Kο}—o+ήφyk0Ϊ½εuψΥ κ“)₯–1Ο1x„hσΖ‡¬”©W¨N`λΤ# HνρDyΠW·z,˜z₯j τA ΩξžoΝ’E™§#xR)‘©ΐZόΩBΨλ0žϋet ^S:`Xo"šAš;Jή•ζέ:ΩΌsΨξ±Ϊj«e)²η!ΊλN™τΏ ά\–|Y~’iάm»TΫtρΓΦZ'€ΏZvί@v +j!Έξ¨NθτΩΩqucΛΔλ£Ζ4.?N!;LQS~‡ςΫ`²δ•WE_·;a΅Μ_μ§ ›―b†Τό1ΊY.zd^σ΅-|/‘ώαΎPΎΨMη’PE“\ώμ°ΉjAW…žζυb}†al‹WŸΓψrr‰›œX~„ο[ΒςH-™άvΡΜn“²¬˜ε +ƒKΗ‹ώΈΙH\€j΄Ι#*1ΰ—3ˆ»[ZŸ’θ8x7§¨ΡΪβζ8ΑύϊA|ΉšίuSsŽρ–Ο Κ5[₯ΌΡO+Ε·ϊŸb[JYσάηΘ[OΑή(Ν>”r5db’Uψξ¦ψP…<ί;‰μZ― q!(HπjcMJΘ‚MtμiΆΐί4α4‡­όE„«0hώ·Kή μo …@~KbDKϋ’¬™λ°Θδ1A],¦΅²Φ&Ϋ΄τ|Μr]ŒwZ•c£³Ÿ7Qv¦ϊΨύΠ~Q ' #ΟlγΠ·‚n(Υ'λ~Ώ]θ΄|$74»™¬,3zΒ`!œbζTDΫ ;¨©μ‘!qUyfΑ³O pb4€Š…*£œxOžŒPGΓ “4Όξq~¦(yΦ‹W™v%2okg"W$k%η·J—βψΚ‘γ?’ ψ͊ω•άs’«UΙIΨ]΄#ƒp¨ IPa€1W&fμ‚ρ9Q{Ωξ§ΣU«ˆΓ€zWΒYD +ωϋτh”ε$2’‘±3φR#½¦—­•lΆΈΖswθ;Ω–Ί2‚ ¬Rΰ;^Κဝƒ‰ͺ8ͺd4ΦŒ…ͺJEXEGΚ‹iqϊF“aέd³nν@„]šϋTmrΜ ‹Ακ”^Ώ―+ԚΑκ\ΟπdΣ5 ZσΏΪΓ98,$΄Ÿ^S•νΐΆΝό;―eΝι{EK»p ‘<|§mΦτ7kG=œͺp\οκ,ΧL…v»άΙ§β%NΟxPθ+~2‡Ώ[<ŸξλΏ«„έv…9  ΡKdή$N:‘o‚VU›ž±TΦΝ™™?ŠΔσ“Μ\Q™΅}|ψΗ‹†ΔΊwΠNϊΈGQ‰―LΊ2QŸΝΕΜd|΅ΰΆi yγgΰknžηQοΩΉ€Χ4Οe/™ή8σέ•P νOΨΧ(—θR10Σt’x^Dυ»YηIbυβΐΐ +€Δτ_-5`;ϋκF<ž<Š] +‰θωVN«ίΚ‘‘-_41\FΡΞ[KΌL +œŒž15ρaΚΏŒ~†4,™Τδ±€¦ttohΚλMNo –σiτ³Dώ…Rζ6Ψυ+˜H¬α°•/ ¬Όk,ε0όO»%2ΓFιg€ͺŒΚξ?‚©ϋΛπE2l:-ͺlμ#½©₯ΆiΔ8Ο,₯yΰι΅˜cΖ‚[ΙΒ|?‘Γ΄ +’‰ΡΥVŸ-£“Qνa•š(}χJ$@βΉπU‹šLνŸUŒKΦNαΡjε_°Ώ£jΚi40ω{σγ α 2ψ•°o΅›ξό.[5 +‹‡ύςΡS’˜Θχ½mͺ§ƒ ₯S,y+·Ζ‡Mq>8Bι0’\΄0£ Ι„³₯9:ά²F +( £aβ<ԏžθqυZ]Ϊ;μ’’ΰλF±|SoŒ + ξ΅“7_Ziο lβ y8·τλ΄ο’ΔΛKμ‘ξη[ƒWλοB`]ƒ9?>1ϊδvϊΜΦ+‹qΣί‹·He‰« +©]Ό«—κhτFΣ‰—!Ψ֊"²”@ˆžF€Έ>d,ˆ€βžA'„w‡ΛbΡΌr τTZ3‡ƒƒζ<+ƒσ³":ΥΑμεΒ@*id5kάR‰˜ ΔXT]ξ4yTbJμ2`-’΅ϊˆ ητ¦I―θcϊžΏΉ£3†λΞΗO±iPYoή‰δ«Dζz„˜Λ@N ­iAƒ²νΛ~Yt`+’bͺ+U;†₯ΙΗιΞ©φΐΑo„δ1ζ+`‘Ή€Š‚«ΰ:̞ΕΩ₯?DΆWΟƒtΓA‘ΪmΞ‘½ίv―sΫŠrΔ΄ύ€κ¨J³:«R—9 Δ-c6—υFάοͺ)¬pnEΣMΐ+”58 +\e˜z"Θg bܟe«#΅φΟ€7[η}Β’ˆ“χ*R=OhΆΒrΏΪŠ +λ ΐΊ’Wr4κδ}ΓE-ޜ½—wΓnδ(!k”έy 70•Βτͺ₯'%σ7o`iΗ(Ζ›C°”&BΥ4ΡITσ}ڍ$d…Kφz΄3δγ―{A7*α΄ KΛ5κΓ7Δ’N‚ΥnnUύ§{ΖiΈΡ"£ ‰mQOAΌŸη5݈SΚuZ‘PέC2§ζS -D˜SΪΏϋ9€š±"–˜ΉΒΐ +εX+ςe€ΎΑ<Ÿ=Υρν + ϋ»Ί―έ΅γαŽ·G[EkŸKφm¬zzAwβ²ΒΏ½$ο'7κŠ§’δ‘+¬2ͺ…Γλk’ζά&Έ6eΫq +σΎΊΡtι±…PuΑMtς›ί7t1@‘6Ξa"˜ξ€–ξ5Φι3ŠώΎ©–$Όα/+Λ’ž‚UNσπϋϋ-7”ί¦Gφ‘Qε‘^ψρψ*&>Ω#ΪΤΜM&•‘τΔs—&Bp՟΄§-‡ΛŽLα<ΆC0„8%’†ΝΕr:”ΐΆœJ΄δκΡzY4e”’r.ˆιΟΠζŠοaΖRpΠ΄bΰ{[™δ"σΪ-5πψFΖƒ¨!Β‘nΡcSΓv΄1ΔͺΰΧ8iVDJmR2¬~%Ϊ3°ϊ_{&Xπ―ώTν9ΫΚDΗ’§ξ™ueΡ)φUσΆΠχ‚όΆc΄Ϊ[ρR₯5’ `4°ύ?ιq›Ϊ•ƒ΅l¦ίWSΏσ–?:Ρηz0O‚ˆz;³xοWVώ!ϋ`4φψξfgχ[“+ηF©ΚFŽh§„aΘΌšΠ-Ή’΄§ΓŒί0cϊμχί9έΕ$EτJΠ‹6τ /0§dοFI—…Δΰ\vπ’t€œ€‘Šdn— δ=T)ΐH"΅kΨεxtŠpΣΖk‘NGžεJF3³—/χωλ'ΪΥδ¦’½LΏu 3μπ)²jΰσOaφœ*3Λ­#œZ£r jOo!JrΈ|ΊςυQ(Ϊa€εΚŸΤ.D ™ΕVdO«ίNˆwΙ<’}PΟΉϋΩHZ+Tϊy- σΔJJ6}ΩR*¦ ¬)=ΪΌΊD±N0YΫ—ΏΟ£)ΰΝ΄ΞΪΧb \Q‡SˆEΰΙΏ§‚>Ÿώ·?φάςMWσ§εΚ +Ρcφ– ¬%Ϊ-¬φZ ΓΣ*Y€¦ΠΪύ Ώ5Sω aδ·ΥΤΥVn…VΜσ):b]ΑφbyδMΚy+9Št"Φ,Ελ// LγnlIGβτM΅δύΔƒΠ}6 +>§`MWN+ΦΞ_ίυ‰²PdθΘ΅aΕ|Ί”y».ρ6 ŸdIe―{r§ΫuΖ90Ι/–6˜€ -7eιΨ)ΨoΗ*ͺz yάχ˜a†¦ι€ULN.§KˆΓ™μ‰ššρuΣ3νΨ©ΜθŸψƒι:΅LιΦ*|K#ρΖ+ηϊΌη8――β|£ΊSuHζ)ω?3ˆΩœC[Τή]I΅g1M?–•m*R@·΅!4έp.ŽswΑΫώ9υΉ­Δn‘Χ‹V%”ヲ+™h^‘tχ_ΕΧρG΄dŠX0q/S£ž››0† •š +χΔfβ\O7¬=6-€N:±pόΑ) j*¬„μ†cΞ=Ω•;˜vU$―|ΰ±&Ώ4ΑV»λ²(n{Ά¬Μiθ6ρ )oœBξMR_žœXΩΣ±ZΜݏP7TπΘ€}«d†‡α δΣΎGΥχ‚IΟ[1–[’Ύεs·rΞ‘†OWEδΏ …)4lzœΉυΞPϋΑ +ˆKy‘|ώ;΄ΪΫ €ώl ^7~λυŒ[e’ τΫ]%­ά•…;ςΜXͺ―}°†ϊ5m’Π‡¦{ι Π©'yŸj{3ƒϊ‰‘‰BΛΝ±Bœ J^ΰν©ΟμΟ ύJαJe9dΧ:sΌqp }WTsΈŸj gͺ‡Ε0έbw%ϊ€2a Δ7’Ζ‹—ŠƒχυχύΎ€;;IˆΒ7φƒΦΪ±žŽ›okΐh8zƒΒΡα6O²-0­ΔM!΄ST,cœ!PΌΌΜ a ‡ΓΫλφΰ’cΑΩI„ΡςςUž„$Ϋ P,πAŸ–Ώˆ=ή-ΟQš΄φ—αPZu8φΣGύ#ΓxRŸ)σγ%_d»vwq Έn2υϊΫύ9ΙB,ά―ςξamuνΤ«Υ§ \ζΕ#FΦΧ’,»gύγ>YwuQDΖvνηhTwΟΙ/χβvEΧ‰Q;1'j9FΩTDa9΅(i;ΨΘαQ€VΆŒAα¦c½ΤNn’6Ϋ1νZpœ νο‘|ε3οFγ?PC–υFΌΘ:νi†etDΫϊη“%idΰAYŒλAx^¦ +L•o<¨ιΖWΉa“Τ‡φ₯ΝΟΥn’ΝΧγα#ΰA0θ³96ƒ^΄Rš{ΔDγ! Sή8©'ϋ7€ZϋxpSŸ0ΦcΉ―πuνώ―ΌΩ8T2ω?ο?€„ƒΰjŽγβφ€κτ―DMO΄ήcqι½pϊ'Ή>τω€GΔ5ΌBΌρH€ƒ:%Ÿ9l«_iΠ­Ι'?ϊ‘VΆ±ΖVpxgξ½{΅œ³ς;> + gΪs£τm»y6Ώ$€‹M5"o/#eT|ƒ$‰œϊ™λ‡HΉ<…ΙϊΝ Žγ4oΖμμ=T–ͺ w¦©I$ΗKΡ§!― »Β흱oΖhΪ­(](Δανζ₯ J…bωtΘ +ΚI“΄±‡βςθ!}ν Œ{ύ Ξ”?BΓ»™³ϋ?d.ƒΈ |W‰Τ¬YMΧu.ΐΐϊuh»_Ή£υΈL`nΗkς IW†—F‘1Ώ άbΉTŒΑn/gΨ[@ωZ`–oΚΆ0Δό^Σ—Φδξζ­>> ‘ Η|VφΈ¨B…؍Hkω2¨SΟΦ„|―$a[·‚I’R›‹ŸDζΉSCen΅’°Ρ«f*CZ›:W +e‰"»‘„XTͺMYτ9pόv9Ωε ³ͺϊXŽΈ*C Μ §ΰ±„}o|™³•ΉΠι³@澜υΰΝΡ(2eΐ=φ°Š(J)#Σ‰…ΣΓΛΠΘv‘0We…ΎΧ?r TvΨλsψa$yˆ.kWΜΩΧ\%±ή‡5ώΈ{σΥ’²yœrX<ΓΗΗΝ’Ι-—οΓ^Q6«½ 4z˜ΐ©§1ΠgNUΪ6+6‘€ΣŒΤŠΠ;fΫ`τ`ΦΜΚEς΅ΐŠ4kΣϋ¨#O›τQvLήΞ&7΅Xκά› +Ώ xtγώ‘ο,lΣm2#ψ :°Υ]Ο[T=€Φά±ΰSΛσ±Ό$UζκJDώ ‚§CP±„ξώπ»dœ φ>?)™φθhc"»Χώ½Ίή‘,<^χ’ϊ’ηŸδΈ–tΝΪ\ιvb’0Η±Τψ p²­4Ϋ«aΟα―Đίt„έ@>κJ,'ΆΙ·P(όC'asŽ'β?ΛK +z)Ϋ+œDλξ$9ΰmŽͺσψ?Ί9 +endstream +endobj +856 0 obj +<< /Filter /FlateDecode /Length1 1366 /Length2 27501 /Length3 0 /Length 28461 >> +stream +xڌ·PeΝ²5»Σ8hάέέέέεΰξξξήΈ»»;Cγξξ4ξ2ύέ{ί{σώ™ˆ™8ϋμΚ\™•YΉΦŽ(R"eZA;# ˜­3-#@XVHƒ ΐΐΐLΗΐΐKJͺbαl ό–T θθdagΛυ; ΪD βdνlR.ΦFf##;€‰σΏ€vŽ\CW €,@ΚΞθK*lgοαhafξόw›zPS99Ωiώ΄:ZΪd Ν6w46΄(Ϋ[=ώW + +sgg{.zz777:C':;G3>J€›…³9@ θttšώi ghόwgt°€s §Ϋ•νLέ €Ώk c ­Σί[ #ΰοζeI€Ό=Πφί`™h9#γ§ϋOτ?‰,llhllgcohλaak0΅°δΕdθœέi†Ά& ­μώΖΊZXύό«rC€˜ "ΐπoƒiΟΙΨΡΒήΩ‰ΞΙΒϊŸιIσχ”EmM„νll€ΆΞN°Τ'bα4ώ{μτž¬•­›­Χ¦Ά&¦4aβbO―jkαΰ”ωδ― φlf@g+; θΊ›Σ“^ΕΓψ/'γ?ζΏψxΩΫΩL6τ±0ώύƒυr2tœ]€>^wΗ^Α22L,ŒF@3 [ΨΙώΧ 4ύχϊοπ-άΪ ΉΗ`ψηχίoΊιebgkνρ?πΝ—^YUCKQ†ϊί·OHΘΞΰEΛΒ  ebeψΛW6v;+3ΐη§Q0΄ψO +ikjΰόw΅ιΏ*vύ(ώ#JΐΞ%gχ—΅@Ő\‡•Αψοƒρ7ΥςΖπ²ό‘ό,HΜΕΪϊ_nŠωnC kώ’ΦΕω―dνώΚΐφ„ͺ-Z!;k“Σ'ιlψW‚ΆfΦ}ˆNbξ@ gcσsείvΥ4fma T°s²ψη£ ed`ψ?|…elυχΓατ—rκζo)jklgςΐ˜XΩ†ŽŽ†°GόwΕ +πbό«D ϋΏ(  §³΅sώψۜΐΤΞφŸy2ό/;μΚmμβθψW\šύߍkύ/%ξ@cΨΥ%;cξ`Λ†ΰη:A\7ΪΓίΌσ€‡κi”΄^«Žέ.―ˆP)”΅YΫŽ‚)cΘϋ’k„^ηνMPaIŠoήοϊ J³‡°+3˜ΓΣE炍Cψ0x΄*GήήjV`ν ½R€y.ˆ +hΟnΏΔέ‡*Φ'C—jΩ€αή+ζhcT£uJH󍲱ˆ!iρ‘©P―έ‘ηQs§Ώ₯¨a}.b˜‹½΄v˜b_=7«T˜œϊ°`kaαƒ= NΞ’y €J}_φ*+‰hΚ_X,•πψΣyp{82SoοΦ™raC'ν3§‚Η;³\6tΑfιC\\iUΐΊŠ50eΘ@,ϋkπΝrΨζτ·γΗΩ’±—n υΥPΟΨ2f–Ϋ#νMΠ:ψ+‘Άμο΄ζ₯n¦!cT‘>΅2― +°εZFΩFε²E―† †‹9i˜έsτ‡«ΌtΒ†ž,ӎ=ΦΩΑΥ³ΥJ¬ν˜ „\iΒ:ά‹χΡH˜€/D5Ο܍¨P5όό›Ά?+|·M,’Εδ²ψ™¦(ͺa@α{Ρ‘±h„ΩήG})Ә–š€-h’άzrYXθœΤ‡?Ελak™Ώ2ƒ’Mκ/ο[Š%„‡Gϊt:–I½Ÿ­F†5!ΫTϊ[œUΝΛ¨s, jg'YJlη΄s χρΰώ°ΌΈϋ Μp±–υΏΎQΦάIΈzΛϋ&(m§˜μΏAΗj‰4qXΚ¦{DG6GτŒΑ[’±HDU¬ωo€GΨ{™εμ}…J¦pζΎι½zρᚁvΰ <³‘‘mIδͺldηˆͺ-βŠό–1Npm,ˆΝΩNυTK UΡ‘Ό‘–x*E²ω67.2)ŌZ2—BuͺH„:tς°‘fSπ „ιAVεΓxGΙz₯ƒΡr°­κ‡Ώ~Λ έΈεΆtψψ’“>ήΚ|s5œ-ΰ8pΦΕb58–£žf>«™νΔCNf5Gst’6ΖιΙΈρΚy%mPΙ£΄”#C£žτRn8Ÿ +6EιιͺX=oƒ ξ§v /+諜,θRΙϊπφezΝκκ’ž9±ΆΟτ8m€/2Ξ΅qΜυGˆΟε’ΟrΊΛ―Ǐ‘†t―o›οΜ\«όεό°N{Rχ|αΨ™7ήIΚχYEή²uΨƒL«["d€’3――˜ΩL H.ΛκΡVOΊΕˆ™ q‚^žιfίiHE’ίSζΗpή38ψ5κjΆΒ Φ ΐ”0ψ-ξc–η—) =΄~ύ«“qό†³φψΈhFΟ¨«k΄Χ9‘ Ζϋx σέΒ /_ίx7[St~œ3»c>$Šj\Wά¬Ε=;Ÿ$•¬*Δσ|Ά½ήςί%Œίύ·wφ­ŠχOΗ5lΪB—2y>ίA†Εΐ=ΰ 4i‚œ³8•cFΈ›z>P‘-ΰ6Ϋ_zd˜·ŽΜξΦfTάϊ0“ Ζ―d1ˆ ζΌ‹/»2n―"4Zφ +2…uΧ5ιωέώήΣϋ"a7“}³9φgΦ8Ω+Μ[pν@ΟΠuw~|σa$=1ί9žσα«½£`Fλ4$A'Ί)ΪN}cڏ"NGŽ΄„.: |,―Αšσ΄Τ½ΞZB)’” 4-8‘Εcς»²·ΫΑϋ]_VIΛ‹ξ_c:)•IΗϊ‰’Ϋ "Ψ‰ξ;ΤpWtšΨU–γΦm?ΒΕΨO)ŒΛ`Ϊ3_Βή<°β.ϋ±P2±AS|ŸOΆPδ 2H§‰Ÿšΰ}?Οpr9$G¨&·φέߝU‹Φ”·Φ_¨Μ§jŸ£mO »ΎέN=¬%Ϋ>λhHeVΖώυη&„Χ;[ω¦ΥDφ‚,|ζT&ε0ai#ι¦[΄Γ&Α|@M.…L±Mc€‚6ώ˜Oz\z½Ά0'ς@k]“Γ"‘$c’πΖDΎ[IžtPΫ¨ξρžΌ¦ηφH'έ‚d―ΐbκ +_GδΑ l RMΈR6¬Ϊσ)Lo€ΐ/Ο18­Šƒ†;­œ.I”‘Œ˜¬y?*ΒAΘπ)"*”;^Θwl—-xνΨ εˆzφU—ΛMτs˜τhΠ‚’—ήC±-era΄΄η_C޽2 +ρυŠ˜#™-δΈ)’Αβgr#Ξƒ¦VΞΝ›Ίfx !}ΆΡ^Œ<£ώݚα>υ3ρbŽbOšΰFM¨*ό²a=ιOλ£ΈΉe ˆδBžEh^<’ζ™³ΤdŽΰbΏρŠ…M_όΑ²!Lό ΅Τ|6‘?uc:\•Οšγδ— V†¨†vά ©q Š o¦]ώϋ5έΌ(ΰšίΗ€cΫ`ρŒ(E”v»—‹Τ߿杴؟mXέ&³ιUHv/«Pμu`Z₯ωΟ:Ә³ζ¬ωώ>εΚx›Ζ”3€C'α&ΗκΆMΥ L|b)O!#Ο‘„‘ήλŸ`ό₯¦φ7γwmw ώ€ °“6GΠϊ2 ¬$\gŐ€ωY•+-Œy“ό\Ί΅9ς•5¦…»–ΧF ε‹ρ*'αƒL%Κgo^SϚεT «εώβG?!eQ½΅o.ΕiΧδj223σx«~ώaσŸ²QNΛY[ φMΥζγ£ζoΆ+Ροτl~νw5rp¨ΝžΜΈωπ›S'Ι$Έρώΰ=ΫƒΎ…Ρ/‘Ψxnόkς Ζτ[xΏ!  !ΡΟΈ•»QοPV΅!œžΛd΅‹ϊ€μέwΖ°ΙηKF υf8± £λ+SŽ:Χ$έ΄/Wߚ u1ιyR+tπ8]Š/"šn€˜έƒ)@•g8HΔ +-»x°Δͺ;ΉΑxjšzκ¦4οΣΝFΘzΜΎ•ݍ8pj΅£• Ώλ«ΰv–Ερo’ΩΤH‚‹›iƒΖΚhDšR*͎.ΚG†ϊw½A0X?`6΅Ω α2ΊYΘ°`ιΓ€€ށp…ΒEΏH€*ΈΡUγΎγΞΎ1 ΪIΑΌ#ͺ£ΩΘ‘ϋyžNω1ψ¬ƒ#<‰± φ*Η–ƒΫQxώ•AΜ”ώoaή_lΡβ_– ˆ™Yl:sčxιΘΌϋ%?ύ0-…h‹$ +œeq΅~Ϊi3ΫΧqrΐ S0ΦP€χ}ŠZηˈš0;KQϊε&$g%r ˜ϋ‹bʟ֩‘Ω#ρΰ6λ―‘έe8.Q '_³‡’ίρh°ΎD/ώώΚΩu1Ο9OR©ΠΤ€\QσmΖ{––ζAΛ9>‡tχ1΄l’Ϋ§νΛ\Š5O, ‘uΚ£˜ΔγE'ψvΑΙX5£žaγ΅‚…?^Ϋ©Žg.'?jϊήη’ŽΜ ŸΪΗ€«#fš4՟Eα:Ϋβτ¦nνΙ6_@(ΉƒŽΟ¬ Όn_§εΘqφΊG ΩX¬kywšβί©KΧ]’kj i6s›=|ΌO&ύž3bΒαφ­Μ―ˆ‡΄Žj4ίr•έfΙ­;©πQΥdšώβΔ|/$ŠHΙm;nُ@JΜQ >»οR#ΏWΝ;ΥλΟ +ΖP₯‘ͺB„ψωŠθ97τjΎJΎΛSšάF¨Ε€ίWΞ]΅Ϋΰ’•­>ής¦AΟΰ‰}k+N)ΪβZF<œhx‰όŽ0ώς€νœa»χtwόπ­"•ǍΏ‰$eζžf€Κμ—£)°.¦7jΏγ„’CΛαΜPω}COښ[ͺ~βεΛΏ!£Αΐ±kR+§ό>€άφώξ’Ο‚§cœŠ’ΚgH¨Ή₯φΡ“ΐ€ΰΠ‚‘ώ·4]“²u§|P-Š^P†UŽ―5WšΥE”Φ’[Fކn{Κ>©b5 ώρ’D3g’φ1ŠρGˆα}EO‘ΒΉΒο‘9nΏn(¦g­boκ—ŒιGzΛ‘‚„;"tΩ=[κ0Ϋ€ χ«ρMβ…z,“^;±*/fχ–Φ Š3'0Σ%=|Τ”S_ΐ&χϋ·Cβ„ΏG]bBI§°)»x―²½π>£ίxƒWIΪu*Οc՟ίκΰ‘γmΜ—4Ϋπ`\Y­ν+χ—Ξ"”|tΏ&]΅Γ ;!BVq@ΰšΠPlRή8μOo[lξ›uΛ„’ιπUJ&°μΧύΜk0)hΎE‘7³LΧ +j•“€έήΙΐΩΝ’{/FγΗΒ/~zyiΰ£xΫ­Ο°9/Λ‹kπuΟ™N8’³0k5S+Α u +,£ξΑuL”"ψέ€ƒ3Ξ^αwέ©Ψ]!eΚKRoCIpšΛAt,FVPώ n±m%”ςz};‹ KνχD·2ο‰β~ @q]«8Φ(rΩRπŽ£:„ωŽ’ˆ:ž‰Žb4‘ί Td86Κο κΓ(τ•*±ξΖ9μwpk;£Κ.b'ϋγΟ₯Ь¬Υͺnβϋ ŒQ€Jν¦SΪF!$ΡHr…‘?dυά¦n›ϋ§‰ΗΑΗΦVŒξ$1‡‚l‰₯BŽω@W c›€pΣΣoω•Θ·Χ`~a;ωמΥož![GόΊΘΌΦΣ,£C–\a±’ΊΩ/$€M}Π²ά"©«LΉcήpρ0S^“—Ρ·Όs"ΕξŒέω^<“pŸΛq+ŸΠ΅ g4ΟΠσυAPΆ‰vœΫVυζα_=0HΘ u+’‘Ω­JWcωQ +η %Γρ«Χ*8…βEF`£ΌΩOΰd΅SΚ«ΏηΡ>=θkΠ• ϊ›E G†»Π­=BŠΎ³E…βŽ#–(] ,8e’!fDε΅‘xcθ&Ϊ†³φ|q›’^Ψ_CΌ/?θ<φ―YbŒ5‡|ς©8RνΨΕηθ‘I|γ6I½­1p;άBj˜½Ω²N_εΜ’ƒv—4uΓσ0‰B‘ΖΩN'Μ°~ПςBτI€ΰγ‰δ―ΘωDό’θΡ· ”ŒΊm{ΐύ&‰‹‰O6³Ή2Ψρ{-AW`α©Χ:ύEcY™ QΣΥ\&γΣ4ε—‹˜Τ&œΠσrΥZήαΧΑQŸZŒΞηΡ§Γ†rΠ&υΙ©›;Bηρ‡bf€θOπoˆIΚ9MYοtž’DΤ5Όƒe,’S;ΘΓneΏG^b +ƒ4ιTθ“έAΰ¬δ€ΓWΛ τnΒΟ·Ιλ$Α»7λεΔb“—θ*ΐw/Ϊt³©ˆH +­‰Ά“B»χͺΕ½ΝΥυ½ή"%L`ΡΞ1~ C±DπΒ Ί±Žθεj’ΉΗ~JΈ›'GψΨ³ 6lL Vƒ€kωBΟJΪ°“ί‘± ΄3 ξ.`ϊ―j³N7 'Wψ½D”ΔpΣ5β0t:— ϋ˜‡ΆˆΫ‹°'ž"€Ρ©ΟΑ5\ΊKηζ;K}dO70와;«Ÿ›ΐ +„'7`­›ϊYœxΣ:ΕƒX0CCςί]i•1+ΜζΫΔFƒˆ$/Μ0<³rω}ΏCκ›ΉιΝ:„ ιηZF±yΛ3ΔΈ^νmŸ.βPeA`ߞ¨ˆϋrωξό«'\§Jaf¬Π?O˜λ’Xό)ΗΕ1¬°jδa±η½ςy•»5Κ¬*Ϋjt—b4’γm)`«Ζ °χψ,c€’ƒ£LμΜSďMͺ:ˆ²0ιΘ”˜5¦υ†SPλΛo +š΅Pώ9ύP™:δ«7%ZΫ™~GTΤ‡ ‘Λσ ΦκĈή?-τΈgŽΗ7dξΦ&ƒΥ(υέb]ΣU;Œέ‘Ωq‘ΙWͺΦ^9H°Ilγ%¬Q%PΆU&yY‚ΣΚyK„­Ϋ@4LγΎ]^3]πΝ•M!i~_ΟTΤΧ.Λ2΄α0£+a0;ꈟ u+΄|y6ά!ύ АƒεQ, +ΔνS9ρλ­“°w '«οΪywG΄ξΌΘΎή€.Π‡ ρwΫ₯σ™:ύ~­»vζ=έ–+R%»BΑzΠN@9½θbMb{ ₯΅„Ώ–τΣ¨/§Χ¬9ρ‡š½QC°+4dBfΆ½±Ώσ9φqδ’AΙ±„ktΔΗΠήLΈ¬›σϋ‘ό—=ύ‡ΧO§G0˜ŸϊΓl"ο]4ΣΒ©UNΒF;wΫ―ΤαpwψI՟άΣΙ‹¨b©kεaIωΆI1Šυ:<Ο¦$Ϊ‡ͺe0 +Pώ£^™»Νμa(Φuυ)…‚ή—\~jξρΪ7B~‘*/₯_Ο‚bVT(‚bJ΄9ρu‘ +;oΞ\‡cMYzΓͺV·q-T33sι6°I>QφΑδ%Ά=†ΛƒΓ’•‘u€²™–CPeA©6ί?΅y”»ό”d, °B‚ΣΛϋwΣΥu‡o»«ΙζΫΝϋ4όιί™4Ž6μΉTϊ³ιΔ£’GώN˜<Œ3Ζ­ YΧλ­~IQ%2ε}DVχu ξGΘ©VόώGjhηζόΓ€yελ'G•ΑΩ CΩ%…•‹!#Dφ؜'{T՘+DΊ0ψEv—ΣΖό™Θξ―ν’e –Αοnρ^Π1/ziΩ8ΰOίGαο‚βσΗ)’'Χj_θρμ3€εˆvά¦CS@οιs!d>€…"Ÿ7’K%Y›ά.ͺBΥ;”6œώύ72=b™ί…u~‹­ξ½Ι„„–νD2‡ž½;&Π]Ν9²WΫ+άZsΈQθώΥ?V~Žζοσ©π€fF[œΉ1 +xsFζo4&’υb^A<ΥGά –žαŽS}mSδe΄δω:uγ»θ_F+1—Z¦ΰ ΒλΓθkf˜-Ǎ69OŸΣjˆͺ +‹ Ο(iΖ‘ήέV9Ύ5Ο•Μ5; +€€‘πo΅[žd0ζs0ρΘ1|y „·-—…ž:ΊI χzή“«™iμEζͺΩdΔM@Υε$°ΘQσ΅κγ.P|&1ʘωΛυς(Χ9aτ@μΔθKC‘ˆc-ά‰Ώ^τLΗP[vΐΚ‘^₯I‰έ˜~dŽ9(£oμDς Χfχ‡‡Xηψ"ρωn p‘>άdπέ΅2Rƒ‹1Μ} ϊ{U«’V±Νδ0ψͺΙπ~lφ\ͺΎ"&m Ι€’!l~Cr[θ‚ΤσˆE €α‚φ3/o½!φsέ#h=Q½θξ³uuΕ΄ΐ2Xa5 B:LWG¬•ͺ€“»Ψzd(ΨΏίb“A…μvhΪΣΕ[δδƒzv*a£'HΪ‘Tυm^η” 8/ά²“Zω΄ΊH*:1*2κΚ1ςb9ΨΠ«‚^TΉΐvYI›nδέρ―ΒO£‘7“e_1Α‰δΑ!zC!™Λβ‡ll{η)r`ΜΞ“œIΟNλΚ)\°­‰h€ E-±[°Μ€X‡Q•χC7τf˜-. +5ιˆε±g|ϊ>YΠ ηjγj]Y·c[m„»ά5'μEΜZ­K»€³ΡodΊΤ›}«&gP”Γτ‹ϊ=ΆΫŒ„½@ΣyΉ£9CVμ…ϊgα7ΣώψΪ­ž½0x¬];$ mΓΛ1$ƒD‚\SβΙ·oΈJQ GlOSπk=žz·©ϋάKΚR3}iΞΈΩγGuLb-ψΏ qq¨•ͺFνΨόςHδ ςΕΑΩxλ eΕ?8ήX²”ΎχŸŽΕΆΤ{ˆg€ΤZš|²όξΜ’nΤi*ΗNξRΆ4ΡTB韯b?9½Ώ•8q΅γ°‘Ό>„ZΒ^j,/ߚ΄HβάΆΏYζ=ψψΛ‹†ίοςζΕί"ΪsΡβАšΰ—ξi9Ρj:Έ&ν*_DθVΒJHˆz¦ξŸF™9ξ& εLφ]Qς―RKΕ©³½u.œπ!ŒfZc/ΐ›ϊu΅ΜUύݝέ‹ΑQTΠ₯€Ζ01]{₯.˜`4Χ !y†Ψ‹ϊGTτ,Λ±¦’zœΡx./Ά#˜†λ\½!œτ[bΨ™V‹hΘg/β”7ˆ:wwi­νD=D€%n W‡“hClγ}&bΟ^)w\;δ,x-΄ΉS[Ά]$1Ό)MH„ρx§h 3Α‹’w£ζ@{΅ͺΜ#Ιm_Χ–-ύSηΙΌώΛxBΝ¨Σ‡•ˆΠ‡ŽbψeσdΉΪή^Ÿ'’‰Ή¨³Θ0!aG)YΗμ*Κ©ΐ£@:£έΥ7mcΙ·P‘*·Ή‘λ Ο“η‘UΛwΔe‹θ—³"₯LZ/XΌΘΜ>ΨυδCψˆΙš~˝FζΫ(£vŸ1Lρr11W‘.νS4xκ½Ψr4λUΏ_%F XΑώόξ^4\z‹ϋνή··§CϊΘEtΆά‘Ά΅<)“RiWŽZΗ9„ZϊοΕ504¨n+4Qˌƒ‘ύLw†9r–Κp‰G‰ q}}πO$ψΠO£ +EeΓΜ#4Ϋν—ύΛ’œ%Οΰϋχψι0ΥP>Χvž‚<$ΈΖΥpZ.&ύψI­€z†0GΑ©χΌn ‡ŽbĝPΚΛOkά(g2γ?|ΪΡ΅T‘mœŠ§YΠ?vVαρ΅ρΞΜΆ5\ψυeρέ Ώ ίη₯grΞΒ+Η§Ÿ°B°DΑϊΜω¬y·`ԁΜΟm[σς’Tˆ΄EΡ P€n +άΖŽ ……lΠ9h ¬…yβQΆEΔΎnŠΰtΛδ3Δ=#›7bFuD3rŒ‡Μ΄ §ε9yΎ&ΒXν‹Έ•/p›΅!(ΝRRΞςΔΒέηY*Υs%Ξ;ΌžŸWδή–„Ω½€`d^wr ΥΌΈlKΆ[1~?¨'ώξΨ αυ™ΈRσK«?©Γ2lB¨μyi”·νoσthδf ς0ψ Κ­£Δ η±y6¬)mM:Ξ•{ΝM‘"Β…LΣ»σcξό‘Nͺ˜žAς΄ŸΫ€Χnͺ%9HΨD±Ή·‡NvΚ₯~HΰH‡ gΪέjA„»ΗnϊεΩ€-{` ½Οqͺ€˜˜!3MTΪχŒ6PU@χΥVY4Φ>‡zωΔH§χy™FωsΧ»ιSΐΡb3Β—©_%χ―΅ΙŽ Δ—@εξΘ +Ϊ&τψΤ‰OΨE•IΫ­6υoέsζ\Ιx❑™|W*%³Α_Ω‰΄JGΫnεeω/+θw$ί+]χ2ƒψυ^•₯³T{jΗyιχ ξ"ΐ‰ά1>|œ¨V³°2E2υη¬l±ΣF>ΖΫz#΅ΦΤnؐ7_a«]#£ρΥΥM^Y”΄!P­‚M’ΨΎ~›1"pˆΫrdλ8Δ'h’ΡΛ,α—dŠ3oCιΏ{Π9Δ׎΁JΞH`1Žt{W‡ΥυžυΑq4Ϋ ζχε/zž&B–²›\Ό|ί +αI6Ξ97«Gš―₯‘\2jœΰsιRŸθ’ΕvdD•]€Μ.ΒΠΞ‡B",%k±(8²#WA,ιΌ‡Ν½eS(Θ€DŒΉΙν΄α―bχ°-­΅gΦpηΊt#Ν£|2˜ζHUŒ£…έ>ΫΛ ώ‘ΑΊΪ†Fΰ$#Tϋ³ΣΉ·«ρƒWlGΝΔ'ϋ*msE–»LyOž§³€2yN"ήγJ*λX 6‘[™Ρ†jJH†ίςξQw껦²Πξφ»rAO5α„-N>ζH:C£YUΑΕΌΐ!R¦vžΫƒΉζνεέ~Ύ\σςβR+-Ζ‚ΚΟ’ΆYxξš2»θβϊH·ρNžͺJΘζciΠo£ΥΥΫ΅)A*mάϊR +ŒΥus«Κ¨²>ΐ؟Β6 λή―¦Ή-—`ώx―Μ9N„MAΣΓΕ“|EυfκΧ”κWζυG‹Ξ ΣΑdFYΔ읯=ζEμ»D_ζ‚iΛjwαž]Χ3Wί{PB†dk̎ρΝσζŽ’Χ ΅Kn}χA~š{!?tά(<7ΆΊkΫQLŽ›R,/βπƒΈΎTωφ eΊΦŠ”>2~’&Έ£PωEƒχΝΌA‘²YG2iAhό"¨JͺΪ§IΗήaΊΔΏβ±”’2Ή&' Πe^ι£N‘ +"<Šγ,η[ΘΡωμ7l*·#)ΓJ>4Ζ†η™ nΠ’Zvω˝{Δ/C€Τ(σΠ» 3‹uD²­;―”Ϋ\ρρΊ.γ&νΜdI@Ϋ5IFσ›„–(­-%7­:€›²(δνρζε‘€˜T:L²dωQQ =ΓΖFτ~Γ`6N1%€ έNΆ₯…ΒϋΒFcΐΦsŞφιkn:#ϊ«•ύ‚"Ω²Δ5g L+‡(έ@₯?Nx΄*^ΥPyx™pŠPςarJͺ}™—§5k`”’Θ ›Αš@3¨YQ©Ϋ'p$Ρη aƒQΰQcεΛ·Ρ,›C'iq-έ`Ψ·m‰Ššΐ pσΠ1ηfφΛ›ΧFσbTϊπ>T˜:¨²ΩΞ– dψO†^?AΛ"fύΨ›ίMc—M‰Ώ]ΌΜaΏΔi&ΊΥΝ€Ax5| pΕH‘ͺ Swb¬›υoΧγيΣΥ> ܏{Θ葨”‡ΥDgE4¬=’lΊ0„Τθ4$v]εAc?=Λj ύD§–ΙoœOg8h›Ύ–‘θ₯w7”Τ‰s0Ξδpοkgya|Rα Ϋ.m};‡Λ…ΪdΧ¦ΎP„ΐ"εZ`ΗrTr5Ξ•1™€ρΤβ3jΠw° τ1Γ#qΫ">σ 4w3Ω,w μίξt, ‡.½ͺ‡<>ˆΣ—=Θ!ƒΩΜΉ„¨ Ύ€ϊυm{q άΊο»iής‘c±Κs‹amϋ’%ΕΌϊ N2Ώ„X) hm”Φε‘η.ή +›K"±?¨C“³`6±ώš ŒλΕ›ωŸ΅Ό‘Ί†Λΰ_:vЊ ΄H9* ΦDδκΝ6ΚπΖ,[9Mέ8ήδύvαΔ90Ÿ“HqΊϋFα© ϊNΚιΜεΏΊhΡ”ΌΖ)ψΙ¬ζωK“:ύΜ3ˆ_π+³οVͺIΔ}΅KŠeΤ*κ_ΫθW/`π,1Γ ™«1·δS_ν$Η5Œ:;/‘βu󀐃ΈθgFMδz6eάΏh±(HσDΥυBd―Β3ŽœA=χ1‹¦ψ4€¬s‘ΈΒˆUbV0Ί5εΣ?κα۞ƒΌ•‡Γ]wq/ƒ3AΊc“!θ'e>NΥπΐ@i˜*ͺμΌnkYvY.Έ?Σ}Ο—ΓυΚμv ±Ÿ½ςΩ£ΐ5:irνxRλM$Ζ}Ϊ3ΝlΡΆU@Tݐ’FσJF„Žu―Nϋe[β +2’]&_‰)Kš8d-Ζ…_Vo'Ιt Η”B—^°χ½«šΟ8΄— mOMύρŒ@IΣυ Cfo,·X†" %.ιu·θθ•nCΊΐEΛy‹I@άΚvξš@)vΌ BAΤ₯r9“-o•Ο-p€V"}Q +uw}­`₯A‡ΰιm|9οΞΣiΖa—K^œm₯©j %ύ†L^5ψωŒΈŠ4œgi2’‰Δp’ΫβόβΣο;—^n¬=žXfν.’«*ŒΞ·«$κ6υ’œ0χ¬λJΣϊ#"‚D9Ύ₯γ-)ByλΔg¨Y20vν΅> μ@ξέΞ‘eΛ£tΨš|΅ž9~ mJ\θύS#H]Πbθ=;+ψ, ΟbΉψ -Κ$ΰΗ’ic绊;Pν#IΣ)€+ÈφΡ‘o”Β–‹¦&’ΐz4ΞNfΝE}‰‹d8Ž˜ nΞ“ΫΩXφιO‚7οΝfε„³φͺͺ"†”΅)¨,Χ3»««)X²nΜΞJ…¨β€ gβφj‰3JgIπμ–qπjmξGό-±(OŠΛN˜)·+:θQ(b}7΅)αyKΔΈn¨j?+½#^5ΎΑלkmυ4‡ΤΕέFNb‘•"α\ΥJ¬– ·qφSXΚHΓΑ…C(O:1[fG‘eyό.kzςΒxΨvΊ+Ί;_khβ(yΘ`$Σ%;+¨»BΪd+Bαoj—Τε Σώž» @Ύ―"τ’mσΕχ’*-P§ΊΥθυΊΞlV©x+:S„‘kˆaFΜ(Q6§8ŸZιδ{Ι¨ϋI½9ΈΟjDτνRll½ά4³Fπ KšΡw δkŠM1+>κŽ<ŒΛ΅ Jθ@ΩΙ*ωΕ|σΞ ε»Π[‘e—Άs%7ζt‰_œ½Uήڎ|ΜH₯©@{Ο±QΫpŠ’ –ε₯ZχKWTΔBΐ%μΜέ]ΏΥ M]ΕFήκ5ω>Α‘ˆλjg°ηβΘΊΰάάΑE,³)‘β&ΥΛν΄Al?ΔWφΣR½Lη۟VΫϊΑIγ»Tm[*OI@p*πœOΨ·’πηKΗiκχ%αW"ΟݘLλέΠ,r +=$ΌŒπ§ˆΨ5ΨvʈίϊΡş“)ΒΫğUœρC2Oiq]΄Ζ]‰e.Lβφ°ό\·›6r +ε―‘°ΰϋ»WΟ"PγrsΥjΰυ­ΰ…5ψ^E x%;`vˆρ &xcpR`ί ν8η1ξ™’Οt¬ΚI†Ϋ‘SˆC’ΡƒηXΫJϊ<‘wιΜ=dJZ‡νlr"Ϊ*ΈOF©ΰk»lΔΊjΨ£;α + +d’εŒΒη­v­₯cΐί– ¬`=Klΐ#γΝ7#«!tvΫ:΄HΩC†?ž°g:yAα§&γŠ+0θTNjΡ`€χˆΨOΝΟyy‡ ΰ–V-αή’ —Ί4zόz{j&₯ωζ<δNGιmC`6a{…:64xΏ…Ϊk&=<†­ρI=‚Ή’ε ύ™/Έ“’ $―<*gLΗάΎΙ#χΙGΡ/τ³2›R£[P^'b[›>:8fΦί•ΤΪ,W9Z’Όΰαϋ Ψ•ž} ήFΆ«Σ©…Tώ­ΒQgš^Ή5(.•ωϊcGι‰οΔK υε£ΝτΊa½…σγBώNθΚ‡±LΓ>Όi…aI\ +έG–6„ΐiCψ…BΨΥΝ―cΊ6ω­πr"›σΝ‰YiΚ걇܆½_Θ‘L^ό{;šΆΖ©“Γ’΄9Ϊ·_΄‹j2λ4¨‚Φ‘˜ysX’ΓπτrŠπ¬―›1f Σγrg&:ϊφƒΥΈΪ―Ϋz¨ό¬»qSΈ:ΪΦ¬„PβΓΩΓ”~k…Ύ8§Ώ˜΄™ƒ ΥΚŏ½Ά=gdύZΜX,α0CZυ‹H«ΕΉ-nœύn4b„ΧζδκnΣ5\μθ߁FυηΠήΏrπζ~°vDθ€vρhΎ· M΄ε»Δ_ˆ280(ύ䎴Υ^ΣΪΝtΩ™|€ͺΠwHC•ΐπŒi―Α!‹Ψ:jftš2Ž›Γ'«) ­ύ8δΓΣl7AžΟE1γ–βθ}ƒώωΣΤC'…€]ˆ¨-Kτ0„eζ™]rI!ί(έ)ρΘ!έΖ»|½N{Pλ^<αωΥ­ό;θχΘ7D€r_FŒϊύ8’τŽUJύΐΕ²•T)z ιxUοΟzGEύ¦=£ZσgsΣΔY„”­*ŠtΗ αΘ"ȝΥf±€ΌRΤ€‡§κ}£¦‹τp~œΛK»ΣύU}Ν5 βΰ’$‚KJ5ΒΉΑ΄_°dυwΈ +ή*­[γΙΆSͺ75Μμ;žg*e±Ζf\υΣe?~£Ρ Ώ‘ΖΒΪϊ%ŠήoήSτθιπvˆ·…hΚlG.:θFΊΞΑzƒ>$Sχ‰‘!'ΜeMοgUΑkͺ +ffΌS|‘·±m [AοtTƒΪ_ΡD£¬’ήδ‘v4A΄*΄ξΈ•n2ςΌΣLΖΑϋ0|…xί'qWίj©Š“yαΤ•4~φ‹λEέyΏI_βaΊ7œI‹ΥiδVWx·ήΛ’A§‰‘Η'―ζΜ9+Ιν†κίβjRχJ)ΪΝόz oδτ½Šί±΄" ίζγλ€4ތ¦Χ핬 Q@–“uˆˆ”…ίΨχ§7‰ς°-¦φ°ΊΆ€’{©λΠ3j©εω’έ‘yΞΝ’Ω)ζΆΑϟΟii§~ΞάΞg!}λ™,”Ρ@½[­Ω +ΜΡB ~ O¬ξ „Y³ Φrv‹½―—€½ΦjAα½=™LŠ^Ÿμ’ΖYŽ€ ›—›+,’Ι>>€Χj•§Σ™¨y§*sΌ’‹%\³’.Ϋސ߾ˊ‰Δ'¬glt‘~‰[δqΑηΒM$Έ{žοUΓ$ΚέȜt>½»V~žήϊ :S<Λ βz2Y9)u'“¬d[h|υχ˜xm?Ιj’4QYΆˆ)`ƒ¦δΉ(›«Σ6 :³‡<Ά#Dn}ςrσ¨ψsTώΫ;ƒ Ϊ€v/Pή°>Ÿ•έmhi9aS}Ξm5€’ —₯'H&f:?ςΥΉgθΡ™υ™ΨOΪ£κΌφΐΣ>Τy xΒZZe³ ηγ 1“|ήζγi”Ό­”YSΏiγΐXTƒΌε3ΨΏb5lKΑ€ςξ1ΰΓUψYt!V{ΊΪ£ϊ‘ŠΞ¬ή$y5Ž©Z9.’§γόQν’½ΌŒΑ§XͺΐF°?(Œ[”dθήHpΡy΄έ©Ξΐ»™τ(™"ϋ eΕς-¨ έ¨P3ΆΧˆ,‘°3βγ}©Κa,ΰΎFsΙuw·λζϋόΨλ m^}c‘\¦ z`ŒρtΏ³iΟM„.ή^r4Ίͺύ˜¬Ί‘ΔΞ€!Ωέ%Yzκ›|ۜw_ u΅-…ΰpε6··lΚ‘,ύ] τڝ.G Ήίμ;Aw‘ΧΖAγbα©NI9 ΆT#1λ±:0T*mθ)πeEΘUfu1¦‘J³‘α|¨ <σœœ?Α7فΥyꊿ›‰δ5ΝγeBxՍmMΈΊΔΰ»κ/1lΏG€ϊ–°ίΗWέ·δ…phziA–6d1@š\ΰq€9―Π釀fŽPŸ@\rό!ύCΗWyκ΅φˆ€ +e”}+Χnό;d¨LT|LMΖIč!ηΣ/ΫΙFj/AΕξ‡κ₯‘ δrˆ­ύ8ϊ£γ3pp―ά½X=γ[9V?ο§ΥτθHzΪΙΉ‘ρβΥΙglMFν7(GΖΗΚ’‚Ž4KcΊ{$αc3³Πγεψ‚°4ΜfEƒΌιθ%δš“ήH³tY'εΡKίq]–aXͺvUpZš£σ~mw#γLγŽθίΛϋrς{ΝJ糆{ΘΙξΥ)@C܏Τ.Οζšbd“όζ%[{hιόC<#lgFαhχ5‘γκΡoλCΞ‘έΆŸτuγΟμξ‡jήR*³¬ρ©έ5…QοΧάε6ζ ›‚ΏX:‡βΌ>ͺPΦ―ά·ΓΑΟa'7‘Σ0ό|Z`O <·(Γε}ςuίnaϋνωcΊ9£}ςΙ›#«=jπϊA)WΫyγΩ.ϋgdμΉH +—“~0Ωβ>žzΦ{Η^<«šΓ«Ηϋύ φ­8τbΡ΅)‘h{RΌ%X8΅U NpŸ~ΕτŠΰ3–αdΉ*ψ¦±71 rαΌ¦Δ γγ[jsaΊ£ΪG>»e‘bρPε κ5+»τηοόσ΄²όΦμ/FM݈_Œλ>a<;7‘ωΈθ$Υιy³+•ΎΩU†ϋΑ›O•otxΰ„=Ό +t²iYd‘uΫ².SγθΈJΕ‹3φTo€ 'ν‹οV-mοŠFΓΕ„φζ-jœ«BΞ( λ»v…‘­’ΰςΦ™t@7€Ζ*RΒ²,yψŸŸ ššJn{Έ)φ`.hΏΈn ς|~­šώnΠ(yφXψ€ZH&άβ!Ξ?W΅ ,]’φvίρšŒΊ:[VSΝ'–.Ά±Α‡„E7œζnQάG/?R₯Žϊί0:JkΩ5θ³τ†C–ΐΏωJŒ›ƒ­Δ€ρ¨ΤΒζy+^X{8V/§ο»pρ€©H\a6―bKΜjε +£UΑŒϊυ Ε’ŸjsFΘ2™δ ’­Υ1“ψδ‹šKU{‘YKsΊ(ΨΣ―Γι&Kυ:738· +ιBΐ’ΫΎΕt|¦± ,+³MΞd):ΒΕ ιξ|xΓ¬ΈF&*_ΟύΙ1Β*—μβ@=ΫBO£­άΥΙ(~Ό„\©Ožω³uΐί¨άο΄YκAY£±G6Φ#ƒL#σ9,Uo8Ή=Fƒ½ύyΥ«Rh |‡/ρνd±«μ' *΄Ρτ°a½°Λ‚$Π!P&υ£—Ν§EWΓ|›*AŠNž¦χ:ϊ$΄μBͺν"EL™@όδgΝC<tΚDγO/QΞν  $lάΠxΣΔ(Dξ²Ω­I…X:MΛhš·Eήκ±όδΫαWKrO&―τΒ`L;΅’S ¨}Ζ^]œ‡^Σp/–αJQ?—΅ΓhΡ°ž(ŒzςT5Ώ΄ά©)ΑΩmΔ?]5Θ£bh fΊ#oΞ!m¦]³ΐB†xΫ:•tδ2Ÿό΄ί’ξΤX”wFFc›O% ]TιFdζ·"ͺΈή_·‘p +-™„ `Ί˜5Ή’έ>J™ήiβ±ήXιmͺΥE”WΘnψΰŸg ―tnΜ‚‘‰t₯`N" ,™•β9Ϋ›ιύTNτl ¨zς{ƒw>%ΆΊΪ!\ϊw"O΄LΥΏ3DVΤ‡λb½&£_ŸφZ]J’hUv­8<»&©0)η‰Vƒ „ρθAΤ°ίΛΒkY9kΙ†™:|Vv ―O6Π‘ηm;“ϊ2CšU mΥτg7ΧL]η?Κ²©-§Φαœ_žΨγ +š…z %σγρρΒχV…„7]¦[2θfŽ$―y¨Š~H=MΪ&θEφ€Μ‚|ςΈf_ ΌΖ1Β€N&’Ue΅! ǁf½,vuΉJ€Aξ7Ό½Ϊ“Z]ΙΓΡfΔh­^–”ldZ:ο%ŸΗ5Œ%A ά¦_~Υόε:ΓΨΘuA₯B^Β+AύI»v φP\„ή”»ψ›C΅lΤf†€§Ϋόσ˜¨J΄/œΞd¦Ύ2e +kͺΏ +Y .YόFв"ή‡ΖEƒ'1Xϋ;ΠPδ!,σχ“=’ȘΚύIc!ΑOη9w€r³'D +lyπ!₯γυ~?ˆΜ©R΄/ά²ΪE΅χ%©άΤτ‚[»*0ž”Άmε9ίH„ώΣΰcΘ­·ΠΈ…ΣS9"Β'˜ μϊ·ξΠ2ug벐 ύτξsΘŒϋ§Θ‘¨)}”7TΗτγ1|/Ι–-+’‰BÈwͺ`*΄΄)m_ν_ΠΎŠehjM:ωj£τadwΣαω}½–%8²δ}釋ék&Ρ¦³—³Bω6.RΎΧ™[λŠίKGθΰO―ψμg0τ’†–š<ψΛψ!¬κ υJ•~Fκφm¬Ϋ8_.ΰΡρRΗγ›„ qΧUξε`»Ό:>ΓΡ‹?:q5ρ7στΦάΗt+W₯“ώ}¦ϊκ–F¬ί­\οr—6³—‰Ψ₯EΌρFα$B³d•ϊ^,Ο' ΅Λ^Ώr<ΣN]fYWRK«x•5η@ΖtŽ‘F"§gL8σγ™ΜϋΓή­%Pξς>ε-Zd§TŠψ¬Qe„ηlPζwΈυTλŒμ½xψωέkX92 2*vκψχΡ +„r \₯G&ιμ¦― `³T*πΨ%6’λژl"£₯¦?ΔΪΆ˜€>¦λΜτΘU-ώ[h&– .’œ“MU= π£n­28D[ά²YJβ„brΎ‡<=1ί α5φ.0$ΗOΒv4βυ½­) +–bΟcŽφειzŽκœ₯ˆH$ΧΎNsssΑ”ΘχΙ¨_λ4ΛAΗuyРκ +l£‰\₯ͺΖXv†ο•5°G”$,ΦΉ1±/Ρ£όVŒ"©…pά,ζαίΙ/εhά^ f{ŽΠΙ'Β VFl›+p +]T―ΧΎAAN°ΊλΖg–wETNΤυ{ΖA?wζ5Β TŽiž+ύΈΠg™Η΄ο ΅ϊΞ7Nπ\™χΏeqπΡŽ?R@ΘΉη}κzY0³uύ\ZG7§NΫ8 ,­§Ζ™ΜfAκFκ™ώ€?3ΠiΆ0¨-’έI OŠζ¨ι6CΑuώ―κ@&8²ƒ#κΌ_M„hqηώTmΖE-U±2σ.ΪΣ ςβ2qnQA+{c0Νe'xYη~©κ=ΕΖJ¦mfZΞ1;Ÿ%šΛ3ιwΒpoY[sΤL=aeΗ³cϊB  ¨Τ,ΝώQA£R„HΊΉβBͺ` ΨΛ7ΤΈ‡žΒŽŸ PσξΡτnί½ζ„-gυ;Mίnwg?€Ay,–`^ΩaŒYώ ΘΞΞ9€›Tސ#jg8©.B^z—½7₯ϋ‰Y}ς[Ώ>ένΣڌ„]δAxΩ y~_Υa¦…3π₯°μݟ9=2>“³Ο]ξ€nrs<I)[ΐ|υœ*›‹°‘JmλfcaWlαΫfΓξG]D°ΝxΘcΗ]Ψτ‘ΐ/VςΠ Lλ"°ξrΥ»ϋ»,‰ ΛΨS•ρAxΰfͺ’ΟΡHΨF]— =–SuΛ‘U֎‚•ΒΌ)Ά‰ξύ”ƒη΅“u΅Όm( Θ)™šι˜1(ž†ŽΝ°λfΑTΏcf„>4ˆ:UΩ΅FˆΈετŒξΛ“^ ο{y:\χd»αžtŽζΧά•Ί*g‹Ž“£7-"s‚9AD‘Tž΅I±ΖKφε2’¬Nρž{FΞο1κ;zρ77YQό›¬2W0oϋ Ίƒ,M›μ€— Ί)Ασώύ Σ3φδ{…ŒΉ©LηΒλ½V8Πi£ n¨‘Ϊν'έΈώd2ύ„Ε„‘BΑš¨Ί5y·'FζΝ₯Η•Ώψηβά1χM”‰΄ά€ϊ4ύιρ€¨=UΉMͺ•MΘ‚κν΅Άs¨U²FΔζt–xΊΨ•ΩG +ωχc\¨4ΗΏ`#Αš¬ΰAV§=nΆmP+₯‘ͺΚi›δΦΞΥ @*Π&ά¬! σψΈθ;VΒ>ΐβ+ηΊώl +‘f±-;‚"X}Em΄#iv=%£t’bDά8J΄—αήΩP½lλ9ΥΉ0$XR1ΣΘ~²ή]2;ΞeBrΚVηγSe‹‰œσ νΕ93#FPfo~λϋNξφιξl*B`ΏSηΡ}§ΨgλWœΜ…}ž–?σ¨©»ξœ†αάΊ;ΖjF\‘IO„Ι±―£cM Α“ΑRΒΌšgGλχϋŠη—λcͺβoΖΏε*R~Ϋϋμ1Ÿ+Δ±Ν¬²©θ‚oΦ‹Θl·Λr•h [’Š SO];iͺd޳ΗγΕ·~­!=Ό€ML­ΕYeώ½}Ϊ:dwΓ«s–yΧTSdcΏ‹(Μ‚RιΫίM‡LŽWŒρΚBθ[­ZΜ¬χ9Ήtžδ°»=Ÿ₯œ€ŽΑο"qτυξ`ΏP§ΕDVrΕD M K:Φ22(HZωrUa—U\λ€.Bχ½½P.LΉͺδ,κz ν‰SΘ[Τ’ ΰN°Yι–ώπξyžIu}¦όξ@²f%¬_¬3RܐΎbαΘυ2ω"gήr&,#?}%χΦ'‹"ηK¨΄«NωF_oPtw@ΊϋέUf‡ς'θΐAΚυjwΕ̜’ˆ–r–‡Ϋ•WΧhώΝ‘Ά3 ΈY·Α΄~]›uJε$ΙQϊυΏN§Ώm_Ο59ι/c€Β’ar{BPτΥέ΄Šμ&Šyιυ=Ζ ¬ήΞοF‚„ƒΝPσθΑΫφP|ͺυΨ•BΏo“묙€Ϋιx?38!]κχϋE½Pϊq1KΊi@½ό­Ϋ±χ4Ž-›,΅GΙcmŁς,ο„Λu²ΕνΗΫ7Š4δ"NΦΝ?,ϊ£F{—·3Α +™ω #I ΰ»Aˆ+X•g+M7σΫeπΈq³VΎƒ>ŽžΟΎz―ΟYžx捰ψ[1ΌΎή‘ΉͺˆΠΪζϋR6£Λ£Ή1 ΟmZ‹ΗJ‘"Ί–t#Kτ ΤŘ'^vB΄\€ZΣƒΥNR*žΕ η1jρ7όΈΡ?ύήΜj΅ΦV O:Δ±όq—ι—0υΨG=¬Λψ~fηnpΆξnΉ\Ίs1‡ΞŒžν Ω.hφ³š0xφ ±XG΄J‘εh―±Π@&Μ„›©—΅έfΓΊO„FΧ$O%ΎŽ£Q'¦»’π”[²4M˜]–ΛVÚΝV! +–όΣQ.ΉCaλΔpQ½ŽZD=~m­Σaq‰οP£ιU¦aΏπI§ϊ»‘ΙSEΈ΅;=‘N9gΒAΔ"ψŽ—‘ρΩΆfF‘fβε3ρΐPΗUR©Ν½υ¬OέΉΚ`Γ~ςB+—W·«LVƒ}XL7ΐζVlƒ7.Ώ`ΕΟvΣl„}fVή‘μ{Ο ΎΏΥ›_Ν―/¨Wμ0ΒΝΉΪό Δ&αX_Δ­<υH4Ό‘‘ΘΠxη[>πδδΑŽm€.₯‚B5ΐ†7έΛΒvΟΜ½@΅h»μχ³Wβ©βSΐη’ιΣ―τ'rί +LΩͺΒ’L†a.>k΅ΉΜθJ*SL%ΤEΠXΙΥιF5Χ!’=$@2WΪ£σι²\fšo‘δ˜γm^Τεrν—Κw€μNOb;0I•^mΆ–>w}i­ ꣞”R«·’RΌΌJM›ΖΔ_όν±²΅zhφcV·{ΐWiΖ.PΓ±τ~#_)²±'*Ζ- o&0{¬ώO‚ž«4gγk˜;발ΟΟs―[κc!Τό±–ZΫvPς³1η lόώτ2Σ¨΅κΝΔ‰™QυΫXγωj+5ΈWfΪσAbθΗ7€žΎψβJx\… Δ[dσfΊŽ€ #cxA% PO•Αs„ά‰ρSŒͺEΌ;•n!i€φ‡m‹\Ό’&Ξ€E’Ϋ NŠς,oύj욷4¨nλ §£Ž¦80&TΧ +υͺΟ’`DTrJSιΰLΥύέ>IΑ‡¨™πQ1ίπ„.ΜU3gX[:‚g±η“*Nƒς‘] *6PΠ»ώθΊ5―Έ†?H9ΈΦηΨ7ΙβpfΓ„_5FŒ``υ8£Gξ˜&&Yυš/ωΫγ-ΣΑ)ω&Bq‘vΤ›Κ/–άιaΙ― ΰδ Σ•8ŸL]90Υa›"φ3°?αζόΔ]gδΗ€δo;£c7bΘΆΉ΅šBOŸΩΔMWP'ThžK ΜUrήΝά loˆ:#έdΓ9 ՟φΜoθ‚²΅4HbΎv€ρ’ΜΠfƒθe%Fή©¬™]Α dbL‰  ŒR`+θΏ7B<ΆΣͺ€sΩmžs!„}N%°λ·πύ=©:0k…ΒϊzΈ°Ν&ΌΠ³πΠ|g(ηξ3 –ΆMγ +zτϊδζhy8άΚvΦΘ’rI}αψ(³ @³IŒγ|Rb],φ)ΆC½QzΉ§±g]~ ΏΊ,=XMlIλ<±M‘Ϊ‹0.Fχ&ŒφΩ›ϋXDΛ_NVϊs©(ϊφD‚1κ`~ΏΚζΖ^Ύ}ΉY ŠQΘφΗ@`GσρY’vΌͺGI„OΣ δΊΤςMΧX͏εφcaŽΉΩΚq^θkX˜Ψir―ΛL™uΈΡ½iΖ δˆ©Κ$ ΔωHKˆ7vβή²Ρζ·ΎόF@ˆ%­€AιΛŠ‘J~χή49R£lΤ%;S MΞ_N…„-θegVλε^v ’$q‚ΣYΓ€Ηω4Ή‘5›0’m6Υ¨—TNΉf9$U±°)œƒΏœσžB!Ϊ·5HbΒ–*Ν€T4)ρΆIΘ[#x8JΔ”E;2V Pω鳞0mƟσΔ”G a%$K½ΎΥΡ³uτΓ°³9LΝ>Mpwψ-Lρͺ>ξ’Oψέ€GΦς§žΎΧaέb]†W8q‰”ΕΠxV.Ά—`½ DςHZsy£ΕŠƒ`W„$ ¨œυ§όX‘­v&‘P|p³‚‰JVΜ°μΘΖpήνΔ0ξπ!%J‰±τ(υFN† ΖωNπU_4€W|έt2lδΐγrΉr_Tγσπkǘb.Ϊ +£ν΄1Rl—]DΘωΆ; |ˆ«‡₯‘'mΜ›*δ“ι=ΎGΦ0ΔA3χ™Œ\W-u©Σ³ŠΡXSTΛ6Kύ£2ΔΥmeγ²WŸXh&jEκП…σXXϊζ§%b|Λ?Β+ZβΠν’-Ξ›f/έg ŎJAI ΔΨ;‹«£6[ΗZ0_9'Ψ€ΫVβŽ7Έ‘+ΜrH•ΓtDΓ…»Μƒhχς †ΊFρόa5sγΌΒ…φεΩ₯%Gξ9·όPYEœ²Œςέ*¬š^τj·¨ΝμϋĊ?Σ½Ψθ­†ΈUοtΰaΛF»‚Λ)Εηυ@ι|ςkPXςFΎ‚-‘=²θ¦h±ό &*R#4Y΄LΙ0’aδΕ%\{/lΡΤ;‘e/b„ήtOζœol«ƒΨžnS₯JϊhΓυ¦S Œ—! } ·‹†ϋn7)›eΚeή ‚šΦ‹1Δ,…y$~ /6q…ƒώPƒΌ¨ΡΛΕW ιvΙδγm\Mδo=7°ͺ“A ΕΛT2άMM;–3•ξ$Kίγ őǍͺρiXAKίt/ZΐFP‡Ώ£I³‚Τάb6Οm{ڏ―σOZγ__ώ- ύ]ΩζγP‰OFH5±Šό5’ΰ@y~=ΪΜ4#6¬“Έόβξ±|zI_eOVΕΏλ²Αy%δΔύvλτάΖΨτ½ΊmgWΡ7€  ‘‰Ž ΐŒ6π‹,ˆfν’}³[η’ΞβϋβŽzKM X΄+ΓΓ:~β7Hδ>*,βŒέΠχN:˜―~‹α7(t5ΨJ~vI©g”fC@Œ^ΠΪξγ0>Σ/”ߍ)–Β γΩ ρp(―w’nš,iρS#© σΤΗ/IΙώRRz9E ξ€λ!HΝ{OϋN“D›ωΝν&N–6œS₯?Β΄§β²Ν ΊΕΉŽμ’g7ζΎ –€MΥΩ£σW¦σ–!ζˆ,ΩΞζώ—c.ΰζ΅+V₯¦ĜΟΪ`Η…(} zπY‘qE΅FD’BœΞχΫ|`ν™φ½–™NŽ[žŽϋζΣΆμ„ ”jό:š™xνβƘ΅ρp,μM»7πμΒπͺΟwα_ŒΚΛ¦3wνl"단ΐŒ«*S f₯ίΚ­O‹F¦5Ν©p+™fΒ“ ύ‘Ho9ιžρΥΰsͺ1PσΨΰ'γ’ύφg€+7λijdRΞ'Η‚5Πe)(„έ&”€?ΛT‘τ]?Yθρl«Ζ©ΧL/œσέͺ>}YžΫ­±"ΘΨ£V°Œ#­[υτΧF›ώ%ΦE(ώμ«™x,6mX|gœQϋ>Π₯Εμ½Ο’υζ}e\J[Ι}Dͺ`·EΥς ΘΚ]„D¦A–^"DΔΦOxj˜‘¬φdρ“C½˜HεEœŽk*8Nu‘9M,ˆ2{šA•ƒλ&‚χac—ίͺl]$#"tφ0αkΦExLQ,4<§BBοIXΝ|Ά„‚A«‡`»qΣ†QϋHΖχ,ϊU₯…vβhJίΤ/ςΝ½ΊΧ>’5”‚2φ)b kιΝή{³ͺΨ°― Ι ˆ%9LU•«΅…‡Ttuƒ7/K]H°vF˜9IbΒΐΨ·E6GΟL2„C‘»Η’ιw΄€²ΖΐχnαΣ»_9Χ–ε)QΊ₯VM +t„νΚφ―ο­#ι០Q›ά,d\Ma`_‘3ΔάρVbnՍ|εΕ{θ+½»*[xΆφ¦ιL=Pqζ }ΛΒE·I,Κα²BΠ?GŸ”Xξ‘HΖ΅ηͺ5ͺΔ[pγΥζγς,¬­ωΓ7ϋ?Ι7φ£•S4Νθ˜μVςMϊ­•”;ΗjϊΓχ»*ΝΧ>3¬¬›οr?ݚl#:[²uιeΗYθP<ή;WBs’κ~Ί6Yϋ[ΰ¬ΔΧδϊ7r‡θ~½β3΄Ή~θš>’6ι«ΓΧp‚Λ£lξ²1ygtm[ξ±7³"šδΪθ…$œQ(;;ψΗwΒ•‚Ήύ΅‘fžψ‹₯z[K $e#ƒ«Όarώφ•,ΧΥu’ψΘKώ+ξ>Πδ0ΖN˜Ν'I;@)»Fώ̘g}ž4GθnΫΒ–κΝκ.ω…tK ΆΌ^,μΦ‹™}Ο<‘Ω†CοΪάNοq;*Ϋ―-u°…nμπoάώη\NPBι—έΆ³Χ㐖W7νΝ€΄#ήόJ›Œ‹ πσ²Ρ5ΓΆƒΞΗυ>"ί‘φ*ΓΥ₯ΣXŠΒέΖκB.#΄‘œ“΅b|rkŒ‹†h›r9Μ£φ¨2†Φs'ΝH{—5>κΊ™G—\Μ!VŒ5‘°χ7<”3]?›œG8YχŒ"δ7ώT)E·gŠhΒ€jφ‚S„΅42{ΓuŠΩ νž)}iŒΚ~o«VZ’˜Q##€’[§m(Kφ²ΐ/qzυ •ڍΛΧS]Υ4ΚcΟ6‚άΣ‘_κϋ Ξs±ŽΊΪΚ_f:EΉdQήNlK,PΝS„ΰ,Βaι³€ˆ=΅ ή›*hQHOp—V―—λƒEλ€QŸΤ—71A †.ng茳Y΅G›υipH?vo‰“½wΆ)aΥKΔΞr-μ~¨cσU° O€¨–Ρ‚ Ρτ˜έԜ!πŽ|oŽΆγΟΝf[εVδτAιΐο‡τTˆL…^Wλ S`&βΎ&½1·YΘΒΦυˆrΥ­'¨3•‹Έž’™'ΝΞ§azΆŠ_’Ь Vš?δG€Ÿd΅­—ήΧςΓ‘fώfΦ%θ**ΰ„ώ ‘€Aš΅X½™iWw–xtI?DΡ%mcίλC–;¨€‚Χ·βN€°MF$laΖε'R˜—΅&vŽ`BοδZœΟXŠΝc'yUΔC~Ε³8›ρmήχ2ξ7 ²ZQlAW,Έά°ζΫ$fƜi*Iό‘η»ΰ;*λ κ0 )=όςH{ §D3G―Όλa΄1ΰ€I³©¬DB‰λΘ‚*―ηΐΘ +‚κΗ0­GΆJ,r={RP :κϊˆvI#¬DΘ-TŸFuL\㠊Kέ·3Κ^ν΅››ΎaD˚wCΦWtQxϋSy‡κγπRρ`>›³;Ά>3οƒBd˜›˜τΌπ§ΥΠ? 48ώ΅ΘGυτ#qŠ@η“l^7iοΕAaBLΎy–E]μUK£5$Π΅ΖΉ±}>γΩA@Ί/ΌΚRΈήλΟτϋD…rΫιΔψΖΩbΡTS¬˜yaZΛ LΜ₯8ΰΰ&o5ηLρΕήΨΥςVΛP»γWΠ’τ]Ο„/Oε”ς)Ό¬•.Rlˆ0φ2sˆ¦δ%#}“:[όΝθa΅΅αΟ·3Π₯t’;¦–ΟZd*aΣ”tΟΙfpϊ½QΞY(=‘…šjΌΆZ=Žeϊψ<”j‡ϋ—+ϊ|“~q“γΆa²~‹qυφ7ίq\υΰφ§k­VhΧ`ΛG¨yNo|β#Δ:’mŠAϋ B]65²3ΠAζ„Νy,ˆ{ΞΗ†Φu3z[Ν‰TΌΎ=.Ÿ,<ΐc΅v Η +ͺŒΦH>vϊƒΘkΗ<―·κγηυΊ“ΠŸΩlZ²5αzΞzhHS +†^rM?½h+ϋ"ΈΜ)"Ϊ3'g“ΫA£Κ;+½σ3 +-%$x―›ΩΌ+Έ˜0ςb‰;Ϋ7Z{8…ΖxSJOO[;€\67ηΥΕ΄ηά„€ΡŽ€!ΘΦπ₯¦Υΰ™Eφ―Πjˆυ4§πnίk +;¬k#ZIόᑍΑνTυL–ΖηkΘ€΄©\^ΈΙΣmΥΜΊ~’Cο3>α˜*Τ{†ϋδd+Έι l Gi@AKΑΊ”Ή­YW¦-[?U2 ͺDο=ˆ=φ €}’UŸ«μΎe†πˆΘ†–2)l.‘ο'\’NLʍΪξͺΡΒ·b1ŠωE§? +r©˜Έ Φ³Cd»›Aά­°₯HkqΝm1NΘunÏ―ω½]€CAβS9#:2ˆΘκq~8θ7£ˆ›~;μήjo-κw€“;ΛdvΫυ„FQώuΛ:γDٌHΪ;ϊ8|«lςCΰς™¦?kΑ€i-™…žΕtπTtωnq™nσ½7Όχ¦B«‰ϋ>Π ω’ΝR¬Φ%­Έ•J2%γΩ‘«V>jˆ²œ?¬}(²Υo$ +4§Yι3gΈ ΩΖpΰ”sFήΟ^D&ίV΅8εK•ζUžΒFq–ŒΧb8sžρ#ά‘f9»F/ιwr1HUΥ;ƒ!ZΗΉ―lr o3τXψΈ™cύΒI²e[!Aψ©e&ΒρΈΰ>,e`R‘ jηR|{‹{Ω]Ά Πρp 0%ώ’uh˜2Ρa³―ďFςUά·ε‡ +œςaY’ +PO1ώ.4Όύ?* ˆͺœ•—Τ—₯)όf) +aO]ΪU'΄gTƒ 6>Yόώ,z£†M—·€±Σ%œ/ Šξ•χή=‡hpKf₯³-ΥƎΦJ"[­–υ™ήΞ­ίνˆΖΰΩύΪ3$Ζ·{M˜ΘŽ&ΧήΘ™h4Ι!ˆ¦C‰@rθ_¦ρΝ–@‹Πί@IY‹Ω &)q½LFF_)ξ{πU)ξθM Γ\T‚ΐ}Bkυΰ0Ητ^Γ)m‘ ͺ!CZϋŠ1ϋ±τpέ λjE?ΰ|ˆc­¨τ +exςΏ?„•Z‹χγέ5&.•§όΙP©’αQ‹S”ΐf7­Έ"ΣUτπYpcοnΞBόω½ Π­*I^Λ…vkΣ_­)xx›m6b‘BƒϊΩ&»£z€uΗh_•[ιYF5όō>Ός' Ϊδw―¦―2‹1υ‚ΙνX^±\δ«”œ±!h²μLN±ψŒ/³Κ9 ½% +–s”) i)συ(5΄nGSΥπ£k#HPψ–_91ΨK]sjΟ–φ΄9ΡƒŸ2LδP¦ίΔΆ F-&ό)‚xX— ›ζZ[ΨZJηΊ(ŠQΡ&Ρ[pxzΎž7¦"Gt°Ζ¨ϊ¨‰G3 κιJiž[•-3;NΔ+ktaΦSΩΧ¦“ +IdΗwD°Δ­#έ‰ϊ‰ρΓΰ3ƒ?QύVƒ-.>Ÿ&<Ɏξ/0*6žΊλΌΌο˜nΛ]n‘ήά/Ύ%±¦JbΐΩΎ,6†~묋tΞ…EΌސω΅.ΦΝQηS?9 ‚|.ϊηΤ2’ζΝπI?Š_UΑ£«gά„ŽE/νώ”aΣψυTB?q +$0k,Ψο§c +‘5,;¦g•η€ŸισKΦΏ<›:q0GΦ‚ϊΛ-±©ΓΒ€QĎ`;‡ΦΟ¦Œ• ΄ΰ˜ώΗΓ±.ή#Σ{¦?Jrgί^-{|v8‘-Xͺ~¨Tεύωβ $[΄ΑEσφΕΡ,Ή›ˆKtuTθμJšiυΰπ! ‹lϋΛε•Έ–bΥ¬ €NζŽ%†*ζι¦%'θ Η«hΰ-žζ½γΕ« .μ₯†wέ€ υΰ›γ H„X ΕB±Π-£ϊρXΑδ¬—ΦΉί»λn?1μΖ€­/Ζ^dΔδπΕέ[:?4‚Ch©9Ερ*ΕΜ½γ°ΔΙkΌΤf,•Τ°ΐ/ƒ@UξqόjŸS%™ι&\©*ŽuύšΐwάVŸŸoρ1£ΕTοΝ¨B%Π4ZωξΓέέ~’˜&%6±Ζ#‘lΚ½ΜΜFD»$ΉΉζ¬S9>‘h΄΅εmyT• +*Β“ΧΪωh ‰ j(tΚ›H"ΨΫͺνeL)’ψŸ#ƒRJFΎ©YRPφγΛ`^`^ P» Y=ϋdNΟώ“«Ffηž{ϋ +‰΅ΆPšN2Λ·MΣ:‘Ekγb|-/Β_p#ΜΫwRY֝kΞM&kΫ4(Š­―_Ÿ±ΩΜ‘pΩ$ƒΩ'ΗJ‘Σ₯ΛΜV€@β\mΓo{Σ\`[Θ¨Λε&B˜¬y­eύΣ$ v²|’Vd½ƒLџ‰‰t ΏΖ_;r€’σίD‘?΅~•8τd˜r~ώYEγυŸέη“-M£ž(θΛΥ΄γJ8Š|’Ί$ΪJί¨{§“5U7#ψBY΅Η£ ˆήτ™7aؚWάK™Ÿo"―f4φΊk$ύΝζΣ.²λ‰Gϋί-L…£ΌkΑΡΌ˜bbΦξΊΐO{θΐυ:±qt0οΆWŽΗΤAd\ηmŠJω2D^˜ ·;zRžœTMMΐET:Ο»$°H±~ΗdΟ)_ Α˜I\L­κͺθ‹4ν΄£`ΦE`R•Ψ;Ά*b_θ‡ά ωϊrΫY’Νiπά4£R€2ͺ™aΡ–c<ύ7ΙY]ˆ’G ~΅ζ€Θε +jσ °F<έ©_όlU2ΐigTŽΈRR‹―ΖΟ<³ψj’[™έs N;‘^±ξ™†€#ΊŒ‘-θ[uύ‰RΥώ›‚EJ‘DνPmή°Ξ°ZϊΚjΦ—ϋ*Ν SΓ£NΆγ_ΨD=‡lcΛuw0‘Sp OhΗζΌ" b=ϋ\Ι••!œΎ(-ΕαΨg’q0ΤXr€°gž,{Β[ΧϋΝ)ζl|’=aγ(ŝ<-ιA‚‘XΈΈ‘v1ƒ>εv|mT< ƒ\$ΖΜ”‘ϋ3Z™&Πδα%ΰZ+“1ςΐ3gΒΑ C« ‹ρˆa›₯Fz–ˆ,(°ΟOη—$χ6ΰ2—Λ&Φ<›”ΔŠ³ƒm Œ{ΞΓ£ΧΘ$Ξ¬RΥ ‰ ¨ͺtωίnΞ°ν1E#θzUͺΎRψ}:\FFEογΟΰ’8έ£ΠqO˘Ό€^”₯0&ϋ¨ΕΆσf• £LΩ r{ £jΝ•‘2Gu»PΧήδΨšΣέξf…a,ΣΔΠ"ΌfΘΰ™ ’œX΅Κ΅\£6ζ‘Σ…†]·p’ͺTtμjŸ+›Β·Άέ Όƒ4Όι?λ §ΟΨP]…ϋQέυΉT *qφkΔBΫ•k#_o$p—/ŸKq­B ₯¨₯ZMŒ`Ωυ +”wrώrͺķ䔐ΘΣωΖ +ο’ϊŒ9ϊ²“-\cόM‰kΥά΄ΪqojKΩhΡ =ω‚ΘώzAΥΊe¦ ΕΎ|ΪυΉ<χ)‘a$ΐJ%v@Ι +'8gΐ\L™Ο.fρΥό&π>ά_ˆ˜c‘3Ιχ“ά#ξ—νΒK[˜“ξQ]/¦΄š΄Ά„ύξΟK’ΐ’€Ey›-*7΄Ί•W†hQΉη[w03xξωΤΦ±‡’˜ύ7ύŽΆ¨―]Ί("Zilς?(;Ž"Έ³”O @δϋΥmŘάf<ςΈ·Ψ‹Λ)Ο«›u"(\ΰ…ψΙφΈγeψσ–-”6E崍9x5V(s˜ούx‡t΅ςίI ό+Z²ΜΆ΅Z>0όžhNu#9¬ΨTΐΫ{Ύ Jψ€ν +™+p‰!<…2x‰˜κ;X£Š:ΒQΑD…;C)±‹0ΐε,­&0]!ΟwTI:΅,qP΅ΊGΠwί:Χω8§f•δnRή#}ΦΤ&ΓϊeΈ57ΨΊκπ/HqμΞ³UΩ=Χ$lε„ίZί•τ₯꒞ξŠΫΖϋλ-/QH¬ω:¦«…ϊ/Υ©\ŸuΑηͺρ“’<ƒ%ύqˆBώH@ΡΨMΑΫ†mΪκ‹ΗKpτšvœ#»ϊΑΑLΆ5bΘmζΐAK…Π€¨¦φΕγ›ΎoτΡό΅•ρ6€6‹JΠ%KΙι₯«τ%CS…^ω΅U+•Ÿͺ“¨QYQ6·ΈΘΚmΩ5dpunΓΨ@δ―ƒ€8UU8Π5a,OΏaΛɌP Ε +†έ―F'‡ˆ›>w‰ͺ.O’~Ywχ·PІ2ΠτP04ΰύΛ«ωgiw9Δa¦Q₯LͺF!4I”;„Ϊv>Ξ}€/ΙλΜ…0cΖF­Σp€°“:ϊΣ%_ΥΈPΕΔψ½“ž—ςζδhO^λν!τt–ΠHo²–?³"ί Ε\]›X{ο@6_Œv±ϋ5$JξΏtoQΰS£ΣŠβ^>C Eˆ©2Ε ‚t+‹ρϋδ©&½θκΗδ—ΌΜœΞ½Kζ5Q[b7Cž'pΘ0ΩΚS§.­_,#.zΤοΦ@bf›κ©žω•ͺβ7‰ΐR°ΦήΫφΫΧxτ–Ο6>‡R4Υ“,·Ίt²ϊf†.Bε/ΰίΰ–jB@ή )η‚QάΙMΨ έwΛΟUωϋSŸθο,ŒƒQύ@‘zjÊP1₯ΰξ£Ÿ~XdΟgy Θΐ0’ηŸ›MΫσ€WvB)_/Η- ΏN& +endstream +endobj +857 0 obj +<< /Filter /FlateDecode /Length1 1367 /Length2 27845 /Length3 0 /Length 28805 >> +stream +xڌΆt€ί³6Ll;ιΨΆmNlwl۞آmgbNlMls‚ ΎωαœσύΟ½kέ»z­ξwW=U»jΧσμ~Ι‰•Tθ…M퍁φv.τΜ L<Qyef+ ,9Ήͺ₯‹ π_3,Ή:ΠΙΩގηˆ:\ώΨČ\ώΰδνν26fV33'€…‰‰ϋΏ€φN<1#7KS€<@Ζήθ K.jοΰιdinαςg›zP™P˜ΉΉ9ιώ۝,MŒμςF.@Ϋ?;šΩTμM,.ž‘‚ŠΟΒΕŁ‡‘ΡέݝΑΘΦ™ΑήΙ\€šΰnιbP:ά€¦€Ώ(ΩιŒ– jaιό]ΕήΜΕέΘ ψc°±4Ϊ9‰p΅3:ώlP‘–(:νώΛύ ό{6fζNχoτ_‰,νώ621±·u0²σ΄΄3˜YΪŠr ..t#;ΣΏ€F6ΞφβάŒ,mŒŒώά !ό`τ§ΑΫs6q²tpqfpΆ΄ω«EΖΏό9eq;SQ{[[ ‹3μ_υ‰Y:Mώ»'γ?“΅Ά³w·σώwafigjφW¦Œjv–Ž@i±!L°c3ΊΨ™˜˜8ΉΩ@GΠΓΔ‚ρ―τͺžΐΏΜ™tΰλν`ο0ϋΣΠΧ ψηΦΫΩΘ pqrϊzߎ\Α23L-M\Ζ@sK;ΨΙώΗ 4ϋgύgψN–¦?άc0ύυωο'½?τ2΅·³ρόψίσe—• ύ§γφ‰ˆΨ{ΌιYΩτ,μΜn..';ΐχ?³(Yώ[Σ„JΫ™ΩΈ)φΟ)ύWΑnΟκ_mPώ3—‚ύTΓq]&v&“?_ΜΏ™ώwΘΑΚςΕρ]„«ΝίnͺΏύ·‘­₯ηΏ€?œuuωΓyϋ?*°ϋίP ΰ?š•šZΊΪώo―΄‹ΡΫ™Ϋόχ1Z:KXzM•,]L,ώ!Λ?v΅ΏDfciT²wΆόλVΠ331ύ/ίe™XΉ9œ0ςoπpώsKq;{ΣΏΖΒΞ0rr2ς„eϊC$vv€7σ)š=ώζ0€‘ΑΞήεOΰO{Ύ3{'ΨΏ&ΚτvΨΘmβκδτG]OΟΖ΅ώ[Κ@ ΠvmΩή„7Δͺ1€ϋΉ^ϝώpšόP#šή{Ν©Ηυ*…Ί.;hΫι—pΚψςζΎ8Υ£Π:Ρ»χyG3TxgΧί>o Κs‡]°«³˜#3ΕηΒMΓ0ψτͺBG>οŽ>κΦΰ ½2δωŽ\ˆJ…hΟξC’MΓ•“aΛ‡_κ8dαή*ηιcΤ’uΛΙ Œs–°I ]θ  iPo<- ζΝ|Ι$ΠΒϊ^Δ°–xkο°ΔΎ,yύ¬VeqξΓ!ΓΡΖ&Dœ£π9I•ΑZρ./ΩvšL[Βμ™Ϊ΅MfΆ9‘ς:TP~v:½Σ§ Ÿα‘ΒΑfG—ώ½έ)•h[FœΙ–s³)³ν!}ΗΩaμL§˜hω©hΓΩΔ Η}ؚΧδuE…Ρ¨Il‘Dd7ΘNšΧDQth>ϊƒ΅₯;jςsβ'zK;7’‘8AGOΐžyv£{ΰιOUβρM1ιΕ[―ω·Σ}τ›“,ϋ‘ή£)gS[}Πς–– ώ“ϊΞ”<18ZΌo˜M9ΏΈ;π pcHS²3σ;X[”ΰF τΟᨳκNΊ†έaŸ#†ΟφΡ+Υ*?…λjςΉ²QŠa€ βb_ΪΐΒw§\1%gwyΔΆPΓwe"b?¨Β +ο5jξ§Ύί_λΡ3ρ1Tš«ˆ Α+Ψ$£iη*ΖΛ]/Δ²γιs7’π”Ιv@ψ\Iΐ…|zFΦΓ*—ΕιΆqr;vƒa τX1ι§yœ§6~ώ Τ<½τ? μΆΔηΕρΤWzti²_†)πΩ°ΊιpLΘ¨’^κ7ΚSΪ·UitoΚd1΄βνοΟg ΉΔβ~9™—ϊ†jΞ>β”:v ϋŒΗ-dν7syΏHΝΗͺ2‡jΪ[&3²Œ%GθHσν±—/tŸΟ’9ί9π^_AΜ­¨5:9›Ar¨YΕΫCϋϊͺ8Σk!λ7CŽΚ·$ω"κ° ΎρΆθ+·w₯‹R>fiREƒCηŸΕ]CcOζυƒϋ—˜Ζ;V΄.aϟI‰7Ώςl•ͺ¨=(_χγ(X8Ό Μ–-"±žfސ‘b,ΏΌ›ίΘWΝθv)@Uί#ή6ͺg–ΟcyΜgi―Lπ,K'dαϋ3NΣ87Ώ¦S`f± i-R§}ΣβFΪ~’DγζΨ]•ζ„,4β:€Λ›#!d*ΊhΟ|‘=›1ύ©'ršŠ> 8šh^fδΩz-\Ϊ8§ΠViχȍ™ΙΞpj™Bύ]Β£ž§ΏθhsζZΛWωxΖ|τΫ―nφΤεΤFνΓ‹ΈR2άI𷠍Ρφ³Oό―9ͺBρΙώ‘^!ˆ.x;λ,?―ΰεzΤp‘β/Α­}™άθ&α<κάιτͺp!ΘjτL΅ΘAώwΟλlφG ‡δ›vε<΅§φ;4ŠQ¦ξΞr΄ψ ΚTϊ¬ŠF.΄”9ιOašΙ"²]βMs"epl©ό*^ε~uK΅€ Ab›ω΅κ9s4Ε“ τPΥ!Ξ$μΧVχ™$jΑπQθνόTuctNύ=λ¬άŠGήjX…Ύdš{„΅IόΎβ5Ÿ¬ΔΕAΒ΅βO“©ΪνΒΔρr›Eέ$Žy‹L–΅ν–ΩΌxB"ν7΅‡Μ=›n%rjυΡvDΫ›Ξ•ͺTT~Šn#ΨγjΜ™ΨAητ‚’ΙΖΔ ΟX5\oε ΒŽ‘ζά‰4‘΄8N+ι'\¨@¬π€±t–4άχ&#τΘ·2rψΟt!N68‘OŒ36ͺDόn~Π+ΐ‘aξΌ₯Cυό>­GŠΫ”¦oX£ΰ·IΙeV1ž΄ ύΦΈ–‚…ΌxŠΆ[‡U§=˜·]v 8a~œΎR–΅Π­ŠΑdμ!‘¨Ύ>δ€οz+½#·Pηά6πs F¨‘λ£*ΏΝ'Κ]>Κ)ɝ{[ ƒκ —Οš« ͺŽ7₯’sTŠ1©¨-My] +»‹ύ{ΝBδς‘Z Ζ +ΎΟΝfpt8Zu”Ρ‘C$"v°‚ +ό¬1E Z<ώα0™ΏΟ“-9˜€jόuύ.τ©“kh½–Ό]T›λ΄†!Ή .W.‚wIVs‚€ήn8ΣΘφM› {˜)ά–wψ$΅έϋGΧ>޽tΡεoδΈΓZͺ”ΚΡ£ΈVσ yφΛΜς•vΎrq΄ 7I<Š-Œ“†OBΑΗ y ±‡ γlν±ΝU2°ˆ!"πί&ΗΘͺ΄Œ–κ₯0`„όΝ™0₯Ώ&γάσmρB¦ύvσα»δ€Z”\;r‰qΤ ‹ΨsΖͺ!―§"Τ|pK™8βτenζ΅©“]― %β(ANMΥKκG¦Ν|έi°Ix0ΰΐΈ§LΕ9Ÿeέ4΄¦Q…o₯ͺ­ΠΛDβ`W^₯Χι§―Ή–!fuζžmMΒChTމ;7+ΜEΫ6›ΜVή20lΌΐ‡D,άe |ν §θΐKυ¨=<Ÿμπ²KG썳-―7sΤ _KyζpŒ΄‡’Iw eψene£οDhα‚7-ΘΚΓRβ²v»ζI'‘AμΞχΛ‘θρ²ΎK‚A~Ρ»b—7S˜ΙξΥ8±VΠmMKašV&mdΏm˜ƒ"Ÿμ"λw<œ'κ©ΣL=ΈnπƒU=ƒκA +Θc8φ EJ8ΧαbžWΓpŽ€}wίδ"‰>VΝK€2r1šGΈ6±ώ0wwq£³†ω«ρ6KΫΟμ66qŸ.i^θNv΅5‘'JtΙ«γΡΕϋΛ)“ς…Zm―μm4>ΕRΰϊ)Q½.ξHMόζΑl!³œH}Υ *©ΜA#hΛMίύΗΰm;ΖdZ3T%A–S[NŒΔ¨(εΑμHšΉ_pΡΚerœ‹‡‘*Z:W² o +d’…#jτ Nx:_0mAc…Σ/ΐsΆGξͺUΰώˆfΊ.ξήC―AιοΔ_JGZP€’ +½š +Β‘©ljΞzΖΌζ  „1τr<ΗΡOŒΥλ΅“hF«©ΠH<ϋ³Μ/£Ιwc† κ})·ββ`y‚­&Θ؈ΛΟθπ’i£œœτ†£kd_κjδέΆ‘ΞΞ0}šB)œ‡’§αk94‰‡ΜIνLgΒu’άQBν¨BLΧbΣμν‰žΚ B/’νYχω~vUΒ€E4ΰ"τ#†{ΘΗKχκΓφ<Τ萙Š©ži/#ΏΐΟZ ΅ν%Yϊ(2‹s»«€u:‹@yΕο.gŽ5·d²˜ί€/Ώ‘s”#οlLC™8₯uάm$hΰ2#ϊ “lΫiNή2·€v±…Ϊ%Κτ+Šc*Wμςsc™³σ_jθ{LMRAx6}ηŠϋΦχ―»pa!ϋ*δ=,KNJM„$'¦GφVe–υΖcƒmAθω‚ΆJ χχ%¬‰«’±ŒΩ*[ςΑ”8±a2žšν%Žž@tE©dSh`x«γ-ƒΤBn|"T"ξ •΅0¦eh˜ΐ"2'­²ηy&p5α1Βw.CͺwξΧ•=¬•u ήε ΣθKψβ‘ +Β+αΕΆΠCΎ]ϋ₯βΪϊYέ$g'BKΈα«ˆ"eoV‘t)΅ +Δ>hEε‘ΕDF,ϋ‘rS@eχ #n™9q"νΆ|νφ³k£Θ±G~E=ΌdΤ‰λΑΗΦI + +PTv–a½ JΝΐ•xΌΦY)ύ¨£³8LΖT^―uεlvDΈθzΆ²΅ ‹ΚdΝ#ήΌΑαvJ 9Ǎ±„9E6tβ"nd'Γm2ωRώ’¦–ρ¦8Αλη»QŽΤŽςl*5† =8™ϊ‚@dήω +ΤFνΝή­δ=*₯‰δ+ΉYΉ¨+_† ‡²dΪ―‰ζΝΙp7Ϋ­ƒ&,ςΗ—ΑΌ¦yt½j~%Qߐ·M–ΝΡEλχGt4Šσ0ΈS^`ƒ$ηJ"D‰tκstΣW³~ΖΞ„_0QΏ‰pΆ―³rΔ*{>,±I³Ό1Λρv¬<’VραOΤ~Φ)™“kς”ΒίΚ6”Dρ;·ευ}_υΒLεD\Ϋ©άΣΗ†)K’24HΧ‹ίUb˜|ΠOD=Fμ{K,’d…„£ΟΡӟΜκ†~Ύ³lΥX.;pΞA„‘γ’yDΝM±•p~»δϊΡvΠ>VΘnν·ΔσΡ“#ζξ@RΕku[/όε΅fΪPEY·Uλ bΓΌ»ΒΦ»hC‚υ‘'Z•ί–«ι)βŒgΐ i›Q‹Μχ$‰Iή Χ{5ρ·+LRL°Š»”ƒ―XΣ%ΤλωƒηけYqp@yΆ0« ψ―Xeδ<DΠmˆΓΐ4“5Ε»rΪ5²f§9ώ΅πΘψZ<»ψχζcvΓΙ”"Ί]ΖΕ}'^vA\ξ‰Υ$―Ω ++’Ϊ–PΑΥΫOΩ ζiaόήa HcL‹RST?š]5zJΌ|²T'›χΥ₯εχλΜhΆ‹B80 &MŽkΧυω²•Ω§άθέkiε‘Vγ"^ξS™ƒΤ Kα-Γ +ƒdDΜBσqrΞ ”KΑ ,½€H›Ω,ΰ¨BAσρΜ‡ύΊaΉΎΣ/ǜ…φL‘ =Sω^daΏEaͺfCͺΒΙ§e‡(Υ4^;,FzE{PfeGxΐG^cΠf&Βώΐβ»βsYIδVΘ\λŸάΆ‘–"Θιœ3Θν DͺŸά›ό‚υΛΏ˜YfΊ²‚&f₯Ύ·BN'£m(Ζ™ΖΔόQhΥΟΦΪΜ­\”ληΔ(ZH·ͺƒΆPœ>JπδxYHΎ%§C[KkJΠWΉͺ&ΞΖb‡>„6LS‘cje½ώO ΑuΫPA ’4&Ί/ξG‚φ^MwYm›Π{ +"RH” M7±XΓκ•ΡĚ‹ΐLΝ{F_ηgΟ©œΜKͺ _nζΘΛ¦Ύη΅τˆ!Θ'WˆΘ£¬γzηΡ"IΟέb,Ρ;|κ@Šν>[ωHrΌ/ϋω“½P™Ί_„{ϋg'ο8D«σ‹δΚPKdD:˜‘Ί΅|.H$ο†λΈžŸQkd¨8ιγΈišQ°΅;ΝzŠ Ύ0ΨIΆ{Ζxο3/―όΪFo·S_ΑΛq$κ3œ7hξwπ€p­Τi°Ή£²ο^ϋ„\ 4ΗΝ°GΊpOjkο_‘ ˆφwx0]o΄AΣt%B/Μ•„λ 4f‘Φ FWVž·-•74=Έ:„OΔνTPSt•1 + σ=ΣΔv‘³ΐ3‚ρ¬fήTΉ™±―•’ΚœκZΧτT}†ζ\™2Όv4δ£TŽωλγ“ΐονπ@βΟ[g½d} Ε@cθNb†–ž‰„‹–%rV­›uχOέ€>ƒ΅Α|EέB½ΟkX<»mύμœZh!КišgS‘TOεgETΦ>6.€Ϋ"©a“"8'–‘~ΠrσRϊλΛΟ„ +Šώ( ΧGŠBaΠ02δ蠘EςΧXn]%xI*’-ψα½Κ—χΗ–°έ ˆρΎ~τ”yd―Π/ψΡyi΄$IxνθλνΑ•κ{RQ‰λ8-1Μ’λyORQ{ZHέ§ͺSσUγ Ξρτƒ*ѐUύ’MωŽ<°Aΰάά@H=εo/ΡππNAOΕN•e†#ΪURΓ7C’U@ ̈uBΆ5‹p–ΐ!~°~γΟe―9~3”ΘΣTκgs+αŸI”αΖ"J¦|uϋ%ϊσ±t ΔHƒnK‡Ά«p2Δ 4|΅ Œ*`c`ός“άΎš:Κ¬»―Β,΄ +Α¦η${n 0⏉:Χ HΣ Ϊ + +AbxϋΗDΨΡE>D¨7²ΗμσίΨ©dw_1h‘Β‚iΩΊ<—„Κ1Ά rbgE–t΄ˆΪ»“aͺ θ ίP’S"“n§ˆKΚθ}*•Υε8`LΙP¬bžͺάkΈy51—Tδ † Ώξ\ϊ’` ±oΗό#rO~5ΗΩ1ZμΌζ6€Κ9;€v/Άθ[Šυ`kaΨξξŠ4 +£ΟˆΦ &Q9ŸκE‹ύάΥRŽ\υ,γΕ3(¬·…Σww+ώKY…Α *™΄cXBH=Q ΊCsz}9ΖΫΔ… +[%kFƒWL‰fqέ6q?νΊ`ΕΕbΰ› NDxnλο P/Ύ"α/:I8Aί›ΜΡhΑΗδς&L°U[mγmϋκς·Ÿ£S₯½]‰Šb5’Ωμ£ώ•hμH”C8³z…epψ*κ ez[`vC~£α }0„πλΚ‘βSβ—Δ–¨L8Ÿ‚Œ#‚Πφ»θœΒΞΘ€ό{'“ΛόΜoΑϋΉH:OpχRΆ_Φd—―y_ψ|4qΌ.‰4RΥ“ϋ6ΨoSͺj2<ΦΎΒ€BρŒ%"»WΪΪΤ7N³Z Ω_]UM₯ͺe©2TκŽέQΠ™Yρχρƒ[π’ ΫSYφΑ#Σ Δς##”69pr€KζˆCε“a5Σ‹ ;‚΅3ΓΘξ­΄F+»«γwƒ£užίX?…ΕνuΔ+…8<¨!|ˆOPΙ­Ρ€π΄κέ2 #€H膏‚ΣΎΓς“1ΤFa`Ρ/μγ½] ꭎŸΪΠƒΌή―<–$hs–ZΪ±ΊeΤ6= η.Ιm~o([™I>λ'Ψ{Jϊ:iՌρHΨΝΚRϊ]/c’”Τ΄ˆΡrPHπ‹NΞ)”»ΌΚ€I‹ΏOZ 2Hr‡Ο₯o±‡Ϊͺƒ}bΩQζ—š³[™υΈg§ͺ™Χ2㫉i–»EMε`ίίo=’κ£D ›tέ‰o.Q+oL2χžNτˆθ9ΪΘ/6Fu:&4lβ ]nιήf_ρΊϊ\αͺθyNΒ\ίLLμΛDCŒ\ώ(r\tEΘω§ͺ²―x?Ξd,;€Šώϋ°Sg#₯…›EΛχΕ?q\ΖΓ8:9KOhFτΝ.›Κ%­ν―‹SD;ZmžcaxVXΛΏ―NψuήUΨlVΆ~ΙV—°"2½guλ½ίGδ);5§Α_r| +K/φi%PSc»ή*ϋwIΛ)ŒΦΓ„9θ“©,bGΆ[?ήπ{–d6:ΐH¬R~W`Ωι 9u‘+„ΤΗφο•'ΰ:X ΪύΘX‹xSμCΌ<[€YιμUΓ°ΑΈO΄΄Α΅DΪ‘#Ά΄– Dδ=–x‚3Œδv²€ξšΧ,šJ•N +΄zQœ–(t\Χ‹&ι„Ϋ}Ώm –ΦΪ Rj\h[w±PJyυ xGώήυKxς˜7.Mμb›Τ{φvΑ₯J6Ώe‚…π 3φ_›ώΤ'­Ί ΤRΩtλΓ1u₯P]ξΎΎ’ΰŒβκZΉ3>ό%9?ξ™²}ΠI!£‘P―Ι°N1jξ­΅<Κρ“C\Q’mIq.­Έ­zOž‘‚'CChhsολ΄Άb±$κVSƒŠΆ45M“±Λu(jš„9¦Xa4‘/)@ ξβ|ϊx)v”OAk±P:εšXmGŒ¬:η™rλP* Iέ4HcdŒ‘8D’U…Κ’|”Q‘4Hr›σΩύΙΕΝΜρ‘™ςO€υόPvAlv‘Hk2zβδ%υμ–1ίjΆ₯Κ£zMͺ,Πνrϊ€Ÿ‡N ΓμςD%ۘT ΊίέΆ%\ψϊEηυΙψ‡~ζΈV•΅ν&Γ=Ήs’2pΐξ§Λ%Κ3›„•ΙM‘/*BP­ΕI΄˜ϋΑξ|½+ž=ηνΎΚv₯“γύ&5νϋ~5œΜ¬ ν™LV΅zMπԈι90)Ψ.ŒΨwκiO0uλ_DΖ £lv»ώc덾ϋC«aBŠQ~JνρlΫΙΩγym‘}ΣΟ °:Βηeu}!VpFάϋ +ΆΆ Χ™¨‚ˆ!9u1ώύ[ΕuψHΟΝ\ϊ“QŽu/4ŽDƒfLva +ηZqYΟ΄†\²HS­0b5wg¨”@άΧQ·π›ηͺ*8AMΐ/€E·τ\φ4ƒω,(R[Š —΅„—Γ½x2γϋλΛ› yB·έŸ†?DΪlΒμνΘ ύPwžuλώΡpΡv]vτέΊ˜~Z˜’–HΠ’ ‘ZΉPΐuΖβΎΧ·υΧvRWιρωΓΣΉΘ&_Κ€ς‡C΅;PŠψr‚―ζ֐ΨΘ©Β²'m Ώθ)ΨΔ’˜αΕ”³ΥπΖν­ΰφˆκ·¨%§Η#‘|±1ΊΤ›/iXZ²ΧΙΘ•'JΠ#Ό^o£J +\ΛG‘Άέ[<#ΐΓ>>z" (QΖ¬nŽ€Cƒ4wY$¬7™υΰΐ\TNρ#SίοDφKώΕΣe©œ‹dŒ„΅«?CBΈΩ=}ΠΟβΩ‰]·jΑΪzœ~ΓRjt+_w“ό•ΚπU—₯δs 4,γŠΞΣαλ _Ύφι*N‚ž©Ή3ΙξOœυ &“”o|ΤούηΧ…ά”#ώcp†ήHΝ €Νd’¦΄ΑŒΟιμΡ2‘a4₯Yo!G3β[HΊ-Α”φer™ι?ˆ[lziΤ”3 έTΝΧYŒΧ Œ›»ΜφucΑδΛκΐBSΝͺξ`J` ¦Žχ?έάT+Ε«xυ5rlΥZή/†-D2jV€gWΎm>ΨY†2ό^šGχΤ€2ΥΒη{l₯ξη<’s zό ΆiΚmb*?Yγ w64Πt _`Σ°C‘IIΒ㜍>±iΒFcΪwίδΎλj«gtή«ϊδΫ?Η…ί˜?a> ˆψΟν +/΄B­‹πΛhݝUψ KŠΫn{N&Η9GNδ³–sΟbαjΛx(ήdΘaκAUΰ"RΖˆ“φυ°―ΰ<Ίm§Λ3ι=„μΜΔΐ₯α^₯e¬Έ³bŽ΄K "±k‘­-΄BχΑύάό/ŠΎΠ.μΜΛ¨ω§(’bη +ΑϋξjQŒf3ώ4°ΊζrA<ΩΑž‹¬»η^½~gœRή=3±ΝU/{>~ΨtC;+ηΒCΩΊIPzšξ菺"ηοΌCΰ"‡ϊν΄eαηȏj€¦x8ΫσΝΦbΙϊhLΝ¦nJ₯$3Šo6㜿Y­±QlN½Uf"}ΟDq:έσj(ͺΑ@6‹ήιΨ u!ΪgG’έΜ{ΎόΚΰΧ³Μφ5j­ͺ–va=A˜πώpξbDKΑS—:œοƒίυΫOud²K’SΩΆΫ0ίΔЏr–λd°γWμ7—\ΩΕEJ„wύJοP«GθλΣ_§3q£†,€TUΟ+<έUAή3΄Wg|@υ›|nβb‘₯‹‹SeθΞ6‘ xn•\βu ZΘΎφΩedφΰ1οWΡ½…ΚΥ +χ‚‚Ϊ νΰΫ\ωKωh¨ΦΠΜ9ύ†΄]1ύŽWύΠC—'`j»ΓFΚ)ζ»G³ι\NΓQΏ|€‚ Χχ4΄|‚:™_όSZΥ5‘Ϋδ*Τiό%~έ’.U-z Šw»Κ˜Eν|nŸ„oY¦Κ°’Ιh*i°qžŒωSi ± ––†tŽjƒ¦š΄ŒΫ₯hˆΫιΝ™δRςθƒpΓ–ΏυToge§°Έ)|WΧ©ΉΙ―©D/“3χογͺ’αͺG‰bP–0ήύ‘ιΰ/ΉΡδά#κε­‡ ήΤͺV\œ―F,‘ΰ–§"G^‘~^(2ͺΩ\Ξω(««6j=t€5oΆΚͺ‘ŠΟ,ŸZνŠiΫ‰»δΎΔBΌK7^*λ0F.CUF1υ½Fΰ…ΝΊω]οΙηΟ€sE~ŒR6€ΠξΙlMζH*MέπΚg*PGD«Ϋ»©=½•gqω 1T‰ž+¨xž Ά'ξxA^AΏy}Rl¬ό­Λ›½$€‘δ3Ά³y<„[¬)°Iχf.0’δ€KαΐΩ³/€[™ŽuΘF€ΟγσΈ(ŽP\Žu©ΕΩ";έ +<•šΏ(Ολ>œ‰HUQ%bμ9\Α·ΓNάRO±R»₯0Νy̐ΨelΖ>’uΎΑ5Š΄VΌ;ΨΑfc·ΣηϋF—%v‰‰;5•ΑwΙ–ΪήPσd‘Ύέˆ&@QDИϊx[ӎσ8Ρio+­M <ϊΆsQS'σρυG )γuI°?ƒFΐ.ΦȘKμ—T gΜω ¨Ζλα)QՁ)ςΤ†δ찜&άaπ‹ΌWΔ¦\έf? +ο·SΛυŠ~XLκ¨k;¨Υ2΅yξzή“τF@)`t΅ZRH’ζλ°§ψ "!ΪζδlΗ’p₯xZ)υ8|L( ++1H‚ŽηVŸ„–Β n<(Rtg’ς’vI_a±Ny:,δzάΙr7fM qΛ}Γo0Pψ2RY2Πχ29Š.ίξ³ϋbŒΨkήησ₯ΠƒX% , i…„ΑbKŠφGowf_Ε'AυοœNbmCάΤsW<9I_ίΨιΐNυΟPEœq»²»FΆν낝 ΫοzT+qE3LoΐΑ“γ’/Ίs IωΨ7?ΔΠ5πŠύ‹.Φ,R$6cλ$σΆϊzϋΘͺD΅Τγm…¦…μ#δΞB"0T?;φ-²Υπ"­$¬oM“I%ί©Α"2νb.Q¨ΓΏ=L =δ²ή„<²§έ e}Kχτ§ε\}t•κa ϋz„τψD¨―ŸjϋΫπΝ©<ϊ‘ŒM•–AΩ+f΅­/l½όΣ”"5Y|Υ{§ΖΚiƒ?NΧ ΞΗΡžΥ,κVJςω“‰U›ΒWCΔϋ|τώIZηχχζLΝ_σΥ*:(j3,’?[Θα]_v8q‘YΒ4ω^’otœ>£PΦνSn4T[ςζΚO~δή‡/mΪΏ#β3­Χ=΅ϋMβB©βΐ L(՟ “Ι=KqΨ#-σΙoŒZΛVο„2WάhbK5Β8œτxΟ³ΑΗ*dΪΓsW΄Ά•γ0Ϋλα*CKα/Κs’ωςπ½Q\Ωq>ξφ‹ždΩP‹΄s‘ Q5 )]PΑz-`*Z”?Ζ₯B’ˆB肬ͺ…tNQn‘πηΫkΪ±ž‘TίeZ$‘pIž3ŒMήOB“ϊq(Ÿ ϋ3s9ΑΑ‡―dV)·+Yά +"ΏC ?ΙΌ~ύ!Wουεaβ I?JF,κDy\W—‰%ξ­]IηT’…Ρ­ψB†lΆsΡΆΗςQ.ΰ°Ψ«]{ȞνϞγΤ{Ax¬]½«ρŒ7^ԁ™­”ibΎε>=Œ;ΛυCάηw|½&kŸAθˆyρςΥΘυσ!ΐσ{ŠŸI]„Ή-θεΫ’Ό/-%±β€pεxΒ₯2†(e +CŒT‘!(ρ48nE-§ΗF₯͝-ςε8=λmUιΉτžΡQJbvΏΖnθD‡˜Ζ—Jz€B_6ΰΌu?O[[αUO„'υBόVBn†ή8hZ~Š΅_DVq ” j’ƒΌΣ0ϊEœζΪA£ ‰I6ε0ΓžŒΒη΄Μʊ}ͺιc Œ#-Sοβ9Κ΄‘[”9ьtΦηϋιˆήD₯)'}«‡]‡•PΈš«BΤ8QΪ[xΰΐΝ{~@_<Μ–±“_Ξ}Υγ’€@lٌVš—4Ί)ΣLπJŒΔ+n„J‰υoθ|;‚ήt”žFδ$`‹UχWFYA  pTkΩVh–YΛ?ωέ?³mΏο`z#zτ2΄ΙO{0©‚λΦF¦€Wϊ‘ΓleΗφ¬žΔžΏ΅«5B¨zuu’ρ΄ςκi<3Fͺvr’΅Ό™Υ>Ρ 5XμΫ•›’©ΙU€ΉΏΑ—[Qpƒ,@rƒjŽckΧ|w€ΛBςgyΨ#€β‡”ΈPo’δ§›v¨σ•QΛζϊyžQ·0t |Ω–¨ϋvΟƒ€;`Z3΅χl~χ˜ΜEVRρ<΄š£S}“£ξI^¨NΉΕΣBy[°"xXˆΪ汩¬7`ΙΙη£ΒΆ­ΦhZM%HpΊ~f~  ž^ ¬bς‰μ*»KQ½σ·ΗA‹ΒρŒ¨Μρx•χ1Φ$αFžPΙ+τΣ";7-³<ΑARC$RΧΰ53)([=ΓJΉc!3'™h’) +[πρήΜαP•&¬ ι|²b§6GšπSρΞΏv‡\Až2O–‘1ζΊ―V£qό` ό ?θ uy;ΑVΝ‡tfK…©™΅šΦ$ya½όΌι GvρjΦ―£ΛZ±¦R%›κ uk— ώ+ž#Ρ6Ε*„βέK‚R~βŠΰΙ@œ›X =’hΈDηzΥ_Ξ=κ±A»€Ν±N Y©žιΊ{žΑο»WΛ΅₯SON{Žψ‚―ΧFG@DC3£v>ίd±7Ž'†( A6†ˆΓRαMμ•§DœΉŠΐ-οχΔ‰ή]-rBΖ„T³&|.βr.M„ˆΊι|— τέx†φ eκ—‘’CŸ%Ή­][@‰|SsΔG»Šl½…ŠFφ…GT\Zνg֎LMαΔ$ΎŒ`‚ο‚(“%Ζ>™ΤζB%ˆ»’Uf5fS_|}?³[έ€,fΟσ‘™‘~‹›_ΚHoΕ)@.ΞO/Αcf&ειΔ4λβ©ϋ#θ•="l έΓ¦½|Ζ“χΧΤ/-.πΈ‹—d' 6?[Βυ΄τ»άΐχ΄σ³!ΔΊ73ΙBΩ•΅μ½χ*9[?VιΥψΰ’’Iΐ©Oh½n lͺ_ #ΥΒς'„0λŽSˆh.n”Ku(χ0 žYλV)Ϊ>΅}§ ~Z€#ΊΕzκ§*‡lmβ,qχΊM +©΅ +ήβ>Q­­k¦eϋRZHut¨qŽ’»6»7`ΑΓBEˆ;ŒpV$Rή8FΦ֜ψ»ώΚΧ&kμΗ„KΫΜk¬K”ϋΚO0aˆΗ΄΅ΛG6ζΟ’N‘BΜΧ—άυη©!ŽφƒΟz…•±\ΐγγΖOΏL$Ν²]ΊPtλ’ζΪz„k^>dγπ2πRl"Ue‘E2J|‰,DΪύ†~ΰZΖН%‘εύ}ˆ©eGΩΜίτΊSΆύ˜ΏŽ—‹„“ίU.]Ο8νd4ε\opfI">+LG`L¨, 9³ΖVέ$½¨\ξ$³­’›|ΘAkj3“(έ<ό”.—Ό»OHΞ‘ν“JΥθcƒhΈKP/ςzθ›uΝ…ŽA‚X+›‘ΦΕ¬›ΚwδI|,n±ιΗΉa3l4Q²  +Šˆšš‡G6ꁓ·ŸŸβΕkΝ n,+FΡ'­ΟJHΕQκΉ¨$ΒhΕίNjς a{ΐu½“!‘ε'›Œ^pΕΓW_·muƒ‘pkλή|OMžH‘‘σΰ- 4€έΪ—ωΑΝυλθΌε-η~pvίΜ₯:0+ρά^½!n6Zvu‹ΰτ ³\₯@ί²lžs‘Τ¦wΊ›ΪζrΧ.λΪίyΖζsΕκ. ηœτ2Ĝ'τ€‚,PPA£zf[)aφδ|γ#·ΗβόώOohμ†yΜZβ‡ `°-Y1΅0+“Β΅)Ν€υΥ|΅lύΡ€'χ§@SX‚‚IQΜfaΔ³_τ¬ ίΌ!&^{Β}n AΡ}ψςkξObξ)VλIτΝ•M=ίϋ-‡Η'―ZηiίΘ$>@0N[Š―TγΞΪD²ΆΙ#πmvÌ_"˜TΰΊΞŒJCͺβDηΣvμ$HRάUΚv#²Έ;šόYκήΕ―ΧδιyBΉ§ΨΐΓ/θΒQςͺ”Εk@sŒ‘2m… σ4’nœmpεΥz;l¨o ΏΤνB•\U½ Κ_αΌkqω!©]D΅JΌύ=hΖρ].Ys$ς N–Ω΅ϋQΉγπVo­Μ/Οα'­Ρρ΅ž>+!aͺέHωή¬0’ReΈCЏi΄Β’²t„‡JBUΔ9–Ξƒγ­j’M‡γ³cΔΤX²τa€!ሯ― §αŒ–4ލBΎ“4Ή‡…[M9UplUjmxλβZ9ΝΙβΖtl"…ĐΝͺ$Τ„žYψζmνα¨UwΑτΎΝ> /o>λΗέ Δgq΅g-Ηβ€³s…ͺHψ’΅Ar9W‹ΩΞlΧ}<Ί,Mέ΅αRΙ§β~‚εœ!ΤΠ„Γ½Xό›\Œ)dδΣ uΪΨΆrώͺ|…Τ6΄hžσg~bΗS9-Žqέ…™JO¬φι~―+I¦=8W8Ϊ^Ύ t>OD\Π(θ‡sό_L8Yα•₯―±Ω "TϋμQq$Ζkoͺ>vφΞ'œγ“3Ήor†^šIz’bNΥ#<'|τΈ„Χά5Œwpf¬Bm2ι«’#ιΈ2ε·z7Ž2΅—².Τρ[p½?θYχ ΧΊ–wiT"ΓgV y½½7!ρꑚ>"¨k Ž-+E9cmΣjo―%ΊzΒςκoY±}- άyuβžυnNδξλKGΛ@ +^GΐξΟϊ#ςUyώεp‡LjΘlξ¦]?ψCŸ₯Π!A1t y΅ˆ<,€ ΫΏJΕΝ―θDοΦp?~αΗsC}>l l„@@9₯φυε˜]Eo9bΆΈ])`yΐ΅³η#(η“~2·€NO βGlL¬Έ!'ϋγyV.ε3²}Ξ@K&ΗΝκq‹Ο‰ΙH¬»0υ[;š―§C·όΖ“KPΛψHτθ0œ†1Kτ˜σ2]–­ϊΣΑ”θΙ€9ζ.dG*ŠΎΕΈ%ΩOχρΗ"-πύ@\u£Ϋ¦πΑΗLέσ}=χU3‘έ܁ο¦ΦY#:šΑ|b/άD„ΈΕώ7?$£—YΌ6―»ικοΊζƒ©~p­¬gΟ('ΔU$¬ΑRv\]ωE|έΰε‘3ud₯Ι׌ΊŠ0κ!ΎI/Ωj~Ά +‚vzζ*ˆ˜~’ Θ7z΄ͺτϋšμ(LβN[b±*3'-xπNΠ};ΞΛIqKv”Ηbd}Ϊ*DΨΗ>!γ”₯ MAo\‰ή,lXMv]Ωίf6 ΣΑρΓ‚LeQ½—XeΡ†ΤQζ -έBkˆ›v#ΚHO +ϊΪ›+m»'’^—ύρšB_ υy’ωΐt[ΟTK%ςΏ+θ@UΜ¦fš‹«ΐkΦΔ%Φ€€“½l8χ0β` V‘ΏξhTΧΎ^"·šύΝ κ1ιύτJžqhTzψZR)ι(εΑ&ϊ5]?,%όψKz.ƒ2Ϊ›Λ›kŒ +ςg£U½³uD―³΄σ@@Uβ4ήPΫgΠΌbχξ½ο¦Nο =ΊCΰμ¬Υ‹βΰXΦD]KΤ Y«/₯ΏΨAλ +L ŽΪ-£Ε©& Žtέ— ;bΕO|oF™[υόΏƒΌ~0·šXcY€²iRgΛ$ΚΈP“Noo«‚†$ΗQ"W8[ξK *ψΖνίK`h[Q;Ά“^.2”Ε5νuτϋˆ£/S•Σ ΅CΰάχΖ-‰ήηΛž…)— +7πBŠ‚ΤghΠ!ϋoJΌxn€Ύέy‰ΰΦ#™e²5€B˜1HΖ}½>*”$Όωάx€9xσ¦ΐ«Ž;@½k«Φς£Γ‘ .yN@#.6ρωΘƒˆ8dΰurώ™"i-3f†·=(”1N{k'‡ +θb3NΒκ}Kμ;½ KoTUsεH¨c—`†wSΨB…ΏhβubO΄t]ςf䘠0λНK ΣΨ5m=¬Ίtηg›x ~φ9ύ˜¬W<CH wgo™*+°<|¨‚8Ύ’τLΝ΄v) +©ώ§œ΄ϋϊwε0λ~ƒOkυ(Νΰ:)“ζ +‚Ξ†’Clu@te ζ―kγ-?Š6ί ,jΦ}#{&yD±`Ζ%žžIŠ¦6^ΉΫC”ΌΣ ΕƜ‡‚±Κ|I–X3&¨)³k4ΠE€(Ω―D4τά™ΰ­Ωψ<,Ω<η°lgΩ[8γ.t―Ÿe8=Ÿu0”υZs“Λ'΄;}­ϊvθ]Rμokh‹'%ΕΊ#UΩ<ί²ιΎΉ.x΅ Μής £žΐΊL‰Ι¬œ '1ΤόhΞuXΜΆΥ7ΤIn–P“qέijD­Ϊx¬J+"WlͺqαχψZDj|Ώ―ƒ%ΦϋέGaK…‘Δmœή XW>#1T žμζ5νE€εT>b5RΛ₯ΝP~Ϊ~M«χ:‹λΨ^ Jάiœβ.°šώ«ΐ·•‚εIsΘ>±Sχ! +…«oόwf7ϋ1ά‹³Ύ&‡-α;ŠΟ‘qΎžv(†¦il—r88E‹ίδs΄Y4UΓaψΔ'Oτ!EWΜ)νζƒΰ.ϊσ Ry&=§―ΧθΔ爧z3Ad\’?;r=g~:4‘¨F–ιOΦ§Ω0ΐSy;.)0av΄”]_²θ1ΑJjW<>Υ΅ΦΘΝ=χ«AU ΅ω!U"S ZŌQ± %’ςδζŒψ‚coŒ Π‘εd†οΪ+L8]Γ~ε–P{L’(ωR:.¬$AqΔά|v|T’πaŠύJ ΤBiNIΑ8ίvΉ@·ήzΛζ―Zx&–a2ušqEΐέAg°5DΫ5>Za7lnU₯0r,Ύρ,3Φ>lŒ/‘¬'χξKο=!;kΆV‡ΈKFΏΘ]ΥΖws3_y<ΈP>ŒίΎ½"_v=(l‘@{!k€τ–73XΙ$Π8΄ +[_αθΑbj¦™Kΰ’ jΆΰϋξ4$JŸϋξMέېωΕΣ<ΙŒŒδΕά3pπ”ι M‚ΤVF Μ/ŸKσο">7Φ#~’oΥΝπ₯s‚dλι'Ξ³ν#‰Jο…°΅Y ©jp,/W{βοb6λ–YBΐ~ωύh\df(uPΰύ…PS³¦°°Ž~νxώΑ/1'Ž©όMΙΪyMqμŠ_ά­΄‚εqυY <2ε)B»R―%!κ‚lHαN­σTξ©r”pz’±ZΜQ{<₯¦ΜρV˜ƒœWN χΉΠ^A‹ΠzΙPx@³QΊF?όύ*΅Θ_ωκΩDˆ юڞ Π‘8ύ1οΘ#x©Ώ]3πΟkΡ¦Ω:vκ1Ο›|>Ο!-‹ώψhψΘYY-|‹|βcέ5ΗkŽkΠ.4q8’gωY>υ9a] ||ωΗ-…εٝͺUΗΆΜ15ˆ>ΙοRΎοΥM$–Η ^w«Κ©f§³„λLκ‘ξβΜχŽ>χ¬/N·Η-,h·Hς;·Ώ3“QΞ–ŒψεA/˜JcΖΛ~β(ΪϋYΕΥ,›|ϋΑD&ΎQ&~-ςƒ7p>ςήqqZω#D•Pwϊ°‘ΩƒΨ?4_ ˆκΜ, νΊFΡnE±ΚHΏœΎW‰.ΫΩύρε²RέQRf58@Y;Ÿ¨U»q“+,¦‘’^š T˜š:v§d™_κωθ]΄cέ”Ο¨>%ΥwΑ€ίθjΕ|?@LeZuΚςˆΚθ §uΕ2€CΞΦfΆγ‚ΥώΜ~½GnTGm_Φ”«ͺΝΈ|ώϊmγΑΤ :΅3nvNϋ4ωŒ άΈ_¨†*«π„WŸuΏώ „šΌJ·ένιjΕkΩ]°~Δ`±ΠW°ιλhΛηhζ‹©Άω4NTέd,Ι筁ΙX<­½ +Μ^EcŸT[eOΎΐ:»ίοF-*ŸzΡRΔδΧS@“•NΞ]?αΪ§lλ,(IϊBΰ€p˜Ÿef©ύ§7ςϊΛΒ%Ή)ΜϋQΡVfΓp6~ΣΝΉCΒ xQφl›`{Š€φ}“ιœΙ ς €\› Œρj£ξκ•a`ͺΔϝH‰3aΰ‰ΟΝδmdw%¦ΤΓΣT…:δ4›C¬u@ΐΖΌΙχ§ό:oͺαρE; 5εf¬ς\Au&MVQIc›UP›2ΞΗk|…υ¬C7Γ‹2€ςc—m‘ς½–/η{’™š­RρΒ€I‰M²·φ- TRA:c5BŽyλ$ϊ‘Ϊ>α +ˆDn]–°‘ΦSΞtδEyyί΄Ί{δΊY€„χ#¨m‘ewc![ΌΘe†aDΊGΆˆψ“ Y)„[_rš`™­)«uμ |)0wHυ:ςΆF'Ρu9μ―υ»ώΞf.– uRž`"'ΒθΨ£»Κ–·ΘaΆ~EŒ ³ΰX3ΉπtΛ}8 ό‡ΑͺΖAC'ΩόΡΒvKμ›EΉG:ξ”Fφ6·£oλΤ‰ΐ.—Η£ν$)X΄Jΰžύ|–I `λbτž'ϋ―-{ˆ)UEm8bΘ$DοĐΧ₯9κKΌIΩQoΜE// ++S5"*8ΊWμ¬ J”YζΚ˜yόιu’° kΖ”_xƒ9(ΐgqΗ¬ό^¨>PM7†αΉ‘Xuiη%{πG{ΤΚŽw8MoωΆΣRή°…όϋd«c(uw, '‚)D‰•8 +²ΊV…Lž2`}§°α‘OζϊΤΞw\Ά£!R±ο άgˆGœζWΆr=α„/ιΜ4^"Ϋ…ΩoM8bn7|5Β,4θtV½φGdž4T3‘tβaΗν{„‹Y™9²jΎ™'ό?ˆ8OzV8zϊH +οŒϋDSšXλ +λ)Ωδ[09π‰ιψ–8ε¦W΄pκΟn”;ϊ ς_“Œ9)…ΧβieœΦnbΟίw kΞY£eϊEΏ žXΐP«κK°Q‘…x‚œέΈE4<΄,ˆΈ νtΣc™0hά!T”ύ₯ +rΏy₯‹νΊ‘άή̏9ΆCy1?Ž^p#ίψ΅Ύ3nڍ2©ΛΎΨΎΡm(zΒγ$_z&φ¬ν9kʚ92&Ίκͺs§ΈqΚ<ΝέaαjKΑ^…€μͺΙ-ϋΗ3t‘™ΩΆf8,;ψ;³Έ§Ϋ†ΨΗΕ’&©ΒζNΨ·Ψj‘λέ²£ψښ…Ρ H2Δή3ωΘ)‘υVƒœ_΄MR[§t‡Oϊ¦†aϋ=tE¬kα5”bŸ©0<+•s™’(­ςΡ©TzIN糬ΫWΏS)\Q€†UMίβ‰₯ΑmΖMr‚), 9H·›ͺšpLœygήΨΔd[Ί?ά<’F£­ΐH’œγ§‘ρ|Sτ +šU£₯=%qc!sΥ1υΛ›Ψ?† 8γ/‹Κ(2tͺ‘.υΛ•‘H9φ=Ά.υ9€q¬bK} iJvρ{[{{ΠϊέΣ!m₯–»“)K ½L­6XΈ‹ΆED©€΄kύyδΈŠε§*u +ώœ{Eˆ`D‰‹’@«ρξIΏ/Έ.‘.{£Ξ[ΑJ‚£HŸΞν°¨«μœ§«-ƒ>„䝖MΰQ:rŒΰŒΧ‘vgσ›ΐc—Ψ1‘ς3i@Λ²§hμ‘LΧμΗά“Σ=εM"‹1ΑρίΛЊγΡ%°“Y:ް`>fY›ˆ–†ƒ“8 ϊx΄ƒ%ΊvqΫώ|ύŸ©’M6gΩϊ >$ςύΟιΥ»mDeέjΐδYλΤ‹ +aaQ°†tGΦ/VώTzυηΦЈy`w6¨Š­HO]IΤ„o5ύŠ:Jž˜έ¬NI“¬b"ˆcΚ ’Ή1XΈ= WH|Ύ'³l(Mκ#ΨPbΌζŸ†§Νΰ\ˆΗ§–κRΌνγνn΄Πϊ™BάΎPVYyχ΄άKgΨ­?yΩψpFιΙpίθΌSΜ<υŸaΗͺƒΛέ!ϊΌ5y;ζ3b#Μάθ*ζx`{Dχ€Uχ­Ψ,ύ \ύ΅ΜΈ°(Ύί“vξκ―xWς‰γτ-ُχ8δΆ4ŽΊ%―7,…˜‰2΄χQ‘ϊŽ‚φΔ\KK/y”{#mήΥfψΫϋo‘ϋΘ6Ϊ\Ÿδo'κoF].zεgΏtΏχ`¨†ηjΈ‹"VswBq p(Ϊb}υΩOξΜALC=&ΰ)-+Μ—tιw…œΚ₯PsΞ*SχP°HX+GΉ₯ +ώΎΥψ\Vϊ`έ†Ε*'R6€ ν%§OνμS.ΝY§•BŸNJϊ ƒ\?Ÿ6y@γœj1[“γοΝ±D¨7 ‘EΔΙKc놡ΗΖb₯μ‘ΜλωϋΤΠ4cRd0Ϋ‰paβbzGBrž`2·ž ¬”υ•ΕΨΒ~€ύ’š +Q¦!‡·t’Ή‘³Δcβi 12έ²Γ™Αφ1t­8—'ΕE>]Ε†ΡΘχ1ΘBά uξX€2€ηUYBΣυI0J]ΟΩΖE§ΫIP'8aΤ€ +‚ΈL¨βΑrηNΧSΩo`Κ…jZΊ¬IΘυ`ύξσ>±¬ %” y(»³σ3‘AΆqΙJεΝYίήWϊ gΥl’›(>βυ;ΆΈΞθ}S™ωυΩ‹iDp%;JΓ<Υ£S ζ…― ξ^όέ2σΣxχ‚ύ«Žφ8WΥφφ”ίŒ(λ=+λv­rι¬c"')mnΜDE9„Q“Α·Ϋ«Lν—mhζM4K{²P_¦Pƒ:t?¬Ab μ­aκN‘W²A~ϊ•R-Λ[h/±2g$lΫ-Δ”±aδΙΗ4ΣΣvΘ›(Έ ›LΞ4{σšQΈ/BχΩήwΰVn‚KΡ,o<£;€ž5 °‹ΓiςΠ¬T8»A›Ϋ`­j’Ή₯”ύD“ζWY°ΒMžΘ˜’›vh7j.νM<Ω}“Η Σ)²‚όSΉPβΆ5PΐRAΎ|uuρ;2⎠RCO­=«#ΈςύTΥdφMυΌ+„^‹,E{™σάwt,ƒ­F@§ϊ©ŒbάM£χš˜΅€3:Ϋz₯]Hυ±šY娔›.>m*­αJα%2°O>R* Β +rh[ΘpT˜ΘδM‚@r>ι„‘νY^FRŒŒƒiBΫ›ν0)₯¦Žͺ₯]/ŒΧϊ°Π€+|O3°šΏ—&φt»χšί»ΩB›χΟ/§³‚]΄ΖƒŸΫ―6ax=Sψ­QcZ–¦kκ,ν½:Λ€τΣ4'¦Ν ΡAA[ΔΤ³ye`΄ΌΗ([ +UMΈBΧ―Ψ4šψηgΥ88ΤU(I hρ–Imϊ­΄ƒ,qb΄w>Ϊ³‡I‚Σ+ϚΜχPΜWšSοe΄YηIΔ'/DaΛtΔΎ83“yA ρ₯?γίgςsgLτІφ,-~bxƒΨzUκmΰΐ!άkθ“\b†m;…Acχ:ύH‰‘Kn +iΑΜΎ‘έθ6ΗμŠ›λΆΊgά₯Υ•œd’bͺ=Z€)/%eέ‡α—x1Χq% υjΜF½Rξ` •‚:“‘Τ%ˆά<ΌΩΈ[t2ZO‡3)-[•ζxLϋέƒw–s<γΪGΘ€ΕNŒΘXs~πd€uJ c#‘vcϊ“I >W^Ϊ!MΡΌguc„ψ²ι$ꚜy9΅φ5•ά»ε½Γƒή2˜/‚Ψ@›Θ₯eλ”τΌ±`—Jέδ0†·A.Gί³εmβdΗzώJή{ΐ|ί€NRmίnŏ?Π—Μ{G›k. +¨X₯λk•)Α  Ί<~δ) ‰4„9€_λhάΛύ0―s1Ɯ"vή—d' *Ή΅n‚©»¬‘GJBO:8νπ{dv£ΎlΖϋΎ;~‚ Σ™†+Eb’;|ήl,β半ͺdBHpy–:Xτιά ΧυAΎm»)W£"ΈέT=μ¦ΨΠ{sΣ¨šΡPXxοa“Ν +©ΊƒϋγNΦνχ Y@Q½bObΒΦώΠ ν&—7¬WXec ·v δς Γ,θw΅΅{¬HηpXοB¨„ε IΧ*tγ‘XΈ—¦ }-¬όηη“ΉUΎkqνΘ\’*]κw(ΌμγΜ(Y"H«νΡB»ώ„ΌΛ˜9€φέ₯ί#Μκ ψ¬lωϋ '΅ό’v–xύΨiBηΞ6"ŸD +CmλΘ8\‚Œοƒώ<ΡΐD°ί1οΚόό n#ͺrΤΓvMΫ*ˆ Sϊ–-Φot­#§’ρβ›Αw‡’Ώώ ΉB}³§rcIO½οQ² r8Ϊ‰`0š†ΛΊΗ *Q3&―ςa¬ΧΈξreΥw=θ#ޏ‘ž€i‰rS|!ΞdωXτvy¨AϋQπμ<Ζzΰ³άΜ *pΣYΑΕxΏD¦‡οtΆ„€nr¦Μ“¦΅˜γΥς²ˆJ―u- ”ό(1Ί"0%σŚ)““²ο’†,YQ\«Ξυ~λ‰.+ˆ“£[PH—ξœaڐ:MΉ>n½‘ζZδ"λ wŒΟθ΄ͺ§κ,βΖUPxΆiό"ΆΙ'“xΰΏΫέ(²ΥmίΎ¨υj+ΚHUΗ'9χ€»“ok8ͺΎƒL"Φ’Έϊ ĝΊ±¦žRΖξK8χΚά¦pZžϊj=&”u9‘6V|Ίq―²Gδ± G>·Β²xG@’εΕε_Τ:»YΒA^€Ω꘸aσΙH~ΤGΙ3ίrΠ'ζνZ˜Ί2+‘‘άΠUΣŝρxMs n΅«χΑ€žπhΆθ)₯¨8λ°λ¬gβšIAl€*yCγ9θ£)%.κδkί]C^‹«`ί *ŽφϊB4©‚0ωΆηŽδχ‚΅Ξ"wΕ­Ζϋ Zι15.βΔΰ6qxτΒͺzn&­ΣŏΙώ (+MW8’%e]7R2A/€ΓEδνπΥz=»†@Δχ• wΛόή&΄ͺG$ΒS<φωΈΎz7©ϊ<}"ήGbΣπΑ…*n(˜nδοXl9„Ήυ§΄’sΈ ڊQύϋήΛh`Π“σCΎΫx¬.l?έ»?Σ-ΦΝ- {C—¨SΘyβsWιpοdƒC¬Κ­¦±o΅λ΄σ~η? ›uƒ²…‘–¨φΉœP{u"O +œ&0±·+’‡6ύAζύΤΎ† rAΓBVΞ9:Lΰp4―›W ΕΉZζ9πSQ2k—hκΫͺΕοιΡ°τKμžσAxg4φFΗΜ² ΕƒžcY‘’z ψa¨Ιm‰ΞΫmOcnR5>§σ]¨ΠΥKi’Ή’ξ:Cmσ˜›΄HΣ7¬q*‹4fηO’Σ]Ε€αϊΨΩ ΟΌ<$NaλΒ“i„ξΝ: V›?Β΄­_7ΦMy¨;ιό2%Ι‘A‰Θ»Cki_χκ΅Έ±=€zφŒ ΏFΆ+hΙ<²ωwˊA)TϊΔ;©Œ“ΑώBnkΆ„«΄‚φ•‘Γq&λ›k{φ€ιTˆ=;UΰΠΞQ‡ΊQΊ΅Ύ©·8œVΏfϊίψ(Ω‚V,-kšQ„wϊ­xŸΐχup=ς»θfΖ ό+F~X‰ίTqΘ݌Πμ0f.-Κίݫќ Ά;O˜oLκ‹Φοrυ7S?…9χ,±Μ§Τ.έα°)™ΉάkιΧrY.Ώj+¦š•/ίvs+j‡ a|ΓΐπžΦŽ­H +Κ"**€βΟφlΜΒ‘UQˆ₯ ©kϋqΫ²ΞUωϋ/ΎxΛY*}g€/WŽh“V U +vN=³ΐ„σ[ƒκ³ςqqΝ œ‚λΣπ^πJ­T¬J‚τ4F ΆŠωqδκκ½cΘ­’Φ‘έVΌ΅4Vϋ `©—EΕΩgθ΅ΥΝͺ+₯»6±Ξκ ιh…γΊ6ȝαR d‡H-ͺzwzϊ H5aβ"?;—žΨτ&Gvhφg†4Ÿφ@Ι¦{Œ„“Φ°Ψ§k‘?Fwo€RπM΅AΡq@i²SY!ρ’9€.$­ιθΫXAΨΫαΡέσδ Έ9ίvolΆ"σKύ {ήΫ’h6ΩSˆΜτŽ’“Y`*ζ<>>zβΥ|½Σ؞s–αΈM‰>I³4 +4Tzy0‡©²½‘ϋr3ŸχUΠ#ΉξA‹λnšV‘Φ£S7cΗθ;\tεI Έ6b’Ϊ…™‘±0~FΑ€έžiΘD ι2h„¨e"υqΗSΊΎηό£f`ΊοvSŽv9ΰ ΨΜ½BdΝϊ»έ₯3C“gM7 ?@»™ζΙ]ΧFŸ€E˜@δFa(³£!ͺŽω_|ϋΩŒK, ηt‚ˆv(Άπ”I&˜’y +Ό-·dϋR}φ{ θ "±ΓΆ²ΟβJqK:μ—ιξfOΚ„OίBKHώ:[) δeΫ Ύ/%TGt:VήΣBΪϊΛ:ΑaŸθη©|₯^pp£]x·N¬θ‘:LaΌ‰γΝEmλΣ H ϊͺ§`›'μRΟΫζ w€7–OgLNλ–Mx2†eΝ‡½wμJΛSν1&·Y~†>W֌U›gQ™‚Ίk;?ήDΝu¬ό-π\uΙώΒς¬ή«g¦ϊ0ζ₯³g:K؍ͧΫ₯B|/=WΎtŸžϋY¦π!ΎrϊΕ₯σί:σΰ“r|”¬Σή4π.Ÿ‹œC­€Ω•Χaυ’g!Cνϊ+jlmG[q‘Ep2}$-ƒt>Γ0ΉΈΛ4ΥΎΦ›8П΄¬Vλ'ϋ,kΩΥ =‘Ξ}x,–«œ0«σ°8„ξΊyΐωVο 6ΤρΈnŸ%E’Ξͺ0Ξ&‰—‹ $TOΨ’τ@΄WuΣθ„ŽζGβΙ β[X9Ύ¬1Τg Bwξ~Δx‡WŽ{ŽϋθrϋžMΠψbEΧŝζυ7)ΐ{*ΕΛ\{ +OuΗ>›½ΐŽ#Ο|r§` +FN—:ΓWvŽλΙ‘nXYηύ·£N2b(7Kٚ°ώ.r}Μ±)}$%ΖΐFv˜Ÿ†hΓ= +΄Ά$5ΓΔ8Ρ0v ~VRΝσ”LΚ’τ ψŠΝΩ¨‰Ρp•+ζξ| m3L~Υ£μO}UδΖΕ»¨jΞά@bόŸλ=βm F'ΎМ‘ΐ"½^pφ’Ž^‹6­`Μ)›–Ό΄˜>Ξή”lΨAq²@Ρ;h‹―Η‘²MX΄3|ΗƒχΐΥo’’ΞΕ3έϊϋόNςQ42kGœΛ©,Β©Δo[ωX―S’ γ0πΌs/ EX@)§ι½ΊΈ„²!ΒΕWBΠ ϋυL\‘€nfά ΰφu$δYgρ=+žΦMgη~°$Ψ¬Œδnn-ύμW!Z_ΘvRQ±ŸΊ5OIΰ½~B³‡υ5i*@e€τcΣσ•3C9u9'ύΧ]w9αA§Atν@YšΑσždH=ΰ4QΈ#1œ  “’9•,‡œ>e"’“‘½%EcH=]=ΑŽ«¬ r[a19pŠ\†‘ yAγ#Τ°ΖvZ·΄­έ@@*γdί%Œ±ƒ›BΆeXΗRMZΥ?%š)ΉΫώ`J\1ΫNλιfjϋ»"΅ΊΎ΅J$1ΰlQy” T‘xG%Gβ0fX’e)δύa ›WΛ S‰‡vςωΩ²ΎΣMf“9ΌX] )η‹ mzRϋ΄OψΒrό?Δrgΰ³tΥbCTεS§ v°UšΔ%}ώ>AξBϊhαΫΜΑ½,)δ›H΄8 ψΌfαP—4/Ζnψ3ΐΦ}‡•Με₯’fωiΪΐOf§ΥX+ΒΟmΒΓ]c©ΑωΆw ι/±ΰ’‡PαΩΗQa$ΔΗu !γwΛ€c–@z>%ωaQY…&Λ,„π=¨ ζWρŠ3`dXδEΟO‡ΕLάNͺΛ?§ϊ_Y~ŸvΈWΪαΌ ΥpφΠϋ™κ«˜Bt[p ϋ,! /σVΣ^…gTrΧΒ±{ˆ A“Bα!±€~·ιwύμσ$“θˆ%>€αΝ*γ£ΡŠ»™e.@·Ι™βό¬y0ΐkžU%v ~•y,}€―ΝFfUγO^:ˆς5sΟ7(θΝ…‚­ρΫu“VΩοjΎ™…#fα“·€—Kωœσ'&twz2ΌΊ1αψ%}^½oi}γu–λωlΑω-z‘Q˜P}&Δ3Χcό΄{g‘Šζ±‘ΫΊ;7vm·}έ>έπEΪbÞ7χ3JuS>9¦o¬‚SΨ%1Ί«»t² SυΐyI+’¨ό:/=ΐ™n!uynπ·• ΜN’ΙΧ‹`[xtζh6Ι§ͺ·ͺ]{]t’ασg?™ϋυθτΦδ,ŒŽϋλΙκQυΒ6)°`£βmœKΈ2ΐu7?i?Wί Ρy{cν+& upΗ ‡£2EFΚ0Κζ:"Γ…χ±λ{H!-Ωf!ρaα…χα΅t¨κΓ<.rT΅NƒδΪ”#›EQ<…•#Dϋt‹ψ‘“eΩ +·Ό=½#ˍV εFρ…Ÿk°Ύθ£Η£έE·°Ÿ¨Ρ/ ½H'άΏΗ,€‚½ +"χMƒω²Η=Ε€°.ύ™Y X.KΤ¦KϊΊG·θ»Mr9/’qΎ¨‰>MKo €¨n²«ΓΟώnjφwμnύd#UΒcΗΘ\p}!Cž +JΥΘAZuqζ‹Tϊ`6α±<!O¦ΰΥs ωυ˜β˜˜ +O39€’Σ‘²ζυ pEν&—j8¦*τŽ5ΆUΨ•΅§Dπ·L 64g`ΫΘ’…φƒ…ήΔχ Ϊ·ΜBί†»©Β Σm + κ +ε‡<έMhš'^nfz¦Νϊ―tD«uM6.aδ);M’ΥΝRpEœ₯ͺΘ½{oΫλWΖѐM—ΣtιYŽšOI(B#k,^βΝ5‘Λ››ς‡ΩOϊ˜ΠZωA&ŽδvV\„­δηbÝ΄χ fφyZpΝaΘΟz¨ήφϋ„έ– hD*ΛΠΕŽ+;°ΐo2.Εκ9«΅‡₯roŠ4΅’cή,Ξ&¬―Ά±'šεSVΝΣθb‹ό=Β j2.#`mγI.αψ₯–6χ«:Ωs&°χ »ΰqζ U…Πn}n!+”«τλΟ\φR†)/ο«›i+_ΰψΉί°ώz•nΙV­z“‡ώρ«S$τ.>²;»ΨT\Žα ™γθyΏ&"“tώ—Š$[oA(ΨσW`όΚ’0ΩΖ=μς&»\κ^=މ*)δ1W8υڐZΏ +-™Qz5ΌE:rΊΎόΡ+’QΦα\ώς(HŽr']½PΫ/ήg€ο SL±ŠWŠηZ a­TΑ€‚Πb(Da§όη5μΖ~&R§ύΕe›σ©Ι.Dˆ•Ύo\ͺρp;@ωαs)Ζ==Δ€Όβε˜„ύOΊš—ΐL{¬«s𑁳΅qoaρΟΛΈίjRj‰›ηϊRžΆ f#Έ,U–‘’,l·$ŒIVΟlpΣ?­ŽΦœŽDΆVIΪš±g”y@VPw&μσΗP-Ž Φdΐ:ƒ”tωδφ9!nn³ϋ~9HYI’5PΉϊώα,8%ς/kΡ™ύωώΦͺΟeU&φœό‚±_Α˜id4x+I]ΑFW=})žŽY½€\’ω’Ρ™“ξϋS·Ε΅{J$ff$τι 5εΗ"-\.–DΑ§­Exπ¬—2ΰφ&ζVS@δ:™f]δ…Πλκ$Λi|΅ +F`*3,ΕΣΎίψΙhcθeΊ4ιo€NjπΕ+1ψ%\¦!ζŠΚ|ω¦ΎQω¨ΫmΕ:S²‘΄˜K-nΉ?νEυLˆ“o/λ`―IHΊσ‹8y§ΣϋΆ.’y‹ΧbM₯WΡEYg0l9­›Υ†x +°–=εεΠqΒ#”OΎ Ή_ŒMΗθJΰĐ?>l( φιΖςB«Ά½Κλԟΰ†Θ}γ£_€Ή±[œ?©ς#x7±i’Ξ³Œ +GijΗϋ…―zςK€ΦΒΨ€ŸqθςΒ¦—Yψu˜HΦ\TZ6/4{~*'$λCJn|SSξPΩsή/Χ•ζ`-ΎΫm%H―±Ps_2}νKτ«QnM@#Κό:άB)JEόu^£DaS˜\nγΏe_’NΗΚh̚‰ή ΆoΥ[’02ΣGσΐw¬°@δ1ΰGVΑ—l=” ―†’9½YQ$Ί΅–"ΆR‰ωΘ8Ψ• u― kν[mOݞsOΓoιμοŽΨ˜πœ uˊεQhq”ΖVΣυδe.₯ •$LXΗVŒ΅πِΫJ<«ƒ8i\Ι¨LwΌ΄ωΪ,) κό}Ι>ΈhlΩuvίgt²LdMΉ8<έn‰#&Δ™οπ…ΑƒˆŒ^κF™f;Αωœχή½79.ΕdΚ‚(φγ€ΌΙ«z7θuzΘξEšόΡ/ΟΊ¨VΒ/@r‡ˆπ9RπJΗ-)ά6§νΌε‹rμ0κ?Ue/ζψεK`«Z(™`Ξμ¨' ωv°]“‘Uw’}1μ₯μN#Ο'_ieF"f +†uΖ$:…»₯¨a.Ο•#AD/"™είX―Ψ1ͺ₯;OΤXΛbb%ylμ‚λqu§ΨΫͺ} +6ϋΑ²Iφχ.ΐν.Β<›Δ;}νέΐ!Άs Vm 5HiWœΰ±νŠg<ΰO£?ό)‘0$Ϊ«F¦₯yΥ(xΦn³ƒΒϋ&1Π© M‰Pέgkœ)&˜)`ΛπR Iω¬»†_Uόj*«Ϊ5j +Ξ½N gRO€Kƒ "›MάΙZϊΑΦώH«ΠΦΒ^ΘwαNž3―ΦW›P€G€ώΫ• +ΌrΌo+―/–Ί’πvœ<’Ώκ³―ΆΏύο΄_GŒςSOE6Za–Ρ/žΔC›s*aZ{)αξ32*ύ Q25½(–c†ΖΔ‡~PrY₯3βΉ’ŸΟ|Ή0Ώ™%z¦MΩYvΙ}CΨH³DGy>a§ρŸ₯ιh±υΛΝ +φž¨«·σ=<Ή2Χε(sNW ωγ˜δm:#€ΦnJ&D€\,=Y‘π/υ` `ϋδΡI*"}⟷†7!rv@δ―I[Ί2i‹UΤλ+ ’M σ©ά.A&Φ^3BΆζJ’ω*…ͺh k’[΄Ψ†œVgΏ2Iλrƒ±ωΟ2(ΝIyŸ§Ϊ"ŸVJ²FjΐΙ―G‹…ω _ύ…±ΣΩ’¬@F%™Ι> ŠΈZϋ|σ$τ¬7©²°₯Žλ>ή!/ψυb”ώΫxlΦό΄δ»ΟbδλλXΧ~Yˆ³“πrΎ_²ώ\`bγh‚xΗƒΆγY―MΦ·rϊ…Α_ƒ€›6ΎdιΝΦTχ‚―»΄‘EρΎ*[œE[FνωπY΅g^TΗmu1fnΥ’«Ξ{5 ΅kr’{tW@…F’Ϊ‹τόgkEΖBžΥΐωόf°NΨκ?c°9`Πζ₯Ge[ "ȌԨ5ψd;‘ζˆΧM,œB­$]Ξ"λ˜λGVΗ/~F”·1Cδu‰²hjFϊ•e€Σz©wΕΔ¦FšMx&βy^£— ȌBΧkτCWΧ—m₯"mκμ΅ΠΤN™γT<Ę\ϋΞθ₯‚žš;"ϋjG'ρYfkE«Kj(ω±‘ƒήΑ­ΌΘ²Ήμm8žς}.iG”R “|#Ϋ - ˆC%ύoφλlκj€(³³ΟgI4rw“QχtΑ_θ:;;p Η»Œ~₯΄δgx[ί93@ήξ‰0wn’­Κz8ϋ†`Ή +n…eprοϊλœkαm£…|crK’ρۜ’ƒœΊ—iy:ΐπœ²U9)ν¬RΖeέ ;ω!]“Œž2„:υ-–’kσD{ƒ‰|"@qZ’pο†βΨ?wj©7Ž%Β‚ι=ψΊŽ½\c―ά&π>ύϋ§žgΠ@ΥjWΝ{ά‡Nθ}ϋTΜψaύΰQ μΤΪ W½λJ}/6}ροΖβ<©ί§ΐΉΡ²³½ιόΗ7ξΓήΔ σby³)7€υ09$ΨA{Δ “ΔnΔ§\"0"ΩCήHFκjξπ;ΣLuΨ•ν?`;•ρέΐ(DΖh6έ‰^ +3ζτ΄SΜ9q7ς'(›”ΎWANΣ pΜOΙ¦±aBYzyΘ_€pŒ†λŠϋzΏ,O›ι½w§π ™aζ©J-¦Ϊ{Y·ž ₯^!°vΚJΗ΄1,ΑΞ©β |ϋΔ]F[g˜6L­kƒΞŒFˆ7f³₯Fl«-όe^ΙΧj*‰S½ω›[’Ηφ[jώ#Ž4τKι²Η¦~ζφpΟ@]³Ή Ηt;Φ©ΝwΥ1^‘Ÿ‘ο·]];Ωΐ‰²‰‚’ηŒiΜFΉΖο±R.ΪΕͺ½ΞΪ5_Oίh—œ‘‚+Γ\άL˜m”€†+κΝό3αΰ’AΘZŽ{>Ϊ6.’†h "’΄Ό”x‡’ϊ#8ι„2—΅«ƒΔ–ƒUκόΏ:O δuΠΝWSΠ]μχυ7ρΊƒGm, +Ρά‘γϊͺJP€k¦ jjBϋΔ“ΛΥJ˜[μœ 2υwμίψ5ΌͺD΄ξbn²h“4¦_fΥσηκΖ9Σ›­Χ‚HWήτβΒ‡;λa‚™‰ŽΆ¨lΟτ-Χ#SιΏ+ː-J@ΰ©ς™šΑ·ς]φa|R‰wΊ™‹/HXψ'Ύ£…kt΅Bcβ¬/Η τσε­½8 jN~2Z|ΡΒ„’0ΠΫcxJ\ΧxΥΆh-dΈΦ/ψ\žΘžλΉϊŸ< Ρ% +endstream +endobj +858 0 obj +<< /Filter /FlateDecode /Length1 1360 /Length2 27857 /Length3 0 /Length 28815 >> +stream +xڌwP$Κ²%ξξ:4ξξξξξNγξ:ΐ0Έ»»»»; ΛΐΰΎsο}Ώ}7b7:’»*Oζ©ΜΚ“ΡδΔJͺτΒf&@ {Wzf&€¨Ό +€‰‰•‰‰Žœ\ΝΚΥψŽ\θμbε`ΟσΏα’Ξ@cΧ?61cΧ?nςφ7[3+€™ƒ‡™“‡‰ ΐΒΔΔύ_ŽΞ<1cw+3€<@ΖΑθG.κΰθεleaιϊη”Z¨L©Μάܜt‡„ν€ΞV¦ΖφycWK έŸMmͺ¦V@W―  β³tuuδadτππ`0ΆsappΆ ¦xXΉZT€.@gw ΰ―r +ΖvΐΏ c€#¨YZΉόcVu0wυ0vώl­Lφ.άμΝ€Ξ€?gT₯劎@ϋœεώq όλjΜ ΜMχ―迈¬μ665u°s4Άχ²²·˜[ΩŠr žtc{³Ώm]ώΔ»[Ω›όqψ;qc€„°2ΐψO}ͺΞΕΤΩΚΡΥ…ΑΕΚφ― +’ωsΙβφf’vv@{WΈΏς³ršώΉu/ΖΏΫjcοΰaοσΟΪάΚήΜό―ΜάΥν­œά€bςψc‚ϋ·Νθ +`gbbβδf@OSKΖΏΘΥΌƒ›δοηγθΰ0SΠΟΚψηΞΗΕΨpuvϊωόοΐξΰ˜™fV¦ …•=ܿ٘ζμtήΩΚ ΛτGxΜ¦Ώ>½£-3{[―»έ\F)i%m)ΪΏ ώoHDΔΑΰCΟΚ gag03q28,όώ“EΙΨκ_Y0ύ;VΪήάΐύO²nιΏvWχ©ώ5Τ€δRpψ£X €κίΧcbg2ύσΕό-σΏCώoκώ‹ε!𙏄›­νί(Υ_πΫYΩzύ £W7Χ?Ϊ—wψ3φΣUψΟΈΚΝ¬άμώ'*νjόg„ν-lϋ­\$¬<fJV¦–Hε»ϊ_fkeTrp±ϊλAΠ331ύμΟT™Ϊόy4\ώθρoψghώσHq{S³Ώ¦‹…`μμlμχ§Εvμζ?chτό[ΑF{Χ?!€?εωΜœαώκ'ΣΨαώƒΫΤΝΩωΟdύέϋ?Χώο1=¦p«Λ¦Όί­Ύw=Υ γ{ΠNσ/j¦QΣϋ¬:w»½ A'SΧf}Ϋv~N@ΩΨ§ΊZ#zχ9oo‚νHTξ|υ}3ŒW™;μ„ϋ9‹52St.ά8LK@―&tδϋξδ«hήΪ+CžηδΖ…€T€ώδ1$ιΩ8\±>²|¨|TΛ! V1O­₯XΊHžo’½„CεJOCƒvν‰Όx°€–;σI$O ηwΝZμ£³ΓσΌδ½Y₯Ζβ‡K†«ƒC~69Gα#r’"ƒ½βSVΌν<™Ί„Υ=΅k—Δl{Bε}¨ ςδ|ϊΫ€‚|†‡ +‡C:2j»C*Α”8Š-ϋzCfΫSϊ7g»‰ b‚Υ§’-g’αΠ­Ε\cZ· οQς€Rj@vjS[΅Ÿ+šg]Ÿ΄»Lλ6HΞŏJ}¨ίp―ό—_+Zˆ1³₯φœmy₯`y&©<γ••Ζ_“>ερNE«>ISΊ˜lαw|υOΕ‡ςUκwLΖR0UΔl«©ž*½Ώ―©ΣΩ$‘V³Œ/™1EEφKΑ‘RΩχJ¦λs‘¦•ΪŽδέ· Α<Κκ& $Wˆξδε’4Ή]ƒ‘„BΦLξK45ΫA4°Ί «n»ξΈ‹³Β3F>$Ήδ΅mνIPΫςψ}ω¬e=Π‚‹‡D$”w/#—TΈξYΖ¦›/ιfόj4ώH’y.‰§ΘΒΚ΅gˆ’Χ—–όfzΠmΞy°P2ω›ee +/~­ΠO €τq<‚X>(t K቉Aˆ΅έˆΖe χsΥΗΒΈ‰`[.`tΙƒς+ΏbϋŽ©Ε“^ύ™{VΜ +ωΜάQ©fU΅k@Uk] RΧζό!*ΦbTχΚ5«π½^έW0ώϊΗψœ9KD[ΞΔ ηγC8i€η₯:ΘΙ`·]GLχ½‘u±ιΈM™O”JΖjp„•ψ±zJηRώ4J.ίEΧ^Λƒ6E}ΪzAœΓ>·‹@βσΔ`gO|Η!\-—u7τΒϊ©h*ΉCηΒ[eL†’=ώo ^±” όœY–x*Ϊ ‰[ίΥΉ~Ι¬Δ“₯Λ¬sŒξ΅†ε Œ[ Wκ{γ7-½ «G;·fά'˜_φΚΩ: υςά{i·° ­ψcj‘Oέλ]Νρ₯Οuε΅-&όΖπ-™…­μؘcή₯,!τ‹jQ1@:i΅EΆΥ¨·#|λθLηΑžT¦‡<αh–¦mΟμVδh¦{5qikΣIl«rqΰώ; ’-ΚΠ3­λΔ𠔆ξ―W¨D)χœGΩΪWg=ϋό7ς+Γ† DψpώψŒδΕμ±Ρσξ4rϊ•υΦήF½ep°idW‘»‰Ρ!ΎφPQη¦7œ+οΒ£ƒD±Ήΰ4*τ _νŒψ ˆ x@pΟ½?κmΪ ‘ΙIπl“š"¨Ύ”^@†πdζΣ\ΤόόTβ½|Lr°C%Q§\GOdψόW·ρ/ϊA†sskLj ά³ γB ­_2π#Gž@Gr€Ιπu 6΄ι€B"Њ{†₯ΧX2V¨Οσή:Κk°{‹›†ΏzGzFqτ–ψΫ%cAk=HhD$=ˆŠ+zΚ„ΌΐYKΨ•|·€ΖΥοΟ'Xš δj %Œ\;MρΩwƒDŒ 6-€9δ-› +™n‚ΨjT}ψ>N£ + ΝΡ£XŸ ›οq7χhΌ™άε7Β’q©½›5Xγ°Ÿjtu€ξ#tάuθ€%σ!‚Lp:`›—šΓ’EPΟξŸλΆMθ_O―FVΓκtٝεBύœ`Τέν¬E—+Mδ‡Πx,rv/ϋC`‡4Ο1¬¦ŽΧ*ΏT“ϊΞyΩ=³MΡΣMάdΑ›K/?δ'_`0>ς• ‰ ―φ’yΆ'ξVu³ρr'a―_σ†[.έΐ§dΓܞωμ‹H±\Φ1Φ·ΠΪ‘‚cΚjC±Π΅*“—‡ΐf7x«ϋ³O7qΜwZŸ0 ύ`{Ž[¦e,ΊύΛΨύςS5~Εοά~C8ϋέ^ŸΣ.eαVH{πςvI‹Di©”(}hqΔη:€ΫxaμΟΦΓ€.6ͺP˜ ½}΅"m"hΎήΖΌΆ”A…vΑ8ωά1„KΔSΚΊ†-τjv’z„Oμ-΅ι†ΛΈˆ¨Ÿ nΒ +Ψ±l5sBo'ΕnGFΪΫ‹Xc?„?wnν ‡œv«ϊbk$'qΞΠUΩ5€ν‹—ž΅qp $/Ό•qJ…ΰ §.αtηξͺ}φgkς’5& +ι•Τ·l§sαΝ-rx₯?Ί·ϋ“ HΧE–ΔΌfδ.δΰΔ7ι†ΩLK–ϋ}Oαπό⑐ ω_Žλ0Z¦<1Md―XήΐΊ"^ ― 4!Τ…58k<•Ώ1I―Δe&ΝΩAUXJ¬3·F‘šν…ΐ$3€)0Ν|;’i³u0ΌΔμ΅Χˆ½uU‰6θΡ-j yyTQUŸ3ιΔ,Ρ―lžΗ ±3ά™Μ­—όλLh +σ˜lνj8Œ Ε *ƒ@œbΆρΌ­ž +Α\Ζ;|rίΰ |ferc Ψu+’ + ‰e*6Δψ{ΦhPΑ•6rˆN[ώέ»AΠœE°Ω ¬:χγΎoώέ‚ΖL·š!Φ&™afV0?sϊ«7ϊ΄δŠδηθ #X8ICKI­Δγ½dY#M|t-φΟδUν‡žmcŠ8kΡMœΐ±Ϊαb·Kœ{Ωn£ŠΊ ·Θ΅98γC–Ϋ‹‡±ΈktN€Τ–8ΐοΫm\%όξ{N;hΤ[°υΟА¬0¬RJ Šε!=r4ΓKŒ2ήhK(ωb‡‘ŸU¦΅±Θ3Þ/TDΧV}8πm˜Ο +“mn{Ι?Ύ¨]έΓΔρ—vp4Σ>ŠΘ ’u Μγ¦/—/R^„t]–m@όΩld:―…kT+fμ΅eτHμΖπ―Ό}˜σ!ρ£“š? ³iΐ!Γdήd―V|!¨z +μ{’ΟW%±‚FΆέ(ΉƒΤAQΌΎξ!ςH8ΠO}ƒ9z9Hͺͺ0β…HrΧ6' οaCs +9ψΡBF (σͺQάχΐΒ‰ϋ¦”›Ί±Ή™H¬Τ–dΣ¬LM«ξψQ{„»ΠОξ+Bšϋ2po *ͺœιΑΧ +πt£« *T¨ΞŠyλшTfΡρŒ²mi΄μ8 ΧWηPk0ΣTM§žGΧA]i+">'³έ…Bm«”Œ¦¦ Lζ‘ Z‘Šƒ #Ώ’y{Z›‘vΨ‹ΩLΔYΪ7—ZͺWPρκδ1fΪωΔΛ―u䇔•±“ >#* ˆ4σδbΥΈtӍ=κgh«J#ύ‘ψ%ΦPI<“›qζx°Ί6Ώ|Œ‚4'ήV:ρ9Œ)f"±±NΝ]?άI$°”Έ~ωb$Π‚{UμY§ερ—ΗB}τ0Ώƒ~`έύβ άτ^ː–‰€Œυ‘Άm ³δύeGZ­άφ…Θ̌ ύ•Ά‚hYΤ4°β^gi.rΡ—ΉWdHŸχΉe7Q-μK]ǜ_Oδf`ΧΩ­Ν]\n 1΄’Α8ϋ)"oIΗφΘβτD|TΊύˆ§Π-ΨΒϊX—yέ^pΨ(‚XΡU“”4ΪB«#ΞΜΪρ¨§Κ5{T7Ώμά¨šλ¨+kΫ.Œ€$±fώqCπΏπΥφ ζVΑ=ξςqgφωλΪD>U―f=υq]π +₯ž>%ο˜VΥΌy©γΐ«Θνξ"¦;Οί8΅‡!άι±υ¨Uε΅‚†7™’}Ρί%mvΧFΏΩ]η€±/θz±ψΔ-½u]w]°ΚAŸ˜ΈŠΆUaa‡C&EŠc΅θ³›Τ?m€Φ6LœxΆΏP8}—ΏΓΟ$'BΫgm(ρ-φΫTίE’¨ž<°DΛz„Ί/> σJoΝ―ς·|ΔJ–ω­ ­Γ­^ΟΛ₯ΜP  ΖMΠ»"ΘΌg1@U§ϊmΝ}/χwEΘ²A¨υ!³Π Nz«u«»OψΩτ~Χ‡ŒŠ|„o³°“ό’ -Ϊ’Νͺ–ϋ§9΅+'Ψπ€/Ψ4₯―³€Λε#5οτ$ ύ²-ΗΞ–—μυzΪ«M“z«κL; }g]Aή=2Ά»όΊr2θμt˜+S—ΨΧπ­Ή?πιΗ#ξ!τ£ΤEjM Ρͺͺoς=₯1&1§ξΓ²Eƒˆ»,w›ΟΧ©ZJρ$lΫƒ‡’νΝgn‹{Ud +ΏΛI ΉLvgf?­“μNAή©ΡΪmB%Eq6rΓ…)ˆ@e―pt@ -s7hΠυΏ­ΐ{ή©i=Υ4[Yq‘ͺδΨPΓEΚ$‹Π[·—δb·εαU‘1™˜!σGν^—ΔaΌLX₯ΦF εD­ΙgTΟά {of°Δ₯ILΌ±±)ŒΊ C‘·€N{f1ϊي}PόήτΤ•Ÿ•-f„O%šΆΎφ0rK °7ϋ±βRσ™z_m)ρ?‘ζΏ'L`΄οήόš>Œs{έηP/– Ώω_IֈτΨ·ΖΘΚO†gΏ)ΎFeοή6!sγΟ5-TΨ½Ύ n¦»η ΅ι|Q6]ΡφΩy₯±}αqΜ™ΗΎ>…qκT7ςš‚cفΙψŠK!4Υ€ΰΚ•χρΔ>]»vοpήί«M[!;²ž!18μμ=ΔΨκ­bΰ¬οM—c;ΘΝ:@ΡoƒP‰ύιυ:DΡV₯—ΪδN[θu†ήqξ&Κς.]“‡tυ5χΟ"(=Α…‰„ΚρPΉ«τ!DEΉο΄TΥ‰Ω3|²„ξψ”Ω|Ώ. TžΤηΪςξE>n6n~L05ήyI eDΔ›_·Κ…ά•Šs ςMΣnξ{dεe0Σj)ά†hL˝φ©I jeαΊΩp8οX”u'…ΒbΉ:ρ?£ΛσΧ~ΎΈ³}΅δ4Ά–ό,χ]N )š(\J―{7αϊιΛy:θrfˆžŒ6*ό{΄™ΕE@θω§M ™ύαβt(Ύ}υj―Ρ+FδL—™¬˜‚ypQν?ΣHi'Gβ}#χvΝδΒΗύΤΪΎ–d’ϊ§j­Fwye…Dι΅ϊ̐FΤ]wΏ3Δ3PzΛ΅4γ‡VΕΤ$Κ]N/ˆ‘Ιnτύ‘}#aφΒΘNdλ:‘/ξ(₯’Ι»ΑŒΜ-Ί#YEλϋOK$ͺŸƒ V!¦σiΘ8πώ«xˆυ,0¬#›T_Μ¨LFλ†μq‰€HœγΥΛ5J#ˆΡνΔ-γνΙP-ή«,―dOm‰;ZώύAH₯μL•PrngΨ±jžQ/h’Ωr ΟΘ©–ξ)Ή –)Γmθ}ΏΔϊ+7Υ¦kƒg™}·Φ΄Ξύ’q£@oτΝLUΘφκ .aΨΖ=MΝc0ά{m>bEΫ₯χοq4/N¦±C\gˆώ/€„‹>ƒ)’7§¦φ_ wΥ΅)0zW6:›E±γΌVΣ³¬Υ^9ν7uΗ’kTŽό( Fe^Ί› Ηx‘Α?ˆf>j€ΩFωw}―›)ηAbyp8Ո㲬Ηΰs\Ξ!ί³OJΓ2:s>2ξΊ^αm„ΰ\Ν &τVυΉ%& ŽŸΩm=¦(«!y΅67ΖwHωζ5κomN…½ώΤα•Ιψ³W4§nί“ξ†Γ§Ÿξρq΄IU\(šΣCQŠHdραΧς^ό[‰―%±­#m­όcςΑoά]ρOŠ5ΝάVΖ)9Q­Ϊ/e9ΞΣΧ‘£!ΧZΔμ6mA4}Γ0δ&ΛΛ<6Ϋ‚(Ύ5ΌγΘGzrϊy#ž”€[}ο±BHͺjl¦’ϋΤnΤ@9|νUνύœB+ϊiψMαΫΣ΅†Bα)Φ§O’1>—3ΗRΔvΓΜΰ †ιφβ p…+ώ}Ω +» aήR{’ŒΒέβ­7!}G€¨zΆvΊηΗ~t]7rΉ]Oώ0¬ΘΫϋMοm8€KΦυZ‘AΣ¨πŠ– +LTΠVΙOgτ΅ΥVςΝMύ7Mόλ€ΘAΨU9uρΟψ/4%όSCp <—;’₯jΰφJ` §/ŽkΫ7’σλތ&ˆ‚’x•“œM\‹Ή¨ΑHοόŽqh8!Ε¦~5H cΠcRZK…Q$ΆFBŸΠyΥRΧ›[€ςΩδ)ΤМγ#W“&C―oX˜=*ώκ ³Γ&³μ <ϊΐ‰ΛKβ7²A‚“SκNŒΟΣκe©΅΄ςς1ΙχHYzό)Sκ ΊηδAΦΰ„?·όYYψ)pHΖy;mtά_ΛqLδπeδΎςΌ¦ΚρρyŽV³°?_‡ƒΔ|ξIA)7=‰; EQέχ/ιžΖ驍·$κγm•Œ‹ )ψΝ…Ιοά§Bu©ά—ΪΚΫWŸ_v„iβs₯3„Ί6+€ΘUQΨα|‹hi–Ο곃ζz©τ~*Χ%½*ƒθδ˜ϋ‚1υ½5|™€ΗXW zι=ˆγΨ@,‹Ρ§ΚͺπI9Ω£`ΈΑ4;·q<θ,ncΑ―†?αιaρ¬ΘZsθG'σΊY½>§,ΑΩjΘυΤ問Ν06VΡMΉ$hU9ΈπΝKlσ–+MωZ~g¦lŊ’Iγ›β^Ο/dƒŽeͺ6Δσ²~[_Ω₯V"r5aΝsXΊ[“6fi‘ꨟ«z”λhjΪMzejΡX'…n Ž˜°L2λ ETΜwIƒ~}€°ώgBo0•΄Ξ˜λ?j(χέϊ­ 7\Ίζ^Έl͚yυυ‹ε ~ά(kυYίδΪzEΤΚ&Δ­uϊR ΥgtŽU%ΥnΏ\BŸaxžΥ‘`Ω<’Κk3­9wκΥ&„qΗ£(Goy|ΐsnv₯ί“Ίfαo&Ώ₯Κέο1P>7£ς9V‰‘cTΙ +#χ2Ιϊ ―alΈ.°§ΒTmφp}©6 ͺ™ΓAσk5΅šxX#²aσΖ{`LΘΒΤ£ε£*6Z6―μξ€@yK±8χ§α Υ[hχjτ‡²ΥιMΐgΒΤ8zIYΛw©ΝuLτW¦„φqq6Wt-CL»Ψ³¬|θ ‘SτJT„κžςπ§ =ψΆ5H&n%ϋ‡―”‡¦bδΜ[žΧ™΄„Ψτϊ\Lv§±οVϊ˜ˆ/ςN_”Ο·ςXΫQ?Α tˆ…hΟsς#Kι¬#"άδzμ–‰Λ„Θ9%KŠ;Xœ*ŸΚ­– Q;!'ιjM ¨šZ™χ‰Yή~½€€Δtυm)ς¬₯·ΪLη£ξλŠΕι:h5ΡPλΙΒκ«?m‘b€τCJCdΘπηhCŒΦΠ9ϋ5ϋN7ά ?\¨ύCό[}s†smŒσͺ^[ΰ’%R”ˆTΧ™ΈšΤ°π€Ψ"Zž©k+™β| φmYͺy™ΌΣR₯½(Φ΅Έ²/lk!gU‹7GΆζaŠι +άπwτ–'¨Uϋ g”Χ7Ρ \Δϊ›šυΰ"YIkΓmAέα…gÚgJ€€X}p9έWΉ gΆ'³•θ"κ™{tΡ³‡εrΫ iy©yΜnyΛ0Β²ΙΦVι6Σ>ρI ςS{f"(νΉ‹ϋ\ρvCgΣ‡ϊί…eV0¬ ”>ΰ Ύeρb¬ϊ Bk© mžΧX4’½/ζ4{ΗϋΆ*½'ΟU’P֓˝w‡³eaœ·φuc₯ύήc9†Ek:B&―&λΐΖ «.w$”o$Χ'MDί +Š9δU]ψ΄©ύΦ¬ όΏnς"©9c¦cIαμtΘ<^θ†U²Ά;=εσ΅ΥrκœQφVBCf0X$Π2MΑλD~™ QFυr„vΈκ@π]g —{/σΈy6]‹rQk) ™ΑΊ[kψ͟¬ƒ£E‘ς:Ώ<¨”z/MwΔ@Ζ¦ Í1€Υΰm ’ά+‘H25ŠηΚυl±η7Ήœe +ΐΦΊΘ@pXD^'[jB’°(P)TρΆ'’‰WΑfZλŒΌaNR@δœmŠ’…!šγžt½υdI~8pG\ξͺw&4T`ω,¨DέΖ;γΞ©ƒ[σρ»vΰyGΛ¦Ιψ鏑σ¦ŸD+bA*yψE`0žΓΏPΎ‚>g³`8QVΕ +M†?oTY3gn8‘2ΰm_7λ▏ϊ‡ΘοΆΙD.Υ”ΰ,m]%‘‡υ|Γ“0ˆ£ηŸm…οrBMz1Ύ8·ΰQΎU»ξm₯%εΉ${W™ΛΦP˜o„6ΖJ‰W­"¨ζ“ό Βρ¦1™d,'Ώ5T)ΦozH7y.KRΈ¦EiΘ{)π\«Œ•1?€NαθφΎ p7λˆu,΄ν~Η•ύ]I3ΩσΓ4;¦~Ώr8ΚIχ YžkNr„}ΙYˆ’‡<8‘ΟAτŸ ±½; +mq­­}Βή'ߞJ­μ†ν‘θƒmΓ]£|⍾œ…4ΖXΥKΖ‚?,,pκ±υ1o[1v‰ehK! $m…Q)ωηΗ«I·8β r1b+ σ›ρΑν'νY;D4ηΆ ew(ϊφ6wNS¦ƒ΄’^… T~ή47uνΞdžE᨞θEr&Tϋΰ7ΚΓ6Yψ>8%ž/0iϊΥoΤe¦&kΦ)˜2ρΩVMsΣλ•H½ΦIΚ ^wGmΖΣ*;10"§μΡxD—~£|@’χ9’©ŠξIΏΆ”¦m‘-a3Υ2Δ€ΨΞ“ςχ13,ςZ =p —*¦VŒΗ^”­*8)έ·­E&œ[$v―O\k“Ά­«Πš@.U’d*ͺx”*‡/­Φλš ™βuΉέΝΉ4bm«Ρ04Ψ3 +¨‘λ ŽΓ’†/₯$iΫ=£‘ƒ‹΅vΆ•†ίAρ^©‹―«σL)œžδy]Ής˜¨ΜXΨjιΤ¬))‘λ“xϊj5Έ O]2Lέ6‹άβύa0lϋK]ϋ–w8δ Βn`ΐiUŊ1ί]„aπ΄eΨΊ‚«ΫxOe˜DΡx:Ν„—-<³m„ .τ |Nμϋ ‘N= “ΣΝ1αδps§φC¦žA-Bψž`ύ)ϋ=BΡΦϋK2ΡE«Ο!(Z1σόƒθ„΅σΓ:xψΑΔ¦PΚδ¬Υ46i£„QσϊτΤiΌ\κjŠθ3αŽ)‹†(” +£Φd™- žΞβ ΓΌή=τ‹ΰh αI|ρN$lΥτA0&Η<ƒ|8'.ΑJΡ5Žt>[t BjΰΛ„‡ξ«h~ήͺδ3giβI'ο Ε1νŽ/4φω²dΏψ°{`S$.YΎ΅Xτo7^¨RnjΠ΄θ > +ΤJnώV“Δυg‰)‰₯OΜχηΜ’ Ηι‚mέhχƒ[₯›5εœak·Ϋ•5a-ΦΏσ$NΈλBΌœΩ[ϊΕ1]‘f˜έy6Oψ@y#ήorφŠ€™GXέΖh`‡ΆκΌ&ύξη;€ξ†­lZύPέθβΫζΐ<‰ͺo #ζ?UŒΨ¬k”Έ’© «@0wLρ·ε“XκL{₯_΅/hΟ§Λ›­=€b +ς ><ΓπŒ"ΑŸΠΥλ~΅QΣ2SQ §_ηbν/&΅?S+*D¬;υ + 4‡„YΔM±ξ,°ΊŽ³ΥGΣžd$Δφή†μπgd"φξ±K€όjMκ Cώφm‰αΎDδ"˜_ϊwΞ艆‘³κ4žz(δDƒ―CχŽσΚV£γΫuFœžΑ­φημyJsWŽ^(\TB{βŒ(> XLά‹ξΓƒ6ΗΤ‚]UΠτΏ{Ρi ‘΅η‹!ςδbΥϊγη +ZzΈb—ΏP³ƒιfΣE~6Ύ)„Ruœ|S\T¦χθπI,RΐΧ_€i 4δͺΏψ½™ύΚ~9rΨ™Ύ T†(‘°ΚSBθέντ$ΔΏŠ&κσiD†™~ΤτlYνΦΜ‹ΩvΠγV|ΜΚBo†­ C9āžδiω^¬ZΠe,ŠηΗω%?ž‰οΠ§‡εεc˜εΓc2¬θ**ΝMFDμkx@ ξʐYϋΞπ'ΰrΝE8γΜί’“ΒCδiϋ,qϊ²¦ΉΨ>ΠuηΜAα(Φ†°Lo¦νΜ”§t ―΄?£«–ΖΊƒΙβ‹3½S²ϊίˆζH‚θ‰6/XEj΄Δ8ψ mE–!ε Δ?†ˆΟŽͺxs )EκI>RGΘ\ώ ƒτ1τw&!„νω’l0]έόiˆK[ηAomΨ@’KΒpG¨ζGl λUυπNVYg(#ΝιrœζŠ22βh>‰UŠl%‘—ΐ ΈG³“}{kTρ»ζ·ΰ}MάE=eώž¨(*ύρF•εκae`9Ώ\°Ϊ°³6Duυ97ΊΏeΤωאsJ2"Wζ>t6#Ε”$φ<§ΆΰEΊrŽΝͺ~ύG`°H '·ηϋΟCS-/Œ¦C―>χo³ς_‘‡EφμP―mΨ§“n[)Bwύ‡Μ£^eΙ%6eμΖVρθv―χϋΑυ±ΰΗ4~•υΥ –Δ1λ}t„°+ίKZι" +•>ƒHόF=εΡLΏΈθoVŸ».π\ΊT±;9ίΉc—0EB”fS*as£)―+φi?ϊ}ŽI}…‚ώϋ˜q©ͺ⠚p${kIςOtl° 9λφm˜’OCέΒ4(σEΝgκŽώK‹ϊΕ>(υ«~aυΝ: •ΨΝ‚<οΦ/y±I/ DΤ.nΝ†ιάφ–ΈΈ ۞εΫ(ζ|‹‘ΣΎη2ύΚΫ*C΅’’Ζ λZLΎ—‡DN‹ƒ5Ϊ&ˆμΗP`Drξ£wϋ{ΟJ<<χ8λDα1.ο΅*6ΟΝx:ι?axw΄ξ$ž’ΖκΝ„G’τR[Ž™_)³TΑ2³Υ|M ²1χB„ ψ‡*`osΖuχM‡N“Θt¬Ϊάλ³ς«1|^ΡL[‰ΞSφ:ΟΡ±€Β3Ύ˜_/VShΕiΥN:Ύ½ζ>k[ΨY…ΥΪZP:Ζ’θ{»γΔ<~%8Ρ! +{Η’Ηό"‡·―ΐμes-n9'‚ζΓ/εIΟ₯ΟέnžŒdΩC­GΘΤ£*ρήhώ•1–t3uœXιqEώSWiϊ₯œ­žN~ω1οζ;νϊH©sd“ΞΊ3y―g$κ±\γL‰ΒlLόΆd§$BF·ιx$]©)Œm0c°Δ©Ή`ΐWεΈ’xμx¦ί&2ΉrŸτYŽ|ZKcd$h K˜,3Υ–:“1ΨιΏ^ΎƒCŒωΤ»Λ²ΐΓoΆŒψΖtZΠQH ˜,fͺwo2gX²Ω3oΒ½ρΎ&~j2Jΐ\ΰλ ”ν™²–ŒrΒ ©-³edmMC§Ž²b±khK¦ S€ΝυoC§œoZzƒΠφ†Kxι·;;\&±¬š·<ψέT°74©Ρ”# A·"K¦vΆEnŠ‹ΫλG/ :A1Ÿl€%?Σ¨?΄υ_βw’X^&³― Ξϋ ,”άΥΫΞρ³;BΈ©w‚Ÿ\ΗUη©*ρΨ9ΰ4,±ŽΪ‰pΈ­΅B. /“Ο^p2wν8\mzΛΕΨα0ŒΆI4€Άo΄d±΄±(SPDΰΌΣ™ƒy[Κ³ζΑΆ•œγες;UFύΰ=Κ1hήτN ©Τ>T +#βδnηΤ>ΫӍΉΥWάΟ%·ΠΑGλΊ©™+nS•ezΒ$W=ω±aœf9‹aΪ ‡MbήT e―Z―£₯2γΠ¦τJ‹)΄ +$8ωvΎ…W £ό,˜˜ςΨΦάΕ·y‹vΊ‹ΪΕ„ŒrO7k·Βζ’4{ςZ½wX„b π ׏WέΤδwS½'3z•[‘ \.”-€dU}9•— ;CΌ6pΨα έ8 Υμ|ΓΉUΟ†fdZŽPτ4;£ςΟμ5ƒ2*!³`Ψ(EοW!vλγفκΨκ“ŠΌεύl σΥ'c*|Ζm`λ>(Β:OÎ'Aγε[ΚΘ³v•όΌτzϊVηA#Ω»ά~S\shK)» π) ›Ο'm+^ή4εK9³Α +$dκ*γ¨9ΩlαrΫ«a‡„œšˆξ±*σάĚ>Ο!τΔ^ +•MΤδ7fͺ‰|'κ Λ}=:Ώΰ7l}±I<›UhΊβη$•3ϋRPλ€ςΙ\ (A[™’΄ώιcιΗt*όBŽξ·h[·–vU‹KQυžž™Z Ω:ŠόΣUΓ+ς•Ω-3zQC+šρ ίZOX™\Τ‘΅$§U«ζ‡{*EA„“EkώθφΗd†TεwŸYρΑcXε1Ξ0'σ–RάΝΠ7yWΗkbykι5!έ”I•΄π9Τ<”α‘ )€ϊυΡWωš‰ΚDsœ,‰$h³ζΥϊβΟg€―‘Ώ«²XV’\pWν;ΐΓ"‰ @SƒΜ†Θ(s¨΄ƒύ, +‰½G@R9λΆb8“ΏΤάFMsDΨφ0FΫΉξ}ŸΫ†ΰΦS’?G0ͺ§½dwϋq:»†¦#%ΚΡηw—YcUwΣ-ν,1’h!„—^yΩΤh7υ¨ θΈΧΈ™mxGKβl•μ‚—ΞΜγ·VŠΓ©Ζ‡Wΐ@ŽΓ¦½lzβ½+VΆΓ€Q2Hσάντ|}`iΐqύ³ΊDFΣb«ZΎKšΞΝΨ+ΎqrςΑΆ}3[o±*;ΌYNΕgxSΗU€Θά―`@ΩΐSco^]$Ÿ%1@i2δΥCΆΥοΡY·–·νάΞίNί—Aζά„ϊCΈƒ½Γܞυl%η’’¬ ΛVŠdT†ΣΟΐl,Ζsg-s&vœ ’RκΤΨMP›aUΰ|’Fd% ]*4 φF±·dν’Ϋ2-²έ‘(ψΛ~ν듁vž-6kε-,W»Ψݎx”ΣKLt$©Š·‡‡qmσ·ZΗ>υ²΄ΐίH½rz"‹θhWρ €–7]Ω4~Δωl₯cΤ <Υ_ Ie-£»‰|ΰ¦;QZ2g{ΊΫjύΟ-Z<΅Ϊυ!½€Β= “J¦MŠM2—`kΰΚταŽα’¬qωέφύε‘ύ°™ΣdςW%3θ₯Hα₯Β }RS–”>•KΎηάζMcc°ψΜΑ(Φ’ΖcΥςAwaΧ C§~-¬BšΩΠσLΌe’ƒe.Ύ¦§ΊΦM”2ˆΉΌς‚U₯0'ύώƒe(Ι“S ₯¨βvNκ=žάS 1;mˆP^ΐ'Υ»Ν’Ιo¬ +^ —§””šr'Παv†Β₯tΧlΊTΡX.€w~αΤjΙHΑΝ™Œ˜Σ)]ΘCPJ΅”₯˜τ†¦†[b-ή†wΦώϋ!‘UžΝfYΠ­B²YΌ”?‹LΛ΅…ΑEίDύι³xUΪR…gtl[ΤŒ·>Ɲ«ŒSδ0 œΥgX\}L’ρ΅ŽŠ^aŽ£ƒ4ςθ€ZωNβ^„~»ΉwA^z6>S/UΰBΞΧ]_eνώ!ΛξEˆA $……C<ϋ¦ ’³ 3΄* +τ@’ϋhΜAό5»t”·σ*Θ0šd„7qrώ+£@ω@Ω6ν<Η ƒύΟ-f)˜Χ!Ζ Ώ 1dι…U2wsgU«CbΫğ;BΨ{——93Ή.kΎ³C'τ]ΧzΖζΟ6L:”IY $©ςΉδ½_XqLπσ.Vλ|[Ěv5ΨΑLκ-ό`»tΟcxλz ˆΘθ,βkμΨB9Φ‰žόfρΎgX՘¨ΗLŸP’β”ΊZΏ8ͺ-,/ݚnϋž2Όc’|μN;βIσŸPί"`t°š:/Ψ\{ΐ ^ν₯˜f6Ρku…Ή?\#6υ1*-m4±ΰ«6Ώ³†d€ΜΌLαΑyTŸPœ¬M¦β#Έa'υ΅«κ μM}\z·ά{&•Y˜Ι'5[Pqƒς‡&L@έCΫφQ?ΑΑ;Ώvτ]„ΞvνŽ]†HŠάθ!f©ίΏ1$Θ!v#™ͺΛ£€ΰ+ω‘£ήί"yΧRΠ ΄WΥηiB‹`ΟΕΜΉœr±m+½Χ £+ cώEΈΘ΄xBΪδμάΈπ―‡P,ΖlάΠΉI€φ"φG—|Uφ:FϋK«-ω΅κΦξxΠΜe$έ¨Φ‚υ»r|ό+XzΝ]FΏc&’LzηΒuMόΊYΩF }Ό―Φ‹<›±ϊωHΤ5ŸΩK!EŠ₯૊c܍aΚχxk=;žΌZΌλ0y­ό“ηC–όžmίβ0џ“‡ηpu"βAΆDl%Zβ§+5}i·:’Υ'"Y=ΈήX[˜ΏxfAΚtaεUH|σƒ $ΎΙ½[ƒ©λ?β +\¦7( pΣΡΏ\Nf~G£4A+ۊ?‡n)£ΣJ…{’Ω"51•“}"τX―ΛΫ&Κ)“)dΩ-2BݘwΡ7βΕG PpVbΣε0 „n³+ͺhcσΫΈο½·h™Z.”ϋ’)υΈ[qtΰΪƒu©Y#< κ»|mφ5ŽμΜe*ηε…«λ°x. {ςΑr”jΒΛ8H·αΌxΠΩ(.ί ΝA3πšAM ΟzKδ£qbέΉA>–l “³QŽϊγΫΏθ%‡ͺι‘{ݞdXa&DΉˆ°wΫ„ΤWψ +Θωhχ,]Œ^\R3Ζ^δ–9#鿏œͺ΅ΙU έ8†ΛΒ!ΤΎd΄UΌΒέ‘‘DXM&[τόА>μ)‡!ή9Κ1ižΏ©γ΄o²²k$κϋ•Μ°Π‘|ι¬Tύ3.›¬rΆά GC~-ΏžΊQŽΉeΦZ*!’‘ζ‘ή¨άpύΏl~€&ί3€Μ_ε%μχΚ_#])υή +έ=jxΎ²LΏ|°¨iΘΘ<$Δ€)fž¦ZRψy !hβ―}θJf[ΑQM{ό”--–U‹#»‘™ŸOΨΌΨώ―LxΠ§ͺσ|ŠƒŽD³RͺxΆyγ›&ΏfπV°e$λW…/zΠBΆχΜ$¦& :ΝσΟ(K¨{°$’'·Œ2αΆ“–‡]έ]7]10oRάΰ:Ι Ψ`0©oνq’~^ΥΈ0„\oœ˜BY<=ΆτόΜφϋ~wτΧ’«=¦ζΧo»ήό‘――›½+Ξθecn\ϊuzgΪ·I"‡P_Q<Ό£έXxOIr⏒©²/`θ€φnfΪzAW‰―βVΐαeQο!ε)žΩA+¦ls œΝeΙ„„%¬žs)™E aοΙΰm_ζ p„|ƒLΈΒΓ&ΐ½ΒΨ^gCLΟ+Ο½ ο·Ό‘pΎΆΪεΊ%1υΖIπqγδ‹™ β +|ζŽY_Œ-Ϊ~°ˆ£•ŒξΕ”έ†ίCί&‡9'py Φξ)ώ¬[›Zx­ΎȄ_―(₯"Οπ|ΑRM‘Ο-,œi?,9R/'Όβ λR°Δ€ͺJ{E‚—<<€6tώŠ7(ϋ-Δ3»c₯ϋ2³…=0˜_q8γ‰ΨP'LΗ Χݎšuk°{ πS=žφ/cςρoΔ¬«‰ΞvΕΦe€¦aJΓό +ύ£μj.Υ²e·khH0’·Σ%“/p™5˜ƒv0ki7y‘„Jwcκ偻ό€Š½l’7;JλfFΫ―Ώ^ψK—@‡Dd΄R—'Œz‘η€7©`rΊΪytSΥθΥε.™δΆ·όΏΤVˆΚ³άΕΆ$pA."―Žlι‰χ\Š4x_’ΣnίϊŠ%GΙOΗD·3L·ύ˜‡ΆD*3^\WWΗγΜ|AΧAfσPΡνC½σό6Oψ5֝‘+¦™ΚK”R„$βN"‘2±Š°εYαˆτ†ΖWD@ފω4(¬‘ΰŸ0ξΥ{­)θ+%ΜFΊ+¦δ©ΫΠlΜΏΟ2Μ|οΎΦΠažΌdΛΘύ_ŒxΉeIΒ¦i½yΒθ6ύΧθσΥBͺ­]ojMk ‰’ΌΘΑςUEJΛuΒ"сFν(ΰCZ‚Š/οσρ9Ε£ΊΊg΅Ξ”Ομε&ΣΟΉiN­$„·Η:!ϊnύ%Z΄;WΧ^&£ŸηνΦv’μγUF^α‚]Δ…Žgf̍p;dω` ΔDrΡZΠk΅χ^€cδiγuΗΣΔ}ΖuSCΟp–ZΧρsαφn#.ΤΫVFK)ΐ―nYΜGώφRΊ£υλ‹hΞΙ(Œs;‘ 4―p3dvP2£3'ΒB³b–π5ͺfgt¦y¬PΓΑ\–$ξχύ`ΉIΗ€Ι¨JŠa§(Τ€ Ϊ6ΊDŸc…*΄a %J0]ηΈΙ§κΘ€Ί¨‰Ύ HλU=ϊœtεqψŒ|Xύ‡Ž† < 'IbέXσŽΕμ»gύ«?ZW=Ζ4Γ —θ£ŸN―#l4·Υn{₯ό0ͺytΰ+ΰ αB~$ίξ£τΰ8-= ˆˆϋ`ƒΫG,)ΔρKπVήʘV¬)ϊΡ6°”WΩR΄f¬ŽΌH:—Ϊb²…<υBRtΰτ6§5&LΏίαPsZ +=τ"³}Ϊ[Ε…»ϋ`RζΤ¨Wa½Š\k֚ψ˜Ÿέ “T… žίΦ»‚&Tc}• ύθšEΡ6μžήΰϊ“Ν1H@:i«"έίqd/’BVΙ"e³ŒΓΞΗw?L`ύ’'#"–ΦZ/υ_—Ÿ!ΉΜ΅2GŒ6!JH€Ψ«ΟP}/“#DΑRZXŠ’4Q’FΨTxtޞcu"υ<1ΖSPKNΥόΞ'f{9MA‡ΉΠ¨.ξ‚ |ΜD¨ +–|LX„ΌgsNvud"=]Υ5Ε²Yβ‚o!ύΖ1JTZ‡UΑ Khς Ώ™ +Iⳑ­£χUaŒ^0UJ$‘xaΣ‰Ν¬ι-δΨ`Λ€²†9!P0΅‚1BE ΣR¬Α³œdΐ΄7ΛΙc½φ7 Ά†X ^.ϋ:Μ< ώ&Sψε#―RιU ΣLPΪθ-_ΰsnΥdρεg™ι|ΎΟ–@ άΩΖEt.[γͺ6……Iέ+'ςr˜]=‰ΈŸUΔzNPλΓ—Υ LߎFΊJΌ†θύύJMΣ 2ΨωhΗTιa^U†cW σΎc~­΄!’„μρN€’PΑΈd“εcwQ%|g FΰΨ0cAƒ‘w¦νQ ηϊ?†‘ότώDo=d\°˜υύ²’σl<ά·ί~4 •†ΨνΨϊΫάnί=`ΌLAgηΒνΓ' Ί2‡λwfΉ ɊΗσ–Ή‰aE¨­ωχ›@ˆ‘<χ†½`f3J­ΓΉ}WXμάξ”(εe•΄Ζ.+bOKC˜[/¬Π³UqšC]dlBβτΥΌ%Π‘d_ΗΙ₯ŸΧUΤε‰-φΡ.Μv#ίνYRω/…:Ϋ4«ρfκνόΟ•Φ.eE“…_  §ΈDψΜκΨ74H£ξ=³«Uε΅ωΆ‡­‚‘ΣCσ6`BP&¦«άG€ωϋƒ›ύΤ‰•%Pτη,s3³z0Ο~_γ™­BσaΡεŽDΘ+ώ4ζGJ/τ K₯³δRcΏGωΫ*4ʜϋΫ©Ο¬9r]έΝΞ$‰ήΌ6>—Qζ\θ\Ήτϋ³½έ°qΤy?‰ωGμΐμu(}-ηIήεkY¨έΝ(šη’ΨΩ2―6©-_Ύ9žήΊH―@6κKŽ#0–Μ˜λŸ5φ@΄ ψ|ρ†ΰω%£œαόQΌk€ƒωF1Ϊ+­ƒ'x؈Ό‰ώ2€†Ž°c―Hζ Z ;ΏςQ嘲Α"Ε Ζ,O<›λϏ#@ Gέ@ '©ίεΣ/AΛ–₯ ²°\cnƒIn3™Μθiψcͺz_ΌΥTάρ©[)’ΰtA²=ΒhŽR£8·ΨUλ^=ΖzΊ©ωQUΈyΠRqb\lΪΖ&φ…ޏΈhIά‹šς;žϊ8uχ―~-£ŠD) Σνΐ@ΎdSAšζ-W©ε’Ψβb!=O'ΌPΊό@-KC IsͺΩ“t!QεvΝQΰBN9FαPΒuΰŸItΖ σ,ΞθΛ:ۍ}Pͺ–­*°Ϊ!τ«7wwB’Sν‚}`»Ϊή;•'#Η,Ϋ#4с7ΘΠ¬9dθuγ}bΈΉ#zτ5ώΎ€»΅·’&Xιb=Ώ•ƒΐ](1²bκΎi1φKηo‰~¬²«`ώ9,\K °˜³P:eŽ$ƒθIΟΌ"£,d”S—»olZ•±ƒ±qΟ–([‡ —Ύ`KΔΆΌΠύaF­ŒM†‘ΰ„ΈφΗZ9§Σ¦~Ί:ϋ«νΨvδδω·Rš’ωq­Ϋ†-|‘\ηko\`h,·Β«»Ό=@1 £P1‹±ι.γϋ*ΰrXchήQΆ6Θ€Νε’uΘ #Δ!°σέ-OqβΩ6υΙΐψχΦtu\)Jν6q+ˆ¨χ6¬R&ΥA(„ŠωJ)J‰!Ϋh™K„Κ0Ϊ‚\ˆ‡b±Ν‰-ΧkΡ’/p]™—LΕΎ| νuS‘δηόΧ[²πӎΜ*ς‚²υ<βJeΙ£εϊψD›<§H΄•*ΙsΕΎΙΠ]Œ%―³Σu¨ν³ο>Ή'έlͺ«DPΖAeΪ|ώ. j ΰΛ`RΨH'k  w2ƒ5MMϊRDjβΠ0 Ρo·θJNζHήύΒόjcΎD + Nθρ]kƒUFΟίο$ΰœ8χ!}©ˆέgΑ‹¨Α²λα‡4§v~ͺVA:|KwΠgՙ듏ηόσΪ\Θ0ν)ϊ‡ χSPΔ:δuΤuδq;9PŸ!4{ΈΨΣ-YYV₯nŸνΎ;’T¨·Ωχμyδ‰Fο₯KŠ‹<>Ϋόόb`†gΥ’ϋ'Ώφ΅Κ³βH~ku%6σEΎθ¦ονm†CωΑ Α£"½~΅$ιOxf¦%+4’λv£˜έΔ7G6eTήI]Ρ7”‰]TΘ;AΣG᝸ ν3Ιΰί€$*wAυγC¬ΊΩ§T|θϊφόT¬ύu%΄ υΫμΧ·mRe›d™ς—°\yƒMUTΰΝ•ΗS6Ζ~Bό¨Ÿϊ€T’–_b2Ϋ¦ D+jΦi8Λ˜ί€δύ,¨³—#‚dΈ[]:dκ#§}+€< {¦`XΐΒz’-9+<_ ‡•ξhφΏψΐ‚Νήρμ &€Ž)dΑ>mοu/εH»m~ΛlTtdνϋχSwmωΠΣ™­X ϋ%:‰ρ’{L2[)>†[ΪΆ£mp“ΧuB’μϋΫΒ•·ζάθ^€7/$Yp~9E;S5 +tΰˆ›ζGΟvNΟ)ΌΙk"1c™νͺ%”‘b=‚ΪšΑsΚW(ˆΩ6τ±‹Γ z^΅™ΜΣ,,Lρtd!2Hξ ֍A5\ζsρ€ΫΘΥCγi σ:Ί 2σ¬Υΐ²Ρ[CΞhΊΗg\`Ά]β+XΒ°K…ƒKχ‘”$[ύ!?υ΅Uδ!Žίννά7`}³*y—‰NAΕψJϊε +ΦΩ­(£­7rίΆ9(2ΎΝσθ½:67™θΨυΑ ωίΓμJθΐηΦχ²X/ZA +ϋξ‚o€Ύ―γΉΒ“K}_Β }‰ϋ‘‡"#δ¦ΪTΞΟ­vk7SQU„π†mUύQ~nΠ>Ζ ·τΐό (‚«‘ΝζŽίŽzΎ`ι£VΠ›g‘eΏ;l»M.Ώλt‘R²ƒŽT°qή’³|”‚Eίi‘Λ΅¦ŒLΔTΤgΝγΥBqAKAž₯©ιĈߜΚEΈ)βεΥ4džυMΗφ0ΠN*!.±ͺΏ+a‡Όnφmϋk΅΅““o_δdβu˜0BzΠ~³Iύ?—ŒΔhΆγwk¨;\_ε+ξ‰ρ+jˆ½^ΡYα°‚έ•N₯]4ΜΤ8Μ@ΡoΞ™;Κι4ς:]RfMΟwεˆ§sLΚ“*Γυϊ.TDΕ=t'ΉΝ’p#6}9CdζŽιέgΪΧ (xΝEίΕΉ}Ιάΐ«ž‹†€σ’¨ΝdΥYτΫ\­4Vο˜_'%ΐΦgπ ‡Η+"ŠηΚ’Γ≦ΜmΚk‡ξΟ@B3)€Ή`”΅·%qϋά‰ΪαHΎCΗhήΓ ›Ά…}*挦ζΩκ—„§‹†ΟU‘Ϋ £ΰ8gΖη ‡Ðτ0-ƒ·ŸΞό!F‹gΡAZž&λ°οn6™ v7yΨ‹…Ζ nY|θHa†NŠ‘f8F>²¦%ΟΏKLΔzjΰωDμιφꒈ MίύίvFψ2:ⴌ‘μ€f©ΰΟRT—-ŒΡkaτϋεT½'xk‡· ΐ^=ήΚRuΜΤm ·λ·ΩΊΧ ±œ~Z3ςη ΒζΥpΌ#~D8t3Τ»\–7+)ΐΌωžύ©ιΗΊlΣΧΠgO-J‡? Di\-[₯ΙΜpθͺIετΉΈλ€ύ”Μ±ΨΥ3Σ­?zΆ"Ι¨SVGQsYƒ1C©X±5έΰ*ΏZͺ`Š9ο“Ÿ/υ.wλΪΒΉRοΈ<'Nc…‰LΪΧΆβ·>(ΝgΊζ7ǘ‹©κ'MH•οξƁ(64*Ž ΐ¦WΪ{4dF#kͺ\@ΘY'\‚Ό»ΛΊ―ίrϋξ$4ϊε:ŠF­Ά'Ο£› €lπ96ˆoύ‡±θβŽ$²lε?ώeόRτžΟ$⬡\"ζέ"·«°Τd?_ΊwΊtZ,X +ιΤ7 +β*]5}θœΰL\6 βa+ΜΧ·t΄’ΤU ΄“`‚33| ΩGK‘•”Ε`Ÿ© ΆΔ@ K_Ή¨žχ΅C§ΏΣ–wδ>ΰ,Ψτ€HmFιΏγtΚ’˜pHC>ς[ο¬ΞR‹z^žnΩ|nΊΓ87ί@=Δ–‹-Z‰Œλ BX.;!λI θJΛ ΜtHQ•`λ₯u)?Ζa±βΊΰ‰"HΤ‰Τ4]Τ'™ΏΖνϋΩ±vύ‡0Oob2O‹g$ΣXFžΖAΖ 1ηi–ΔhͺΝΡž}n—ΠŠ$sρπƒλ ‹ΧϘΡζ;S|α&ν!½dlΌθΈxΌΕ­9ΦΉ}φ£-3L°9›(Vύ}GŒaήXέΎRfάJrΓΛq+U…“~Œ₯›ΩΩYvUζn€Š5OŸo@ŒνΝ\‘ΰηN‹b:ςfrΥ+ΤBVQμΪτζ ,k«V Ξί‰Ίο»t>ŸΠ:‰ΪΕΙcƒΪzHd1ΕΘΌw@ίZά ͺSyPτέ"-0S%Η|+•ή€ΰΐkΜuq\} Έ +†§w3oJ Ή"+!Ν9φ¦-0Έλ]%­7Έ3•δύMT—Τš5»νγ•œΐΓZ˜k¨/ΕΉζ{Ω1MΙ!t1 Xθκvpu +A>Τ»(ΐοmη,³₯FΊ†ƒΣΝ'‡Βπ΄Λ+ފYΒΧ„ "7]9‡ΣRuΣ’ !―Τz¬Ω|±ςϊc{EϊΛ.Zͺ “ΙŠέ[Ή.C; -2‡Œ^»EcuΓazΙΪB5x²αςvρ,ZΡ x~Ό僇|h½Ž_ι€V―|Ÿ]l Z°q%΄»X½k’ΗρkŸψž\ίqΫ&π₯˜eΖ΄άXqσ»T#O¦‰’ΩXΟkΓ3Etœ3–Eςᑉ}{υdd|©Γ#“‰i’7£$σ?h:XΕόGυό]XA η‡˜84—c]Ϊ2―PΗ[Žoj`π|nI«δY@±K ‘'ή(Q€ž(ΑΥ2JBΘΎ:ουŠaΕGΛ“kͺAd’[6š s"vi@vMd”ϋO‘)&0yΠΏεv\‘ιέ’a`TYΊυ½Υݚ‰Μ7ΰΖ+—αΗ ϋueΩω/O™ΪD―‰ΩX½[Πeμ!?yͺΒTΓΫhΤW|υ鬏¨vέ=|'Κo‡™re΄y5‹Sπ„λ’”ΡΗΓ”Ί¦γ³$œ nπΆγΦ(Ώθ―sp–`δœIΦišιTPΣŽιšJΝΝ(TY’7·΄)Δ`σώO|iΦΑ“έώ7B\SF›Ԟ"ϊŒΊ₯0±V>ήςΫ>‹ΐ+>‰ΕΖ`U€_oe†•p¬ _΄,&DjΡlΌsƒ*1χΞ«›οηπλE MkΧ²η6C<ξΞ H¬\α™΅m€zΙS*#"3ςZwΦμ0μy€…¨-Π$‚/ή5υΜξcu½Χ'YΝ‰-T€«ΜͺŽ ΎPω>ڌI―σΝL|€p·WšΊ26qθ +]Y ΉγLΓ©Δ:δ0Έͺ€˜—Ε ήΗJ# +>b§§ {•‡ €SG:" +tΡYK—ίk˜ Ί˜άΦ뀄Αωfz`OΛν$Γ7ƒ%g'Π­ω( ¬Η.>‡―!Za1’4t]o¦§>ο~I¦(YDΞ€ΨτYΔrNo|*Ξ +ΜL.CξicΕA‰G‘‚Δ‡₯=tΡ’γGΞi‘₯RΦ15 Ωΰ/κΚ.r”Φx‹αn¦-,EφtRdε/Ϋ^Q + Άi5'κ΄_Ρέs’“€δ»FΎθlλ±c&ΩKBdήjƒ£Η½vFόΆ`₯νμcΡ†;4»―U»L΅|< Όςλ²6Ω¬ ρίΑNSώ‰­:fNw™1ΤnRfΊͺH ΗυΖ`ΗΓF₯Ϊ«s$ψ¬ϊfO“ ΕγΪ€j„΅yun’ͺyδGΏ‡sσrΧδΠ€|άp›κ†υ$=T€“5Dύ +20‚H– Έ>lιnΪƒfK˜oI΄.ςXζϊSΞGί²§šϋϊ]η‹E|Gyϋ#γFκ΄―LoΰZηλψ³½I 7`|ʏϊ!6d”9ώέYαΜi}Χ©O·\k$v}u‘›Ξ Ά +iPΠΙ£΅nχ·ŸžE({οΊNτΞ­iuƒy…”;Ϊ'±zX γΡ†Lfφ 7η{Ε™~ ;fEύ <`ΊV˜ΰεέ ”½q·`ΑοeΨ‰ fNρΧί²LpώPΘ© όZ#2u κŠ‚βQ…ζl"KΌz.hXόμ—+K}Ÿ°Ρ₯ 5ςͺ»ΘyΌμΩ½:hVΰct1'ΟΪ¦ςΖƒ/ΙxWczVΎΥώ8· = •Ytζζβ +Ί³OlΆ40ˆΪφ+cώQΞI+ν“FΫκXώOΎve˜—ž³ +Tͺ,vJ(ΈIϊζο*I›’²6ŽM"׏ήψΎΩO›†πφzuό+4™½$t+ιΝOΉz”)W„EκCB”½ν9²]‘εΎ¦yΒT ‡½\-ηΠΙxύA7ι7 +φ’1Σ‘J ΖρΘ jλ;ŒaτkΤM€CB΅ZαDMΟ!‹—{Ωί)l s, !~ή6˜ό ™:’‹S Λθ―~mύ_—$M^©ˆlι&κƒΑΚOІjίδ˜‚YηSeΘSWPρΑC±W._Ώ‚Ξ˜Ζˆ+H’ ™φkιΊ™Šπ ©ΝA8 nυ/ #ό,†šί ’7IΌ\J[IΑY©φ8μΘΚϋ˜΅ͺΈΣφgIl₯).Κξ” U )°ΓμγI όfR]‚%RiΕΧDΆž{ +f‚vίxΝ+/Σφ>(ώΑΫΙ’UΏ²l“…ΔΒ-ωήτ`GΤ*ηažz.ak.|πΕ;¬λ6žSYξyΊ&Ιηε'f__ΧHm¦΅sE€€ΆiοdRŒ5ΠάΰXΧ;|—ΣΆP. ki}T5ψUωςωΉί’MΒβωλΫM’Θ‚ΡζSΓΛ"₯]­μ%n3λͺb¨”•2›Qχέ:ΞMQ¨·Q&‡]²tρ|αŠθb|ΛoΆ"u―ίͺ=Γ"mjvjΐΘΥΗiΖuA4(†"5‹ jΩ)VX΅χΫGŸΚαGΪςαΉΡ‘›a“τL½…;Ρ=²;wjΚ΄d6HfMΏ-ο+Εͺϋ!²ωΉQόλ­ Σ”.g+],θ{ΕjβοTͺ΄§§Ÿœδ0Q±o{_ΖPiΟψ'μw’JΚJΌαΘώ„U¬Δ5οo +,°|ΓθDyWyGj{@Μe2Κ6ϊϊL λμ™U]JΤ€%ηΟ–”½SιΤ‚γωLΚ«nt­­ερt‰Υ?/a₯rΏ„ΎόΛΔv8i +QJu%SoPf6jI―d+4¬Eά‘ZH ]ΫŽΔθ–·}ˆ°U$fl/;κW(IάΕANβτ«uςR΅I=­>ΛO.Ψa1½‹+™γ<ς‹Cmώ‚αVΚ_Qsΰ4rC ΖŽΡέt Λh‹Q>χ"΄°:€$z57μb·—b‹Ε³A£μ*³ešΘυΕZ}~ Εώϋτ™L0)-JηΕBŒ>ς–Cv[e?”€Θ²β<ώAS’K τeίΌΙοΨ6?ΩYjά[”^ͺ¦ΏΎ»(mœw£ΡΨ%tϋU\oDxvγΒnjPγIτ›=ΓβYˆͺΛΔŽΣ…ι†η°jΩ-‰εΩnq0”rZZm§ͺ₯A™-Δ…PR°TπΛFn°Ν­8€P8ž΅/μΒxC1˜Ο€‰Νεβ½"³Δ“.iΦ~“L‡jπqσŸw±¬ͺξξη+μτqΪΫε™ήπίΠπ=8θoψV°—ξ$ΓζοN’ ’Ξ §fΊw”Χ]¦“eŒΘΧDl Q·2Σ›w­³΄-zΛΰ+O€η+πdλQpΎλTSTϋ»Ηή^’MιΓ€½\‹P•ψO*QdcCι +c< H³ΕΑ³κνφίƒ3π-x +e†ZV’^όͺW[ωNσ5,₯Θωήy:CύŒ½PVΞΎ7cΎqCηZ‚Φ³€i⽝(6£Ξ*ϋ{‹ο`††Αε–Π5ΞιΰχΑμιν„ &—cHσΗά+\2grZίmnZoјεL˜4kΕ ?>Oέg>σO +Y£+ςS~υgAΤKNζ?ψφžl’ν–‘ˆeβ=l©b&X“’y³  •Κ>ΛΒ’:W΅δjΔ™I8.:ς©D^f¨›α°ΜΥ© ΥΈ2,Π[υαvΊwZ¬~$Ž„Ÿ% ΨφQ »6ΌΆμ―‡Ε Νc›u¦8 «X!Vc·6g_Mώ·^l@κA§XXQhΙpΚΘ d4¦@³±„:~Ά6Ύ;%τ·‚Γ%ΤΪ0YγtΊΠsU³Ο~ΕγX9‘aΊ¦~o‘άπfΞυΰ@ΉΙ”ƒ#₯³€m΄Ι.…aά Ή`ρx²Λ«Idp-^½^,,x»Ή64­/MΤ|δ«sνΙΝ#4 us+Y¬ώ^ΥE €―δ`[˜άΧgŠΨό_^_φ UAηεz’θχΖ_5η,Κ ˜YχŒΎΦ“gλΟƒ#·Κ€Fh|ΌΚIΞ§€ΉlΖP/ΟΤrR2Ή°­πj$w‘)§™8Μ°³Ή¦3T·~  ΨƒΘ/Ν»‹S<Ε0š‚ΞXJΆ²±Wπ^¦„S%ΪUI3›ήZœ!Ά˜7‰ηvΌ5kο>αέΡMζŒ{8ξΖ¨sΜώ’Ž’!1Ý Τ‹MΒQΡφRbζŸ3αλ›SQφσΨ†vΑ_€ώp&^H‚9yΕύ‡,€Β€~β}α‘W΄Ηu–PŠρ DRΕΆΦJ©˜FΨνΔΞΤiί«©Φ ₯˜« ·1Ϋd€#Ϊ¦‡^žVŠ4.Yά©ΩΈI>$Œ0hhW·Jωυ퐐’Ώ3iλƚχΦi?\Š4ŠΰΉύ<Οk©ΩšdBd?%ΨA‚2\9)Fτ(γw>/04”tαήzEjU?ι’ £Vk–β!†JE6/ΙΞδo”‘NXώ.0P­1˜0X―rW‘ίύKνί5ΐmώn½£k~Ί‡QΑήθq±qZδο +gϊρ"Μ0'J»P5qx’»“ ν3ΜΓ]λεGŒΏVV’Σ‚ΪZ°TΨλсθόYŒΫτ`Ό…$u0AΕΝέάͺΝ*ΐ½jTJ‚ ψ’1Α4ž,7%„€φΜOς˜xί6e1 xρξjή~-cΞ;Α7οό)zb$ƒ–ώ RΔΘ5ΠbΉςΧu› ϋΈσuiƒΣΰΞdlmβoΈψχ`Χ{C'j΄6erγƒI‚gΞφδgυšwΣηxέ£Ρ…ψ†87έC”ϋ"9CϊθIˆp2—’G$ΠCš:a,1 œ‡k™ +ω»<@Ή°-Β1PΞ·ΰKΩ5΄­πEcIφώκΤ/ζ·λΩθ>dy<ΰQΫdŒ0g—šΪοέ•‘ŸD;τ}όͺμŽΩi]˜Ώiό«υΖkŽjϋ[Eύ(-ωG‘Tα‡Β<ΤζVUΒΒΨugœπΏ†:³fΒ”^Ÿ"ξγScv-6­ο2ώμOϝ<†O)—α:@sOΪ2ΑB\ΩΊ₯Ž3QužΘŸ¨[}γΰT6HWC_qόU+Φͺ M Ι§μ$ώLκx΅ί‡ˆΑ #m°)¦εχ™δwΕgp;π~0 Qo=Ώ-¦γ?ŸԘtφ +Ά J9H²aݍΑ εΪ³kL:·\’3TK­’nΫΡΣFψ5@Ӎθ²B₯V«ΉΦΡΓηΈ‘Ϊη»EΖ²S‘‰ Λ­†˜“πώΉΓ«bϋC`ι'J–;.oOσ“Ž€rλε,Γ9麘ΰΦά#Ά ΄ι βyšyD#‚Ψ$ Π,ύlZpMΌ(ͺ“BLΜtΔ=ˆg9ηŽίn'½ΐε¬AA1Ί‚ΤβήλχNτUU, Κ‹1λίoQŽQ ˜:kΞ6žφΡΎK±U¦™9©!·<πoJbL~ϊ€π½Jπ"‚w=C›Δ‚»τW#ŒŸϋήΘrVŽŒ6n«τΞρ γϋ·59fχΠλΠFΐvιΩH~…€[θΙ€"zVŒεΓo½΄¨ _¨{Δdπ0=s oͺ5θCNŽξ]ηTΘ€ύ€τ‘°• Bvδ*–Ώ"οkρIVtGβlɟ§6ΡTΌ#ΥUƒΎθμϊͺ.ρbοΐθβ’γ₯b+F2γΝσƒ­ + j\ή|,ς=₯ݞϊ !”‚²zGQ«BCτδ^Œ£9ΨuρA―–|Σί±ΓEΛ,ηMΒ9r(πDCšη™bωθυΑό&{ΛΚΓβύύ“αjZΖFΥχZ_ψ6?”γΈζqΔA@ΩcΠq|ΨΡΚ¨@›δΩDg;ʏ5”wγέο’ l©`βl΄Η,ΥΛΣRxΜ»k―:­T_³­R7<΅»;ί5Κ†@ΕuΠυDά’…AΒˆΈ½Ž― 3―PΞϋ+³`£Ϋ”ζLΓ±’$7/+½7Κ’š“<†.•ΥVl%ΊŸ”R½kT΅!ΜT•ε5GψEΓ:J€Ώ TΫΛΡ…£ξY°ό»Yg‹Ω₯Fr¦©MBPϊzμBΥ Μ1½ŠάξΖΕήξ_κσ™ΩΙπfΥ@<ΰs*l~©s …zθγΎΦjJŒτί)©«―HμƒΪ°~Υψ“ο‘ΌΘγ¬Α°˜zΙ―°,σ³΄κd`7)Λ«υήt' §Ζαlͺ[4/ΫwP4₯tΔ+’ς–’–·‚†±D¨― cΛuΔ5°BX¬T€Pw)ꁩKο‘ΪthSοže?w΄x/U φΜτt.yΖΫe‹ye¬pPλG(G#mcr—Fβ@Ώ[.fψ©»ΡΘk%^ή׊R›ƒπίXB8²SΆ +.F[6Aλεͺ³0ΦβΈfœςZθ[…‡q'Ϋ ιώl;Κ©$|rˆ,±΄Π\iŠφ}q^\Ϋ-s6ώΣ\Mpσ²ΏI‘4>m^3“ZΉέ˜pε“b˜4MŠ4΅ΏΘ³'p€OŒ~žΫPͺλΊTΆ†$ΖH<ωD~Βχ@Ζ' RΔ‚x·%(ΒϊΎ.Γ΅ƒ>νΓDz-a“˜Ψ½!\ρ[X+bc/œ'ώ‘‚–ŒδήBάΔ6u+?΄ P‡ξπΑCυBφ»9‘ξ³yΎΰvβA`\Ο―2ΪΏ9…ξajΫκiLrϋΞr+±–~ί†uψς¬f4lΚ!l”δ¨#Ύ΄ύ<ΌP…Τ©ρΏ±>!φBΈƒΉδ_ϋ_XCG&Ϋ­8a/ /ηκ‰ωMοσδΎ·σκ‚“ηG¨L²­Z+ΕZ•,qΐ‘ θjς³Hύ΄†Ϋ²ŸkKƒΛΝν_Κi,πΟ ;ά9 +endstream +endobj +859 0 obj +<< /D (subsection.6.2) /S /GoTo >> +endobj +860 0 obj + +endobj +861 0 obj +<< /D (subsection.6.3) /S /GoTo >> +endobj +862 0 obj + +endobj +xref +0 863 +0000000000 65535 f +0000000015 00000 n +0000000208 00000 n +0000000876 00000 n +0000014395 00000 n +0000014429 00000 n +0000014477 00000 n +0000014551 00000 n +0000014626 00000 n +0000014735 00000 n +0000014986 00000 n +0000015059 00000 n +0000015132 00000 n +0000015241 00000 n +0000015351 00000 n +0000015440 00000 n +0000015559 00000 n +0000015695 00000 n +0000015807 00000 n +0000015925 00000 n +0000016020 00000 n +0000016182 00000 n +0000016361 00000 n +0000016534 00000 n +0000016709 00000 n +0000016889 00000 n +0000017067 00000 n +0000017240 00000 n +0000017413 00000 n +0000017587 00000 n +0000017759 00000 n +0000017932 00000 n +0000018103 00000 n +0000018289 00000 n +0000018449 00000 n +0000018610 00000 n +0000018783 00000 n +0000018938 00000 n +0000019150 00000 n +0000024775 00000 n +0000024997 00000 n +0000025044 00000 n +0000025131 00000 n +0000025186 00000 n +0000025234 00000 n +0000025323 00000 n +0000025386 00000 n +0000025670 00000 n +0000026002 00000 n +0000026254 00000 n +0000026434 00000 n +0000026638 00000 n +0000027002 00000 n +0000027230 00000 n +0000027474 00000 n +0000027718 00000 n +0000027986 00000 n +0000028238 00000 n +0000028586 00000 n +0000028950 00000 n +0000029106 00000 n +0000029309 00000 n +0000029512 00000 n +0000029711 00000 n +0000029911 00000 n +0000030096 00000 n +0000030368 00000 n +0000030641 00000 n +0000030917 00000 n +0000031163 00000 n +0000031395 00000 n +0000031713 00000 n +0000031986 00000 n +0000032223 00000 n +0000032483 00000 n +0000032838 00000 n +0000033140 00000 n +0000033441 00000 n +0000033699 00000 n +0000033952 00000 n +0000034177 00000 n +0000034350 00000 n +0000034515 00000 n +0000034702 00000 n +0000034893 00000 n +0000035097 00000 n +0000035326 00000 n +0000035532 00000 n +0000035587 00000 n +0000035609 00000 n +0000035797 00000 n +0000035984 00000 n +0000036144 00000 n +0000036315 00000 n +0000036502 00000 n +0000036690 00000 n +0000036878 00000 n +0000036978 00000 n +0000037000 00000 n +0000037046 00000 n +0000037174 00000 n +0000037254 00000 n +0000037301 00000 n +0000037430 00000 n +0000037502 00000 n +0000037665 00000 n +0000037850 00000 n +0000038024 00000 n +0000038199 00000 n +0000038369 00000 n +0000038536 00000 n +0000038698 00000 n +0000038858 00000 n +0000039018 00000 n +0000039232 00000 n +0000039394 00000 n +0000039562 00000 n +0000039725 00000 n +0000039887 00000 n +0000040061 00000 n +0000040227 00000 n +0000040395 00000 n +0000040560 00000 n +0000040723 00000 n +0000040886 00000 n +0000047474 00000 n +0000047720 00000 n +0000047890 00000 n +0000048062 00000 n +0000048237 00000 n +0000048399 00000 n +0000048569 00000 n +0000048743 00000 n +0000048923 00000 n +0000049096 00000 n +0000049268 00000 n +0000049444 00000 n +0000049623 00000 n +0000049799 00000 n +0000049978 00000 n +0000050152 00000 n +0000050333 00000 n +0000050507 00000 n +0000050682 00000 n +0000050859 00000 n +0000051034 00000 n +0000051207 00000 n +0000051378 00000 n +0000051544 00000 n +0000051726 00000 n +0000051887 00000 n +0000052050 00000 n +0000052215 00000 n +0000058351 00000 n +0000058611 00000 n +0000058775 00000 n +0000058939 00000 n +0000059102 00000 n +0000059265 00000 n +0000059435 00000 n +0000059603 00000 n +0000059765 00000 n +0000059931 00000 n +0000060101 00000 n +0000060268 00000 n +0000060436 00000 n +0000060604 00000 n +0000060770 00000 n +0000060935 00000 n +0000061099 00000 n +0000061261 00000 n +0000065919 00000 n +0000066111 00000 n +0000066281 00000 n +0000066445 00000 n +0000066618 00000 n +0000066792 00000 n +0000066957 00000 n +0000067125 00000 n +0000067291 00000 n +0000073407 00000 n +0000073626 00000 n +0000073790 00000 n +0000073953 00000 n +0000074128 00000 n +0000074292 00000 n +0000074455 00000 n +0000074620 00000 n +0000074791 00000 n +0000074959 00000 n +0000075129 00000 n +0000075293 00000 n +0000081278 00000 n +0000081566 00000 n +0000081731 00000 n +0000081895 00000 n +0000082073 00000 n +0000082237 00000 n +0000082401 00000 n +0000082566 00000 n +0000082729 00000 n +0000082896 00000 n +0000083062 00000 n +0000083230 00000 n +0000083409 00000 n +0000083591 00000 n +0000083771 00000 n +0000083940 00000 n +0000084114 00000 n +0000084294 00000 n +0000084467 00000 n +0000084654 00000 n +0000084830 00000 n +0000085001 00000 n +0000085175 00000 n +0000085345 00000 n +0000085507 00000 n +0000085670 00000 n +0000085831 00000 n +0000086008 00000 n +0000086170 00000 n +0000086341 00000 n +0000086515 00000 n +0000086679 00000 n +0000092341 00000 n +0000092558 00000 n +0000092726 00000 n +0000092895 00000 n +0000093058 00000 n +0000093232 00000 n +0000093403 00000 n +0000093577 00000 n +0000093755 00000 n +0000093920 00000 n +0000094086 00000 n +0000094266 00000 n +0000094427 00000 n +0000094597 00000 n +0000094776 00000 n +0000098571 00000 n +0000098842 00000 n +0000099010 00000 n +0000099178 00000 n +0000099346 00000 n +0000099514 00000 n +0000099682 00000 n +0000099868 00000 n +0000100036 00000 n +0000100212 00000 n +0000100384 00000 n +0000100555 00000 n +0000100723 00000 n +0000100892 00000 n +0000101060 00000 n +0000101229 00000 n +0000101391 00000 n +0000107408 00000 n +0000107585 00000 n +0000107748 00000 n +0000107915 00000 n +0000108081 00000 n +0000108250 00000 n +0000108417 00000 n +0000108586 00000 n +0000108754 00000 n +0000108922 00000 n +0000109092 00000 n +0000109255 00000 n +0000109418 00000 n +0000109589 00000 n +0000109770 00000 n +0000109946 00000 n +0000110108 00000 n +0000114337 00000 n +0000114581 00000 n +0000114752 00000 n +0000114920 00000 n +0000115095 00000 n +0000115265 00000 n +0000115428 00000 n +0000115599 00000 n +0000115767 00000 n +0000115929 00000 n +0000116092 00000 n +0000116255 00000 n +0000116417 00000 n +0000116583 00000 n +0000116747 00000 n +0000116910 00000 n +0000117073 00000 n +0000117259 00000 n +0000117436 00000 n +0000117610 00000 n +0000121847 00000 n +0000122121 00000 n +0000122286 00000 n +0000122466 00000 n +0000122630 00000 n +0000122810 00000 n +0000122979 00000 n +0000123157 00000 n +0000123318 00000 n +0000123500 00000 n +0000123675 00000 n +0000123849 00000 n +0000124022 00000 n +0000124203 00000 n +0000124377 00000 n +0000124555 00000 n +0000124732 00000 n +0000124900 00000 n +0000130289 00000 n +0000130427 00000 n +0000130642 00000 n +0000130856 00000 n +0000131069 00000 n +0000131281 00000 n +0000131580 00000 n +0000131878 00000 n +0000132177 00000 n +0000132396 00000 n +0000132614 00000 n +0000132824 00000 n +0000133050 00000 n +0000133275 00000 n +0000133508 00000 n +0000133740 00000 n +0000133939 00000 n +0000134140 00000 n +0000134340 00000 n +0000134540 00000 n +0000134741 00000 n +0000134949 00000 n +0000135158 00000 n +0000135358 00000 n +0000135566 00000 n +0000135773 00000 n +0000135978 00000 n +0000136184 00000 n +0000136388 00000 n +0000136592 00000 n +0000145036 00000 n +0000145214 00000 n +0000145429 00000 n +0000145643 00000 n +0000145930 00000 n +0000146217 00000 n +0000146503 00000 n +0000146730 00000 n +0000146956 00000 n +0000147227 00000 n +0000147498 00000 n +0000147727 00000 n +0000147955 00000 n +0000148173 00000 n +0000148389 00000 n +0000148612 00000 n +0000148833 00000 n +0000149144 00000 n +0000149455 00000 n +0000149766 00000 n +0000150077 00000 n +0000150275 00000 n +0000150475 00000 n +0000150675 00000 n +0000150876 00000 n +0000151107 00000 n +0000151339 00000 n +0000151589 00000 n +0000151838 00000 n +0000152088 00000 n +0000152338 00000 n +0000152543 00000 n +0000160754 00000 n +0000160906 00000 n +0000161115 00000 n +0000161322 00000 n +0000161532 00000 n +0000161733 00000 n +0000164876 00000 n +0000165042 00000 n +0000165104 00000 n +0000165166 00000 n +0000165228 00000 n +0000165290 00000 n +0000165352 00000 n +0000165414 00000 n +0000165476 00000 n +0000165538 00000 n +0000165600 00000 n +0000165662 00000 n +0000165724 00000 n +0000165785 00000 n +0000165847 00000 n +0000165909 00000 n +0000165970 00000 n +0000166032 00000 n +0000166093 00000 n +0000166155 00000 n +0000166217 00000 n +0000166279 00000 n +0000166336 00000 n +0000166396 00000 n +0000166459 00000 n +0000166522 00000 n +0000166580 00000 n +0000166638 00000 n +0000166696 00000 n +0000166754 00000 n +0000166812 00000 n +0000166870 00000 n +0000166928 00000 n +0000166986 00000 n +0000167049 00000 n +0000167107 00000 n +0000167165 00000 n +0000167227 00000 n +0000167289 00000 n +0000167347 00000 n +0000167409 00000 n +0000167471 00000 n +0000167533 00000 n +0000167595 00000 n +0000167652 00000 n +0000167710 00000 n +0000167773 00000 n +0000167836 00000 n +0000167893 00000 n +0000167956 00000 n +0000168015 00000 n +0000168078 00000 n +0000168141 00000 n +0000168203 00000 n +0000168266 00000 n +0000168329 00000 n +0000168387 00000 n +0000168450 00000 n +0000168513 00000 n +0000168571 00000 n +0000168629 00000 n +0000168692 00000 n +0000168755 00000 n +0000168818 00000 n +0000168881 00000 n +0000168939 00000 n +0000169002 00000 n +0000169060 00000 n +0000169117 00000 n +0000169175 00000 n +0000169233 00000 n +0000169295 00000 n +0000169353 00000 n +0000169411 00000 n +0000169474 00000 n +0000169537 00000 n +0000169595 00000 n +0000169653 00000 n +0000169716 00000 n +0000169774 00000 n +0000169837 00000 n +0000169900 00000 n +0000169958 00000 n +0000170016 00000 n +0000170079 00000 n +0000170142 00000 n +0000170205 00000 n +0000170268 00000 n +0000170331 00000 n +0000170394 00000 n +0000170452 00000 n +0000170511 00000 n +0000170573 00000 n +0000170636 00000 n +0000170694 00000 n +0000170748 00000 n +0000170806 00000 n +0000170869 00000 n +0000170932 00000 n +0000170990 00000 n +0000171048 00000 n +0000171105 00000 n +0000171164 00000 n +0000171227 00000 n +0000171290 00000 n +0000171353 00000 n +0000171416 00000 n +0000171479 00000 n +0000171542 00000 n +0000171605 00000 n +0000171663 00000 n +0000171721 00000 n +0000171784 00000 n +0000171842 00000 n +0000171905 00000 n +0000171968 00000 n +0000172031 00000 n +0000172089 00000 n +0000172152 00000 n +0000172210 00000 n +0000172268 00000 n +0000172325 00000 n +0000172383 00000 n +0000172441 00000 n +0000172499 00000 n +0000172557 00000 n +0000172615 00000 n +0000172673 00000 n +0000172731 00000 n +0000172789 00000 n +0000172847 00000 n +0000172905 00000 n +0000172963 00000 n +0000173021 00000 n +0000173079 00000 n +0000173137 00000 n +0000173194 00000 n +0000173252 00000 n +0000173310 00000 n +0000173367 00000 n +0000173425 00000 n +0000173482 00000 n +0000173540 00000 n +0000173602 00000 n +0000173665 00000 n +0000173723 00000 n +0000173781 00000 n +0000173844 00000 n +0000173902 00000 n +0000173964 00000 n +0000174022 00000 n +0000174085 00000 n +0000174143 00000 n +0000174205 00000 n +0000174268 00000 n +0000174326 00000 n +0000174380 00000 n +0000174443 00000 n +0000174506 00000 n +0000174564 00000 n +0000174627 00000 n +0000174690 00000 n +0000174748 00000 n +0000175128 00000 n +0000175586 00000 n +0000176534 00000 n +0000176938 00000 n +0000177586 00000 n +0000178263 00000 n +0000179214 00000 n +0000180179 00000 n +0000180646 00000 n +0000181700 00000 n +0000182483 00000 n +0000182944 00000 n +0000183610 00000 n +0000184143 00000 n +0000184325 00000 n +0000185131 00000 n +0000185488 00000 n +0000185855 00000 n +0000186307 00000 n +0000187257 00000 n +0000187618 00000 n +0000187686 00000 n +0000188449 00000 n +0000188474 00000 n +0000188521 00000 n +0000188599 00000 n +0000188677 00000 n +0000188767 00000 n +0000188899 00000 n +0000188946 00000 n +0000189025 00000 n +0000189104 00000 n +0000189195 00000 n +0000189331 00000 n +0000189493 00000 n +0000189665 00000 n +0000189826 00000 n +0000189989 00000 n +0000190154 00000 n +0000193741 00000 n +0000201432 00000 n +0000201604 00000 n +0000201801 00000 n +0000203700 00000 n +0000213649 00000 n +0000219760 00000 n +0000223317 00000 n +0000225286 00000 n +0000228023 00000 n +0000235905 00000 n +0000237918 00000 n +0000239640 00000 n +0000244244 00000 n +0000245980 00000 n +0000246167 00000 n +0000246357 00000 n +0000349674 00000 n +0000452742 00000 n +0000460069 00000 n +0000471741 00000 n +0000535602 00000 n +0000535654 00000 n +0000535746 00000 n +0000535826 00000 n +0000535878 00000 n +0000536086 00000 n +0000536133 00000 n +0000536263 00000 n +0000536327 00000 n +0000536379 00000 n +0000536472 00000 n +0000536652 00000 n +0000536704 00000 n +0000536788 00000 n +0000536835 00000 n +0000536965 00000 n +0000537045 00000 n +0000537288 00000 n +0000538206 00000 n +0000538251 00000 n +0000538602 00000 n +0000539266 00000 n +0000539475 00000 n +0000539693 00000 n +0000540480 00000 n +0000540505 00000 n +0000540759 00000 n +0000541391 00000 n +0000541449 00000 n +0000541691 00000 n +0000542438 00000 n +0000542479 00000 n +0000542643 00000 n +0000542682 00000 n +0000542890 00000 n +0000543105 00000 n +0000546284 00000 n +0000546540 00000 n +0000546579 00000 n +0000546618 00000 n +0000546657 00000 n +0000547314 00000 n +0000547736 00000 n +0000548107 00000 n +0000552284 00000 n +0000565659 00000 n +0000569334 00000 n +0000573009 00000 n +0000612745 00000 n +0000612984 00000 n +0000613648 00000 n +0000613673 00000 n +0000613941 00000 n +0000614288 00000 n +0000615205 00000 n +0000615530 00000 n +0000615684 00000 n +0000615838 00000 n +0000615992 00000 n +0000616146 00000 n +0000616300 00000 n +0000616454 00000 n +0000616608 00000 n +0000616762 00000 n +0000616916 00000 n +0000617070 00000 n +0000617224 00000 n +0000617378 00000 n +0000617532 00000 n +0000617686 00000 n +0000617840 00000 n +0000617994 00000 n +0000618608 00000 n +0000619441 00000 n +0000620023 00000 n +0000620177 00000 n +0000620331 00000 n +0000620945 00000 n +0000621778 00000 n +0000622360 00000 n +0000622514 00000 n +0000622668 00000 n +0000622822 00000 n +0000622976 00000 n +0000623130 00000 n +0000623284 00000 n +0000623540 00000 n +0000623696 00000 n +0000623900 00000 n +0000624148 00000 n +0000624302 00000 n +0000624456 00000 n +0000624610 00000 n +0000624764 00000 n +0000624918 00000 n +0000625072 00000 n +0000625681 00000 n +0000626259 00000 n +0000626413 00000 n +0000626567 00000 n +0000627176 00000 n +0000627754 00000 n +0000627908 00000 n +0000628062 00000 n +0000628318 00000 n +0000628574 00000 n +0000628830 00000 n +0000629086 00000 n +0000629240 00000 n +0000629394 00000 n +0000630006 00000 n +0000630838 00000 n +0000631418 00000 n +0000631789 00000 n +0000632230 00000 n +0000633185 00000 n +0000633591 00000 n +0000633663 00000 n +0000634415 00000 n +0000634440 00000 n +0000634492 00000 n +0000634712 00000 n +0000634759 00000 n +0000634838 00000 n +0000634917 00000 n +0000635008 00000 n +0000635180 00000 n +0000635232 00000 n +0000635312 00000 n +0000635359 00000 n +0000635438 00000 n +0000635517 00000 n +0000635589 00000 n +0000645447 00000 n +0000653971 00000 n +0000655551 00000 n +0000657572 00000 n +0000659579 00000 n +0000662293 00000 n +0000662538 00000 n +0000662890 00000 n +0000663135 00000 n +0000663510 00000 n +0000665046 00000 n +0000668534 00000 n +0000671248 00000 n +0000671706 00000 n +0000671958 00000 n +0000672203 00000 n +0000672454 00000 n +0000686034 00000 n +0000691530 00000 n +0000700235 00000 n +0000706794 00000 n +0000714786 00000 n +0000716767 00000 n +0000731609 00000 n +0000731650 00000 n +0000733772 00000 n +0000733989 00000 n +0000734987 00000 n +0000735019 00000 n +0000736810 00000 n +0000737019 00000 n +0000737563 00000 n +0000737594 00000 n +0000738684 00000 n +0000738890 00000 n +0000739426 00000 n +0000739457 00000 n +0000741248 00000 n +0000741456 00000 n +0000742003 00000 n +0000742033 00000 n +0000743123 00000 n +0000743328 00000 n +0000743866 00000 n +0000743898 00000 n +0000744988 00000 n +0000745196 00000 n +0000745734 00000 n +0000747525 00000 n +0000748069 00000 n +0000750191 00000 n +0000751189 00000 n +0000752279 00000 n +0000752817 00000 n +0000754608 00000 n +0000755152 00000 n +0000757274 00000 n +0000758272 00000 n +0000760394 00000 n +0000761392 00000 n +0000763514 00000 n +0000764512 00000 n +0000766634 00000 n +0000767632 00000 n +0000768722 00000 n +0000769258 00000 n +0000769289 00000 n +0000771080 00000 n +0000771288 00000 n +0000771833 00000 n +0000771864 00000 n +0000772954 00000 n +0000773161 00000 n +0000773699 00000 n +0000773730 00000 n +0000774820 00000 n +0000775027 00000 n +0000775595 00000 n +0000775626 00000 n +0000776716 00000 n +0000776921 00000 n +0000777455 00000 n +0000777485 00000 n +0000778575 00000 n +0000778780 00000 n +0000779316 00000 n +0000781438 00000 n +0000782436 00000 n +0000784558 00000 n +0000785556 00000 n +0000787678 00000 n +0000788676 00000 n +0000790798 00000 n +0000791796 00000 n +0000793918 00000 n +0000794916 00000 n +0000849284 00000 n +0000849336 00000 n +0000849524 00000 n +0000849576 00000 n +0000849764 00000 n +0000849811 00000 n +0000849975 00000 n +0000850027 00000 n +0000850120 00000 n +0000850180 00000 n +0000850232 00000 n +0000850325 00000 n +0000850421 00000 n +0000855406 00000 n +0000862701 00000 n +0000873922 00000 n +0000879532 00000 n +0000881914 00000 n +0000925703 00000 n +0000953794 00000 n +0000985744 00000 n +0001013987 00000 n +0001042959 00000 n +0001071233 00000 n +0001099205 00000 n +0001127709 00000 n +0001156286 00000 n +0001185207 00000 n +0001214138 00000 n +0001214190 00000 n +0001214366 00000 n +0001214418 00000 n +trailer << /Info 2 0 R /Root 1 0 R /Size 863 /ID [<96eff7b33f17172214393742ca5ac9ea>] >> +startxref +1214642 +%%EOF diff --git a/data/PrideandPrejudice.txt b/data/PrideandPrejudice.txt new file mode 100644 index 0000000..2712426 --- /dev/null +++ b/data/PrideandPrejudice.txt @@ -0,0 +1,14905 @@ +ο»ΏThe Project Gutenberg eBook of Pride and Prejudice + +This ebook is for the use of anyone anywhere in the United States and +most other parts of the world at no cost and with almost no restrictions +whatsoever. You may copy it, give it away or re-use it under the terms +of the Project Gutenberg License included with this ebook or online +at www.gutenberg.org. If you are not located in the United States, +you will have to check the laws of the country where you are located +before using this eBook. + +Title: Pride and Prejudice + +Author: Jane Austen + +Release date: June 1, 1998 [eBook #1342] + Most recently updated: October 29, 2024 + +Language: English + +Credits: Chuck Greif and the Online Distributed Proofreading Team at http://www.pgdp.net (This file was produced from images available at The Internet Archive) + + +*** START OF THE PROJECT GUTENBERG EBOOK PRIDE AND PREJUDICE *** + [Illustration: + + GEORGE ALLEN + PUBLISHER + + 156 CHARING CROSS ROAD + LONDON + + RUSKIN HOUSE + ] + + [Illustration: + + _Reading Jane’s Letters._ _Chap 34._ + ] + + + + + PRIDE. + and + PREJUDICE + + by + Jane Austen, + + with a Preface by + George Saintsbury + and + Illustrations by + Hugh Thomson + + [Illustration: 1894] + + Ruskin 156. Charing + House. Cross Road. + + London + George Allen. + + + + + CHISWICK PRESS:--CHARLES WHITTINGHAM AND CO. + TOOKS COURT, CHANCERY LANE, LONDON. + + + + + [Illustration: + + _To J. Comyns Carr + in acknowledgment of all I + owe to his friendship and + advice, these illustrations are + gratefully inscribed_ + + _Hugh Thomson_ + ] + + + + +PREFACE. + +[Illustration] + + +_Walt Whitman has somewhere a fine and just distinction between β€œloving +by allowance” and β€œloving with personal love.” This distinction applies +to books as well as to men and women; and in the case of the not very +numerous authors who are the objects of the personal affection, it +brings a curious consequence with it. There is much more difference as +to their best work than in the case of those others who are loved β€œby +allowance” by convention, and because it is felt to be the right and +proper thing to love them. And in the sect--fairly large and yet +unusually choice--of Austenians or Janites, there would probably be +found partisans of the claim to primacy of almost every one of the +novels. To some the delightful freshness and humour of_ Northanger +Abbey, _its completeness, finish, and_ entrain, _obscure the undoubted +critical facts that its scale is small, and its scheme, after all, that +of burlesque or parody, a kind in which the first rank is reached with +difficulty._ Persuasion, _relatively faint in tone, and not enthralling +in interest, has devotees who exalt above all the others its exquisite +delicacy and keeping. The catastrophe of_ Mansfield Park _is admittedly +theatrical, the hero and heroine are insipid, and the author has almost +wickedly destroyed all romantic interest by expressly admitting that +Edmund only took Fanny because Mary shocked him, and that Fanny might +very likely have taken Crawford if he had been a little more assiduous; +yet the matchless rehearsal-scenes and the characters of Mrs. Norris and +others have secured, I believe, a considerable party for it._ Sense and +Sensibility _has perhaps the fewest out-and-out admirers; but it does +not want them._ + +_I suppose, however, that the majority of at least competent votes +would, all things considered, be divided between_ Emma _and the present +book; and perhaps the vulgar verdict (if indeed a fondness for Miss +Austen be not of itself a patent of exemption from any possible charge +of vulgarity) would go for_ Emma. _It is the larger, the more varied, the +more popular; the author had by the time of its composition seen rather +more of the world, and had improved her general, though not her most +peculiar and characteristic dialogue; such figures as Miss Bates, as the +Eltons, cannot but unite the suffrages of everybody. On the other hand, +I, for my part, declare for_ Pride and Prejudice _unhesitatingly. It +seems to me the most perfect, the most characteristic, the most +eminently quintessential of its author’s works; and for this contention +in such narrow space as is permitted to me, I propose here to show +cause._ + +_In the first place, the book (it may be barely necessary to remind the +reader) was in its first shape written very early, somewhere about 1796, +when Miss Austen was barely twenty-one; though it was revised and +finished at Chawton some fifteen years later, and was not published till +1813, only four years before her death. I do not know whether, in this +combination of the fresh and vigorous projection of youth, and the +critical revision of middle life, there may be traced the distinct +superiority in point of construction, which, as it seems to me, it +possesses over all the others. The plot, though not elaborate, is almost +regular enough for Fielding; hardly a character, hardly an incident +could be retrenched without loss to the story. The elopement of Lydia +and Wickham is not, like that of Crawford and Mrs. Rushworth, a_ coup de +théÒtre; _it connects itself in the strictest way with the course of the +story earlier, and brings about the denouement with complete propriety. +All the minor passages--the loves of Jane and Bingley, the advent of Mr. +Collins, the visit to Hunsford, the Derbyshire tour--fit in after the +same unostentatious, but masterly fashion. There is no attempt at the +hide-and-seek, in-and-out business, which in the transactions between +Frank Churchill and Jane Fairfax contributes no doubt a good deal to the +intrigue of_ Emma, _but contributes it in a fashion which I do not think +the best feature of that otherwise admirable book. Although Miss Austen +always liked something of the misunderstanding kind, which afforded her +opportunities for the display of the peculiar and incomparable talent to +be noticed presently, she has been satisfied here with the perfectly +natural occasions provided by the false account of Darcy’s conduct given +by Wickham, and by the awkwardness (arising with equal naturalness) from +the gradual transformation of Elizabeth’s own feelings from positive +aversion to actual love. I do not know whether the all-grasping hand of +the playwright has ever been laid upon_ Pride and Prejudice; _and I dare +say that, if it were, the situations would prove not startling or +garish enough for the footlights, the character-scheme too subtle and +delicate for pit and gallery. But if the attempt were made, it would +certainly not be hampered by any of those loosenesses of construction, +which, sometimes disguised by the conveniences of which the novelist can +avail himself, appear at once on the stage._ + +_I think, however, though the thought will doubtless seem heretical to +more than one school of critics, that construction is not the highest +merit, the choicest gift, of the novelist. It sets off his other gifts +and graces most advantageously to the critical eye; and the want of it +will sometimes mar those graces--appreciably, though not quite +consciously--to eyes by no means ultra-critical. But a very badly-built +novel which excelled in pathetic or humorous character, or which +displayed consummate command of dialogue--perhaps the rarest of all +faculties--would be an infinitely better thing than a faultless plot +acted and told by puppets with pebbles in their mouths. And despite the +ability which Miss Austen has shown in working out the story, I for one +should put_ Pride and Prejudice _far lower if it did not contain what +seem to me the very masterpieces of Miss Austen’s humour and of her +faculty of character-creation--masterpieces who may indeed admit John +Thorpe, the Eltons, Mrs. Norris, and one or two others to their company, +but who, in one instance certainly, and perhaps in others, are still +superior to them._ + +_The characteristics of Miss Austen’s humour are so subtle and delicate +that they are, perhaps, at all times easier to apprehend than to +express, and at any particular time likely to be differently +apprehended by different persons. To me this humour seems to possess a +greater affinity, on the whole, to that of Addison than to any other of +the numerous species of this great British genus. The differences of +scheme, of time, of subject, of literary convention, are, of course, +obvious enough; the difference of sex does not, perhaps, count for much, +for there was a distinctly feminine element in β€œMr. Spectator,” and in +Jane Austen’s genius there was, though nothing mannish, much that was +masculine. But the likeness of quality consists in a great number of +common subdivisions of quality--demureness, extreme minuteness of touch, +avoidance of loud tones and glaring effects. Also there is in both a +certain not inhuman or unamiable cruelty. It is the custom with those +who judge grossly to contrast the good nature of Addison with the +savagery of Swift, the mildness of Miss Austen with the boisterousness +of Fielding and Smollett, even with the ferocious practical jokes that +her immediate predecessor, Miss Burney, allowed without very much +protest. Yet, both in Mr. Addison and in Miss Austen there is, though a +restrained and well-mannered, an insatiable and ruthless delight in +roasting and cutting up a fool. A man in the early eighteenth century, +of course, could push this taste further than a lady in the early +nineteenth; and no doubt Miss Austen’s principles, as well as her heart, +would have shrunk from such things as the letter from the unfortunate +husband in the_ Spectator, _who describes, with all the gusto and all the +innocence in the world, how his wife and his friend induce him to play +at blind-man’s-buff. But another_ Spectator _letter--that of the damsel +of fourteen who wishes to marry Mr. Shapely, and assures her selected +Mentor that β€œhe admires your_ Spectators _mightily”--might have been +written by a rather more ladylike and intelligent Lydia Bennet in the +days of Lydia’s great-grandmother; while, on the other hand, some (I +think unreasonably) have found β€œcynicism” in touches of Miss Austen’s +own, such as her satire of Mrs. Musgrove’s self-deceiving regrets over +her son. But this word β€œcynical” is one of the most misused in the +English language, especially when, by a glaring and gratuitous +falsification of its original sense, it is applied, not to rough and +snarling invective, but to gentle and oblique satire. If cynicism means +the perception of β€œthe other side,” the sense of β€œthe accepted hells +beneath,” the consciousness that motives are nearly always mixed, and +that to seem is not identical with to be--if this be cynicism, then +every man and woman who is not a fool, who does not care to live in a +fool’s paradise, who has knowledge of nature and the world and life, is +a cynic. And in that sense Miss Austen certainly was one. She may even +have been one in the further sense that, like her own Mr. Bennet, she +took an epicurean delight in dissecting, in displaying, in setting at +work her fools and her mean persons. I think she did take this delight, +and I do not think at all the worse of her for it as a woman, while she +was immensely the better for it as an artist._ + +_In respect of her art generally, Mr. Goldwin Smith has truly observed +that β€œmetaphor has been exhausted in depicting the perfection of it, +combined with the narrowness of her field;” and he has justly added that +we need not go beyond her own comparison to the art of a miniature +painter. To make this latter observation quite exact we must not use the +term miniature in its restricted sense, and must think rather of Memling +at one end of the history of painting and Meissonier at the other, than +of Cosway or any of his kind. And I am not so certain that I should +myself use the word β€œnarrow” in connection with her. If her world is a +microcosm, the cosmic quality of it is at least as eminent as the +littleness. She does not touch what she did not feel herself called to +paint; I am not so sure that she could not have painted what she did not +feel herself called to touch. It is at least remarkable that in two very +short periods of writing--one of about three years, and another of not +much more than five--she executed six capital works, and has not left a +single failure. It is possible that the romantic paste in her +composition was defective: we must always remember that hardly +anybody born in her decade--that of the eighteenth-century +seventies--independently exhibited the full romantic quality. Even Scott +required hill and mountain and ballad, even Coleridge metaphysics and +German to enable them to chip the classical shell. Miss Austen was an +English girl, brought up in a country retirement, at the time when +ladies went back into the house if there was a white frost which might +pierce their kid shoes, when a sudden cold was the subject of the +gravest fears, when their studies, their ways, their conduct were +subject to all those fantastic limits and restrictions against which +Mary Wollstonecraft protested with better general sense than particular +taste or judgment. Miss Austen, too, drew back when the white frost +touched her shoes; but I think she would have made a pretty good journey +even in a black one._ + +_For if her knowledge was not very extended, she knew two things which +only genius knows. The one was humanity, and the other was art. On the +first head she could not make a mistake; her men, though limited, are +true, and her women are, in the old sense, β€œabsolute.” As to art, if she +has never tried idealism, her realism is real to a degree which makes +the false realism of our own day look merely dead-alive. Take almost any +Frenchman, except the late M. de Maupassant, and watch him laboriously +piling up strokes in the hope of giving a complete impression. You get +none; you are lucky if, discarding two-thirds of what he gives, you can +shape a real impression out of the rest. But with Miss Austen the +myriad, trivial, unforced strokes build up the picture like magic. +Nothing is false; nothing is superfluous. When (to take the present book +only) Mr. Collins changed his mind from Jane to Elizabeth β€œwhile Mrs. +Bennet was stirring the fire” (and we know_ how _Mrs. Bennet would have +stirred the fire), when Mr. Darcy β€œbrought his coffee-cup back_ +himself,” _the touch in each case is like that of Swift--β€œtaller by the +breadth of my nail”--which impressed the half-reluctant Thackeray with +just and outspoken admiration. Indeed, fantastic as it may seem, I +should put Miss Austen as near to Swift in some ways, as I have put her +to Addison in others._ + +_This Swiftian quality appears in the present novel as it appears +nowhere else in the character of the immortal, the ineffable Mr. +Collins. Mr. Collins is really_ great; _far greater than anything Addison +ever did, almost great enough for Fielding or for Swift himself. It has +been said that no one ever was like him. But in the first place,_ he +_was like him; he is there--alive, imperishable, more real than hundreds +of prime ministers and archbishops, of β€œmetals, semi-metals, and +distinguished philosophers.” In the second place, it is rash, I think, +to conclude that an actual Mr. Collins was impossible or non-existent at +the end of the eighteenth century. It is very interesting that we +possess, in this same gallery, what may be called a spoiled first +draught, or an unsuccessful study of him, in John Dashwood. The +formality, the under-breeding, the meanness, are there; but the portrait +is only half alive, and is felt to be even a little unnatural. Mr. +Collins is perfectly natural, and perfectly alive. In fact, for all the +β€œminiature,” there is something gigantic in the way in which a certain +side, and more than one, of humanity, and especially eighteenth-century +humanity, its Philistinism, its well-meaning but hide-bound morality, +its formal pettiness, its grovelling respect for rank, its materialism, +its selfishness, receives exhibition. I will not admit that one speech +or one action of this inestimable man is incapable of being reconciled +with reality, and I should not wonder if many of these words and actions +are historically true._ + +_But the greatness of Mr. Collins could not have been so satisfactorily +exhibited if his creatress had not adjusted so artfully to him the +figures of Mr. Bennet and of Lady Catherine de Bourgh. The latter, like +Mr. Collins himself, has been charged with exaggeration. There is, +perhaps, a very faint shade of colour for the charge; but it seems to me +very faint indeed. Even now I do not think that it would be impossible +to find persons, especially female persons, not necessarily of noble +birth, as overbearing, as self-centred, as neglectful of good manners, +as Lady Catherine. A hundred years ago, an earl’s daughter, the Lady +Powerful (if not exactly Bountiful) of an out-of-the-way country parish, +rich, long out of marital authority, and so forth, had opportunities of +developing these agreeable characteristics which seldom present +themselves now. As for Mr. Bennet, Miss Austen, and Mr. Darcy, and even +Miss Elizabeth herself, were, I am inclined to think, rather hard on him +for the β€œimpropriety” of his conduct. His wife was evidently, and must +always have been, a quite irreclaimable fool; and unless he had shot her +or himself there was no way out of it for a man of sense and spirit but +the ironic. From no other point of view is he open to any reproach, +except for an excusable and not unnatural helplessness at the crisis of +the elopement, and his utterances are the most acutely delightful in the +consciously humorous kind--in the kind that we laugh with, not at--that +even Miss Austen has put into the mouth of any of her characters. It is +difficult to know whether he is most agreeable when talking to his wife, +or when putting Mr. Collins through his paces; but the general sense of +the world has probably been right in preferring to the first rank his +consolation to the former when she maunders over the entail, β€œMy dear, +do not give way to such gloomy thoughts. Let us hope for better things. +Let us flatter ourselves that_ I _may be the survivor;” and his inquiry +to his colossal cousin as to the compliments which Mr. Collins has just +related as made by himself to Lady Catherine, β€œMay I ask whether these +pleasing attentions proceed from the impulse of the moment, or are the +result of previous study?” These are the things which give Miss Austen’s +readers the pleasant shocks, the delightful thrills, which are felt by +the readers of Swift, of Fielding, and we may here add, of Thackeray, as +they are felt by the readers of no other English author of fiction +outside of these four._ + +_The goodness of the minor characters in_ Pride and Prejudice _has been +already alluded to, and it makes a detailed dwelling on their beauties +difficult in any space, and impossible in this. Mrs. Bennet we have +glanced at, and it is not easy to say whether she is more exquisitely +amusing or more horribly true. Much the same may be said of Kitty and +Lydia; but it is not every author, even of genius, who would have +differentiated with such unerring skill the effects of folly and +vulgarity of intellect and disposition working upon the common +weaknesses of woman at such different ages. With Mary, Miss Austen has +taken rather less pains, though she has been even more unkind to her; +not merely in the text, but, as we learn from those interesting +traditional appendices which Mr. Austen Leigh has given us, in dooming +her privately to marry β€œone of Mr. Philips’s clerks.” The habits of +first copying and then retailing moral sentiments, of playing and +singing too long in public, are, no doubt, grievous and criminal; but +perhaps poor Mary was rather the scapegoat of the sins of blue stockings +in that Fordyce-belectured generation. It is at any rate difficult not +to extend to her a share of the respect and affection (affection and +respect of a peculiar kind; doubtless), with which one regards Mr. +Collins, when she draws the moral of Lydia’s fall. I sometimes wish +that the exigencies of the story had permitted Miss Austen to unite +these personages, and thus at once achieve a notable mating and soothe +poor Mrs. Bennet’s anguish over the entail._ + +_The Bingleys and the Gardiners and the Lucases, Miss Darcy and Miss de +Bourgh, Jane, Wickham, and the rest, must pass without special comment, +further than the remark that Charlotte Lucas (her egregious papa, though +delightful, is just a little on the thither side of the line between +comedy and farce) is a wonderfully clever study in drab of one kind, and +that Wickham (though something of Miss Austen’s hesitation of touch in +dealing with young men appears) is a not much less notable sketch in +drab of another. Only genius could have made Charlotte what she is, yet +not disagreeable; Wickham what he is, without investing him either with +a cheap Don Juanish attractiveness or a disgusting rascality. But the +hero and the heroine are not tints to be dismissed._ + +_Darcy has always seemed to me by far the best and most interesting of +Miss Austen’s heroes; the only possible competitor being Henry Tilney, +whose part is so slight and simple that it hardly enters into +comparison. It has sometimes, I believe, been urged that his pride is +unnatural at first in its expression and later in its yielding, while +his falling in love at all is not extremely probable. Here again I +cannot go with the objectors. Darcy’s own account of the way in which +his pride had been pampered, is perfectly rational and sufficient; and +nothing could be, psychologically speaking, a_ causa verior _for its +sudden restoration to healthy conditions than the shock of Elizabeth’s +scornful refusal acting on a nature_ ex hypothesi _generous. Nothing in +even our author is finer and more delicately touched than the change of +his demeanour at the sudden meeting in the grounds of Pemberley. Had he +been a bad prig or a bad coxcomb, he might have been still smarting +under his rejection, or suspicious that the girl had come +husband-hunting. His being neither is exactly consistent with the +probable feelings of a man spoilt in the common sense, but not really +injured in disposition, and thoroughly in love. As for his being in +love, Elizabeth has given as just an exposition of the causes of that +phenomenon as Darcy has of the conditions of his unregenerate state, +only she has of course not counted in what was due to her own personal +charm._ + +_The secret of that charm many men and not a few women, from Miss Austen +herself downwards, have felt, and like most charms it is a thing rather +to be felt than to be explained. Elizabeth of course belongs to the_ +allegro _or_ allegra _division of the army of Venus. Miss Austen was +always provokingly chary of description in regard to her beauties; and +except the fine eyes, and a hint or two that she had at any rate +sometimes a bright complexion, and was not very tall, we hear nothing +about her looks. But her chief difference from other heroines of the +lively type seems to lie first in her being distinctly clever--almost +strong-minded, in the better sense of that objectionable word--and +secondly in her being entirely destitute of ill-nature for all her +propensity to tease and the sharpness of her tongue. Elizabeth can give +at least as good as she gets when she is attacked; but she never +β€œscratches,” and she never attacks first. Some of the merest +obsoletenesses of phrase and manner give one or two of her early +speeches a slight pertness, but that is nothing, and when she comes to +serious business, as in the great proposal scene with Darcy (which is, +as it should be, the climax of the interest of the book), and in the +final ladies’ battle with Lady Catherine, she is unexceptionable. Then +too she is a perfectly natural girl. She does not disguise from herself +or anybody that she resents Darcy’s first ill-mannered personality with +as personal a feeling. (By the way, the reproach that the ill-manners of +this speech are overdone is certainly unjust; for things of the same +kind, expressed no doubt less stiltedly but more coarsely, might have +been heard in more than one ball-room during this very year from persons +who ought to have been no worse bred than Darcy.) And she lets the +injury done to Jane and the contempt shown to the rest of her family +aggravate this resentment in the healthiest way in the world._ + +_Still, all this does not explain her charm, which, taking beauty as a +common form of all heroines, may perhaps consist in the addition to her +playfulness, her wit, her affectionate and natural disposition, of a +certain fearlessness very uncommon in heroines of her type and age. +Nearly all of them would have been in speechless awe of the magnificent +Darcy; nearly all of them would have palpitated and fluttered at the +idea of proposals, even naughty ones, from the fascinating Wickham. +Elizabeth, with nothing offensive, nothing_ viraginous, _nothing of the +β€œNew Woman” about her, has by nature what the best modern (not β€œnew”) +women have by education and experience, a perfect freedom from the idea +that all men may bully her if they choose, and that most will away with +her if they can. Though not in the least β€œimpudent and mannish grown,” +she has no mere sensibility, no nasty niceness about her. The form of +passion common and likely to seem natural in Miss Austen’s day was so +invariably connected with the display of one or the other, or both of +these qualities, that she has not made Elizabeth outwardly passionate. +But I, at least, have not the slightest doubt that she would have +married Darcy just as willingly without Pemberley as with it, and +anybody who can read between lines will not find the lovers’ +conversations in the final chapters so frigid as they might have looked +to the Della Cruscans of their own day, and perhaps do look to the Della +Cruscans of this._ + +_And, after all, what is the good of seeking for the reason of +charm?--it is there. There were better sense in the sad mechanic +exercise of determining the reason of its absence where it is not. In +the novels of the last hundred years there are vast numbers of young +ladies with whom it might be a pleasure to fall in love; there are at +least five with whom, as it seems to me, no man of taste and spirit can +help doing so. Their names are, in chronological order, Elizabeth +Bennet, Diana Vernon, Argemone Lavington, Beatrix Esmond, and Barbara +Grant. I should have been most in love with Beatrix and Argemone; I +should, I think, for mere occasional companionship, have preferred Diana +and Barbara. But to live with and to marry, I do not know that any one +of the four can come into competition with Elizabeth._ + +_GEORGE SAINTSBURY._ + + + + +[Illustration: List of Illustrations.] + + + PAGE + +Frontispiece iv + +Title-page v + +Dedication vii + +Heading to Preface ix + +Heading to List of Illustrations xxv + +Heading to Chapter I. 1 + +β€œHe came down to see the place” 2 + +Mr. and Mrs. Bennet 5 + +β€œI hope Mr. Bingley will like it” 6 + +β€œI’m the tallest” 9 + +β€œHe rode a black horse” 10 + +β€œWhen the party entered” 12 + +β€œShe is tolerable” 15 + +Heading to Chapter IV. 18 + +Heading to Chapter V. 22 + +β€œWithout once opening his lips” 24 + +Tailpiece to Chapter V. 26 + +Heading to Chapter VI. 27 + +β€œThe entreaties of several” 31 + +β€œA note for Miss Bennet” 36 + +β€œCheerful prognostics” 40 + +β€œThe apothecary came” 43 + +β€œCovering a screen” 45 + +β€œMrs. Bennet and her two youngest girls” 53 + +Heading to Chapter X. 60 + +β€œNo, no; stay where you are” 67 + +β€œPiling up the fire” 69 + +Heading to Chapter XII. 75 + +Heading to Chapter XIII. 78 + +Heading to Chapter XIV. 84 + +β€œProtested that he never read novels” 87 + +Heading to Chapter XV. 89 + +Heading to Chapter XVI. 95 + +β€œThe officers of the ----shire” 97 + +β€œDelighted to see their dear friend again” 108 + +Heading to Chapter XVIII. 113 + +β€œSuch very superior dancing is not often seen” 118 + +β€œTo assure you in the most animated language” 132 + +Heading to Chapter XX. 139 + +β€œThey entered the breakfast-room” 143 + +Heading to Chapter XXI. 146 + +β€œWalked back with them” 148 + +Heading to Chapter XXII. 154 + +β€œSo much love and eloquence” 156 + +β€œProtested he must be entirely mistaken” 161 + +β€œWhenever she spoke in a low voice” 166 + +Heading to Chapter XXIV. 168 + +Heading to Chapter XXV. 175 + +β€œOffended two or three young ladies” 177 + +β€œWill you come and see me?” 181 + +β€œOn the stairs” 189 + +β€œAt the door” 194 + +β€œIn conversation with the ladies” 198 + +β€œLady Catherine,” said she, β€œyou have given me a treasure” 200 + +Heading to Chapter XXX. 209 + +β€œHe never failed to inform them” 211 + +β€œThe gentlemen accompanied him” 213 + +Heading to Chapter XXXI. 215 + +Heading to Chapter XXXII. 221 + +β€œAccompanied by their aunt” 225 + +β€œOn looking up” 228 + +Heading to Chapter XXXIV. 235 + +β€œHearing herself called” 243 + +Heading to Chapter XXXVI. 253 + +β€œMeeting accidentally in town” 256 + +β€œHis parting obeisance” 261 + +β€œDawson” 263 + +β€œThe elevation of his feelings” 267 + +β€œThey had forgotten to leave any message” 270 + +β€œHow nicely we are crammed in!” 272 + +Heading to Chapter XL. 278 + +β€œI am determined never to speak of it again” 283 + +β€œWhen Colonel Miller’s regiment went away” 285 + +β€œTenderly flirting” 290 + +The arrival of the Gardiners 294 + +β€œConjecturing as to the date” 301 + +Heading to Chapter XLIV. 318 + +β€œTo make herself agreeable to all” 321 + +β€œEngaged by the river” 327 + +Heading to Chapter XLVI. 334 + +β€œI have not an instant to lose” 339 + +β€œThe first pleasing earnest of their welcome” 345 + +The Post 359 + +β€œTo whom I have related the affair” 363 + +Heading to Chapter XLIX. 368 + +β€œBut perhaps you would like to read it” 370 + +β€œThe spiteful old ladies” 377 + +β€œWith an affectionate smile” 385 + +β€œI am sure she did not listen” 393 + +β€œMr. Darcy with him” 404 + +β€œJane happened to look round” 415 + +β€œMrs. Long and her nieces” 420 + +β€œLizzy, my dear, I want to speak to you” 422 + +Heading to Chapter LVI. 431 + +β€œAfter a short survey” 434 + +β€œBut now it comes out” 442 + +β€œThe efforts of his aunt” 448 + +β€œUnable to utter a syllable” 457 + +β€œThe obsequious civility” 466 + +Heading to Chapter LXI. 472 + +The End 476 + + + + +[Illustration: Β·PRIDE AND PREJUDICEΒ· + + + + +Chapter I.] + + +It is a truth universally acknowledged, that a single man in possession +of a good fortune must be in want of a wife. + +However little known the feelings or views of such a man may be on his +first entering a neighbourhood, this truth is so well fixed in the minds +of the surrounding families, that he is considered as the rightful +property of some one or other of their daughters. + +β€œMy dear Mr. Bennet,” said his lady to him one day, β€œhave you heard that +Netherfield Park is let at last?” + +Mr. Bennet replied that he had not. + +β€œBut it is,” returned she; β€œfor Mrs. Long has just been here, and she +told me all about it.” + +Mr. Bennet made no answer. + +β€œDo not you want to know who has taken it?” cried his wife, impatiently. + +β€œ_You_ want to tell me, and I have no objection to hearing it.” + +[Illustration: + +β€œHe came down to see the place” + +[_Copyright 1894 by George Allen._]] + +This was invitation enough. + +β€œWhy, my dear, you must know, Mrs. Long says that Netherfield is taken +by a young man of large fortune from the north of England; that he came +down on Monday in a chaise and four to see the place, and was so much +delighted with it that he agreed with Mr. Morris immediately; that he is +to take possession before Michaelmas, and some of his servants are to be +in the house by the end of next week.” + +β€œWhat is his name?” + +β€œBingley.” + +β€œIs he married or single?” + +β€œOh, single, my dear, to be sure! A single man of large fortune; four or +five thousand a year. What a fine thing for our girls!” + +β€œHow so? how can it affect them?” + +β€œMy dear Mr. Bennet,” replied his wife, β€œhow can you be so tiresome? You +must know that I am thinking of his marrying one of them.” + +β€œIs that his design in settling here?” + +β€œDesign? Nonsense, how can you talk so! But it is very likely that he +_may_ fall in love with one of them, and therefore you must visit him as +soon as he comes.” + +β€œI see no occasion for that. You and the girls may go--or you may send +them by themselves, which perhaps will be still better; for as you are +as handsome as any of them, Mr. Bingley might like you the best of the +party.” + +β€œMy dear, you flatter me. I certainly _have_ had my share of beauty, but +I do not pretend to be anything extraordinary now. When a woman has five +grown-up daughters, she ought to give over thinking of her own beauty.” + +β€œIn such cases, a woman has not often much beauty to think of.” + +β€œBut, my dear, you must indeed go and see Mr. Bingley when he comes into +the neighbourhood.” + +β€œIt is more than I engage for, I assure you.” + +β€œBut consider your daughters. Only think what an establishment it would +be for one of them. Sir William and Lady Lucas are determined to go, +merely on that account; for in general, you know, they visit no new +comers. Indeed you must go, for it will be impossible for _us_ to visit +him, if you do not.” + +β€œYou are over scrupulous, surely. I dare say Mr. Bingley will be very +glad to see you; and I will send a few lines by you to assure him of my +hearty consent to his marrying whichever he chooses of the girls--though +I must throw in a good word for my little Lizzy.” + +β€œI desire you will do no such thing. Lizzy is not a bit better than the +others: and I am sure she is not half so handsome as Jane, nor half so +good-humoured as Lydia. But you are always giving _her_ the preference.” + +β€œThey have none of them much to recommend them,” replied he: β€œthey are +all silly and ignorant like other girls; but Lizzy has something more of +quickness than her sisters.” + +β€œMr. Bennet, how can you abuse your own children in such a way? You take +delight in vexing me. You have no compassion on my poor nerves.” + +β€œYou mistake me, my dear. I have a high respect for your nerves. They +are my old friends. I have heard you mention them with consideration +these twenty years at least.” + +β€œAh, you do not know what I suffer.” + +β€œBut I hope you will get over it, and live to see many young men of four +thousand a year come into the neighbourhood.” + +β€œIt will be no use to us, if twenty such should come, since you will not +visit them.” + +β€œDepend upon it, my dear, that when there are twenty, I will visit them +all.” + +Mr. Bennet was so odd a mixture of quick parts, sarcastic humour, +reserve, and caprice, that the experience of three-and-twenty years had +been insufficient to make his wife understand his character. _Her_ mind +was less difficult to develope. She was a woman of mean understanding, +little information, and uncertain temper. When she was discontented, she +fancied herself nervous. The business of her life was to get her +daughters married: its solace was visiting and news. + +[Illustration: M^{r.} & M^{rs.} Bennet + +[_Copyright 1894 by George Allen._]] + + + + +[Illustration: + +β€œI hope Mr. Bingley will like it” + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER II. + + +[Illustration] + +Mr. Bennet was among the earliest of those who waited on Mr. Bingley. He +had always intended to visit him, though to the last always assuring his +wife that he should not go; and till the evening after the visit was +paid she had no knowledge of it. It was then disclosed in the following +manner. Observing his second daughter employed in trimming a hat, he +suddenly addressed her with,-- + +β€œI hope Mr. Bingley will like it, Lizzy.” + +β€œWe are not in a way to know _what_ Mr. Bingley likes,” said her mother, +resentfully, β€œsince we are not to visit.” + +β€œBut you forget, mamma,” said Elizabeth, β€œthat we shall meet him at the +assemblies, and that Mrs. Long has promised to introduce him.” + +β€œI do not believe Mrs. Long will do any such thing. She has two nieces +of her own. She is a selfish, hypocritical woman, and I have no opinion +of her.” + +β€œNo more have I,” said Mr. Bennet; β€œand I am glad to find that you do +not depend on her serving you.” + +Mrs. Bennet deigned not to make any reply; but, unable to contain +herself, began scolding one of her daughters. + +β€œDon’t keep coughing so, Kitty, for heaven’s sake! Have a little +compassion on my nerves. You tear them to pieces.” + +β€œKitty has no discretion in her coughs,” said her father; β€œshe times +them ill.” + +β€œI do not cough for my own amusement,” replied Kitty, fretfully. β€œWhen +is your next ball to be, Lizzy?” + +β€œTo-morrow fortnight.” + +β€œAy, so it is,” cried her mother, β€œand Mrs. Long does not come back till +the day before; so, it will be impossible for her to introduce him, for +she will not know him herself.” + +β€œThen, my dear, you may have the advantage of your friend, and introduce +Mr. Bingley to _her_.” + +β€œImpossible, Mr. Bennet, impossible, when I am not acquainted with him +myself; how can you be so teasing?” + +β€œI honour your circumspection. A fortnight’s acquaintance is certainly +very little. One cannot know what a man really is by the end of a +fortnight. But if _we_ do not venture, somebody else will; and after +all, Mrs. Long and her nieces must stand their chance; and, therefore, +as she will think it an act of kindness, if you decline the office, I +will take it on myself.” + +The girls stared at their father. Mrs. Bennet said only, β€œNonsense, +nonsense!” + +β€œWhat can be the meaning of that emphatic exclamation?” cried he. β€œDo +you consider the forms of introduction, and the stress that is laid on +them, as nonsense? I cannot quite agree with you _there_. What say you, +Mary? For you are a young lady of deep reflection, I know, and read +great books, and make extracts.” + +Mary wished to say something very sensible, but knew not how. + +β€œWhile Mary is adjusting her ideas,” he continued, β€œlet us return to Mr. +Bingley.” + +β€œI am sick of Mr. Bingley,” cried his wife. + +β€œI am sorry to hear _that_; but why did you not tell me so before? If I +had known as much this morning, I certainly would not have called on +him. It is very unlucky; but as I have actually paid the visit, we +cannot escape the acquaintance now.” + +The astonishment of the ladies was just what he wished--that of Mrs. +Bennet perhaps surpassing the rest; though when the first tumult of joy +was over, she began to declare that it was what she had expected all the +while. + +β€œHow good it was in you, my dear Mr. Bennet! But I knew I should +persuade you at last. I was sure you loved your girls too well to +neglect such an acquaintance. Well, how pleased I am! And it is such a +good joke, too, that you should have gone this morning, and never said a +word about it till now.” + +β€œNow, Kitty, you may cough as much as you choose,” said Mr. Bennet; and, +as he spoke, he left the room, fatigued with the raptures of his wife. + +β€œWhat an excellent father you have, girls,” said she, when the door was +shut. β€œI do not know how you will ever make him amends for his kindness; +or me either, for that matter. At our time of life, it is not so +pleasant, I can tell you, to be making new acquaintances every day; but +for your sakes we would do anything. Lydia, my love, though you _are_ +the youngest, I dare say Mr. Bingley will dance with you at the next +ball.” + +β€œOh,” said Lydia, stoutly, β€œI am not afraid; for though I _am_ the +youngest, I’m the tallest.” + +The rest of the evening was spent in conjecturing how soon he would +return Mr. Bennet’s visit, and determining when they should ask him to +dinner. + +[Illustration: β€œI’m the tallest”] + + + + +[Illustration: + + β€œHe rode a black horse” +] + + + + +CHAPTER III. + + +[Illustration] + +Not all that Mrs. Bennet, however, with the assistance of her five +daughters, could ask on the subject, was sufficient to draw from her +husband any satisfactory description of Mr. Bingley. They attacked him +in various ways, with barefaced questions, ingenious suppositions, and +distant surmises; but he eluded the skill of them all; and they were at +last obliged to accept the second-hand intelligence of their neighbour, +Lady Lucas. Her report was highly favourable. Sir William had been +delighted with him. He was quite young, wonderfully handsome, extremely +agreeable, and, to crown the whole, he meant to be at the next assembly +with a large party. Nothing could be more delightful! To be fond of +dancing was a certain step towards falling in love; and very lively +hopes of Mr. Bingley’s heart were entertained. + +β€œIf I can but see one of my daughters happily settled at Netherfield,” +said Mrs. Bennet to her husband, β€œand all the others equally well +married, I shall have nothing to wish for.” + +In a few days Mr. Bingley returned Mr. Bennet’s visit, and sat about ten +minutes with him in his library. He had entertained hopes of being +admitted to a sight of the young ladies, of whose beauty he had heard +much; but he saw only the father. The ladies were somewhat more +fortunate, for they had the advantage of ascertaining, from an upper +window, that he wore a blue coat and rode a black horse. + +An invitation to dinner was soon afterwards despatched; and already had +Mrs. Bennet planned the courses that were to do credit to her +housekeeping, when an answer arrived which deferred it all. Mr. Bingley +was obliged to be in town the following day, and consequently unable to +accept the honour of their invitation, etc. Mrs. Bennet was quite +disconcerted. She could not imagine what business he could have in town +so soon after his arrival in Hertfordshire; and she began to fear that +he might always be flying about from one place to another, and never +settled at Netherfield as he ought to be. Lady Lucas quieted her fears a +little by starting the idea of his + +[Illustration: + + β€œWhen the Party entered” + +[_Copyright 1894 by George Allen._]] + +being gone to London only to get a large party for the ball; and a +report soon followed that Mr. Bingley was to bring twelve ladies and +seven gentlemen with him to the assembly. The girls grieved over such a +number of ladies; but were comforted the day before the ball by hearing +that, instead of twelve, he had brought only six with him from London, +his five sisters and a cousin. And when the party entered the +assembly-room, it consisted of only five altogether: Mr. Bingley, his +two sisters, the husband of the eldest, and another young man. + +Mr. Bingley was good-looking and gentlemanlike: he had a pleasant +countenance, and easy, unaffected manners. His sisters were fine women, +with an air of decided fashion. His brother-in-law, Mr. Hurst, merely +looked the gentleman; but his friend Mr. Darcy soon drew the attention +of the room by his fine, tall person, handsome features, noble mien, and +the report, which was in general circulation within five minutes after +his entrance, of his having ten thousand a year. The gentlemen +pronounced him to be a fine figure of a man, the ladies declared he was +much handsomer than Mr. Bingley, and he was looked at with great +admiration for about half the evening, till his manners gave a disgust +which turned the tide of his popularity; for he was discovered to be +proud, to be above his company, and above being pleased; and not all his +large estate in Derbyshire could save him from having a most forbidding, +disagreeable countenance, and being unworthy to be compared with his +friend. + +Mr. Bingley had soon made himself acquainted with all the principal +people in the room: he was lively and unreserved, danced every dance, +was angry that the ball closed so early, and talked of giving one +himself at Netherfield. Such amiable qualities must speak for +themselves. What a contrast between him and his friend! Mr. Darcy danced +only once with Mrs. Hurst and once with Miss Bingley, declined being +introduced to any other lady, and spent the rest of the evening in +walking about the room, speaking occasionally to one of his own party. +His character was decided. He was the proudest, most disagreeable man in +the world, and everybody hoped that he would never come there again. +Amongst the most violent against him was Mrs. Bennet, whose dislike of +his general behaviour was sharpened into particular resentment by his +having slighted one of her daughters. + +Elizabeth Bennet had been obliged, by the scarcity of gentlemen, to sit +down for two dances; and during part of that time, Mr. Darcy had been +standing near enough for her to overhear a conversation between him and +Mr. Bingley, who came from the dance for a few minutes to press his +friend to join it. + +β€œCome, Darcy,” said he, β€œI must have you dance. I hate to see you +standing about by yourself in this stupid manner. You had much better +dance.” + +β€œI certainly shall not. You know how I detest it, unless I am +particularly acquainted with my partner. At such an assembly as this, it +would be insupportable. Your sisters are engaged, and there is not +another woman in the room whom it would not be a punishment to me to +stand up with.” + +β€œI would not be so fastidious as you are,” cried Bingley, β€œfor a +kingdom! Upon my honour, I never met with so many pleasant girls in my +life as I have this evening; and there are several of them, you see, +uncommonly pretty.” + +β€œ_You_ are dancing with the only handsome girl in the room,” said Mr. +Darcy, looking at the eldest Miss Bennet. + +β€œOh, she is the most beautiful creature I ever beheld! But there is one +of her sisters sitting down just behind you, who is very pretty, and I +dare say very agreeable. Do let me ask my partner to introduce you.” + +[Illustration: + +β€œShe is tolerable” + +[_Copyright 1894 by George Allen._]] + +β€œWhich do you mean?” and turning round, he looked for a moment at +Elizabeth, till, catching her eye, he withdrew his own, and coldly said, +β€œShe is tolerable: but not handsome enough to tempt _me_; and I am in no +humour at present to give consequence to young ladies who are slighted +by other men. You had better return to your partner and enjoy her +smiles, for you are wasting your time with me.” + +Mr. Bingley followed his advice. Mr. Darcy walked off; and Elizabeth +remained with no very cordial feelings towards him. She told the story, +however, with great spirit among her friends; for she had a lively, +playful disposition, which delighted in anything ridiculous. + +The evening altogether passed off pleasantly to the whole family. Mrs. +Bennet had seen her eldest daughter much admired by the Netherfield +party. Mr. Bingley had danced with her twice, and she had been +distinguished by his sisters. Jane was as much gratified by this as her +mother could be, though in a quieter way. Elizabeth felt Jane’s +pleasure. Mary had heard herself mentioned to Miss Bingley as the most +accomplished girl in the neighbourhood; and Catherine and Lydia had been +fortunate enough to be never without partners, which was all that they +had yet learnt to care for at a ball. They returned, therefore, in good +spirits to Longbourn, the village where they lived, and of which they +were the principal inhabitants. They found Mr. Bennet still up. With a +book, he was regardless of time; and on the present occasion he had a +good deal of curiosity as to the event of an evening which had raised +such splendid expectations. He had rather hoped that all his wife’s +views on the stranger would be disappointed; but he soon found that he +had a very different story to hear. + +β€œOh, my dear Mr. Bennet,” as she entered the room, β€œwe have had a most +delightful evening, a most excellent ball. I wish you had been there. +Jane was so admired, nothing could be like it. Everybody said how well +she looked; and Mr. Bingley thought her quite beautiful, and danced with +her twice. Only think of _that_, my dear: he actually danced with her +twice; and she was the only creature in the room that he asked a second +time. First of all, he asked Miss Lucas. I was so vexed to see him stand +up with her; but, however, he did not admire her at all; indeed, nobody +can, you know; and he seemed quite struck with Jane as she was going +down the dance. So he inquired who she was, and got introduced, and +asked her for the two next. Then, the two third he danced with Miss +King, and the two fourth with Maria Lucas, and the two fifth with Jane +again, and the two sixth with Lizzy, and the _Boulanger_----” + +β€œIf he had had any compassion for _me_,” cried her husband impatiently, +β€œhe would not have danced half so much! For God’s sake, say no more of +his partners. O that he had sprained his ancle in the first dance!” + +β€œOh, my dear,” continued Mrs. Bennet, β€œI am quite delighted with him. He +is so excessively handsome! and his sisters are charming women. I never +in my life saw anything more elegant than their dresses. I dare say the +lace upon Mrs. Hurst’s gown----” + +Here she was interrupted again. Mr. Bennet protested against any +description of finery. She was therefore obliged to seek another branch +of the subject, and related, with much bitterness of spirit, and some +exaggeration, the shocking rudeness of Mr. Darcy. + +β€œBut I can assure you,” she added, β€œthat Lizzy does not lose much by not +suiting _his_ fancy; for he is a most disagreeable, horrid man, not at +all worth pleasing. So high and so conceited, that there was no enduring +him! He walked here, and he walked there, fancying himself so very +great! Not handsome enough to dance with! I wish you had been there, my +dear, to have given him one of your set-downs. I quite detest the man.” + + + + +[Illustration] + + + + +CHAPTER IV. + + +[Illustration] + +When Jane and Elizabeth were alone, the former, who had been cautious in +her praise of Mr. Bingley before, expressed to her sister how very much +she admired him. + +β€œHe is just what a young-man ought to be,” said she, β€œsensible, +good-humoured, lively; and I never saw such happy manners! so much ease, +with such perfect good breeding!” + +β€œHe is also handsome,” replied Elizabeth, β€œwhich a young man ought +likewise to be if he possibly can. His character is thereby complete.” + +β€œI was very much flattered by his asking me to dance a second time. I +did not expect such a compliment.” + +β€œDid not you? _I_ did for you. But that is one great difference between +us. Compliments always take _you_ by surprise, and _me_ never. What +could be more natural than his asking you again? He could not help +seeing that you were about five times as pretty as every other woman in +the room. No thanks to his gallantry for that. Well, he certainly is +very agreeable, and I give you leave to like him. You have liked many a +stupider person.” + +β€œDear Lizzy!” + +β€œOh, you are a great deal too apt, you know, to like people in general. +You never see a fault in anybody. All the world are good and agreeable +in your eyes. I never heard you speak ill of a human being in my life.” + +β€œI would wish not to be hasty in censuring anyone; but I always speak +what I think.” + +β€œI know you do: and it is _that_ which makes the wonder. With _your_ +good sense, to be so honestly blind to the follies and nonsense of +others! Affectation of candour is common enough; one meets with it +everywhere. But to be candid without ostentation or design,--to take the +good of everybody’s character and make it still better, and say nothing +of the bad,--belongs to you alone. And so, you like this man’s sisters, +too, do you? Their manners are not equal to his.” + +β€œCertainly not, at first; but they are very pleasing women when you +converse with them. Miss Bingley is to live with her brother, and keep +his house; and I am much mistaken if we shall not find a very charming +neighbour in her.” + +Elizabeth listened in silence, but was not convinced: their behaviour at +the assembly had not been calculated to please in general; and with more +quickness of observation and less pliancy of temper than her sister, and +with a judgment, too, unassailed by any attention to herself, she was +very little disposed to approve them. They were, in fact, very fine +ladies; not deficient in good-humour when they were pleased, nor in the +power of being agreeable where they chose it; but proud and conceited. +They were rather handsome; had been educated in one of the first private +seminaries in town; had a fortune of twenty thousand pounds; were in the +habit of spending more than they ought, and of associating with people +of rank; and were, therefore, in every respect entitled to think well of +themselves and meanly of others. They were of a respectable family in +the north of England; a circumstance more deeply impressed on their +memories than that their brother’s fortune and their own had been +acquired by trade. + +Mr. Bingley inherited property to the amount of nearly a hundred +thousand pounds from his father, who had intended to purchase an estate, +but did not live to do it. Mr. Bingley intended it likewise, and +sometimes made choice of his county; but, as he was now provided with a +good house and the liberty of a manor, it was doubtful to many of those +who best knew the easiness of his temper, whether he might not spend the +remainder of his days at Netherfield, and leave the next generation to +purchase. + +His sisters were very anxious for his having an estate of his own; but +though he was now established only as a tenant, Miss Bingley was by no +means unwilling to preside at his table; nor was Mrs. Hurst, who had +married a man of more fashion than fortune, less disposed to consider +his house as her home when it suited her. Mr. Bingley had not been of +age two years when he was tempted, by an accidental recommendation, to +look at Netherfield House. He did look at it, and into it, for half an +hour; was pleased with the situation and the principal rooms, satisfied +with what the owner said in its praise, and took it immediately. + +Between him and Darcy there was a very steady friendship, in spite of a +great opposition of character. Bingley was endeared to Darcy by the +easiness, openness, and ductility of his temper, though no disposition +could offer a greater contrast to his own, and though with his own he +never appeared dissatisfied. On the strength of Darcy’s regard, Bingley +had the firmest reliance, and of his judgment the highest opinion. In +understanding, Darcy was the superior. Bingley was by no means +deficient; but Darcy was clever. He was at the same time haughty, +reserved, and fastidious; and his manners, though well bred, were not +inviting. In that respect his friend had greatly the advantage. Bingley +was sure of being liked wherever he appeared; Darcy was continually +giving offence. + +The manner in which they spoke of the Meryton assembly was sufficiently +characteristic. Bingley had never met with pleasanter people or prettier +girls in his life; everybody had been most kind and attentive to him; +there had been no formality, no stiffness; he had soon felt acquainted +with all the room; and as to Miss Bennet, he could not conceive an angel +more beautiful. Darcy, on the contrary, had seen a collection of people +in whom there was little beauty and no fashion, for none of whom he had +felt the smallest interest, and from none received either attention or +pleasure. Miss Bennet he acknowledged to be pretty; but she smiled too +much. + +Mrs. Hurst and her sister allowed it to be so; but still they admired +her and liked her, and pronounced her to be a sweet girl, and one whom +they should not object to know more of. Miss Bennet was therefore +established as a sweet girl; and their brother felt authorized by such +commendation to think of her as he chose. + + + + +[Illustration: [_Copyright 1894 by George Allen._]] + + + + +CHAPTER V. + + +[Illustration] + +Within a short walk of Longbourn lived a family with whom the Bennets +were particularly intimate. Sir William Lucas had been formerly in trade +in Meryton, where he had made a tolerable fortune, and risen to the +honour of knighthood by an address to the king during his mayoralty. The +distinction had, perhaps, been felt too strongly. It had given him a +disgust to his business and to his residence in a small market town; +and, quitting them both, he had removed with his family to a house about +a mile from Meryton, denominated from that period Lucas Lodge; where he +could think with pleasure of his own importance, and, unshackled by +business, occupy himself solely in being civil to all the world. For, +though elated by his rank, it did not render him supercilious; on the +contrary, he was all attention to everybody. By nature inoffensive, +friendly, and obliging, his presentation at St. James’s had made him +courteous. + +Lady Lucas was a very good kind of woman, not too clever to be a +valuable neighbour to Mrs. Bennet. They had several children. The eldest +of them, a sensible, intelligent young woman, about twenty-seven, was +Elizabeth’s intimate friend. + +That the Miss Lucases and the Miss Bennets should meet to talk over a +ball was absolutely necessary; and the morning after the assembly +brought the former to Longbourn to hear and to communicate. + +β€œ_You_ began the evening well, Charlotte,” said Mrs. Bennet, with civil +self-command, to Miss Lucas. β€œ_You_ were Mr. Bingley’s first choice.” + +β€œYes; but he seemed to like his second better.” + +β€œOh, you mean Jane, I suppose, because he danced with her twice. To be +sure that _did_ seem as if he admired her--indeed, I rather believe he +_did_--I heard something about it--but I hardly know what--something +about Mr. Robinson.” + +β€œPerhaps you mean what I overheard between him and Mr. Robinson: did not +I mention it to you? Mr. Robinson’s asking him how he liked our Meryton +assemblies, and whether he did not think there were a great many pretty +women in the room, and _which_ he thought the prettiest? and his +answering immediately to the last question, β€˜Oh, the eldest Miss Bennet, +beyond a doubt: there cannot be two opinions on that point.’” + +β€œUpon my word! Well, that was very decided, indeed--that does seem as +if--but, however, it may all come to nothing, you know.” + +β€œ_My_ overhearings were more to the purpose than _yours_, Eliza,” said +Charlotte. β€œMr. Darcy is not so well worth listening to as his friend, +is he? Poor Eliza! to be only just _tolerable_.” + +β€œI beg you will not put it into Lizzy’s head to be vexed by his +ill-treatment, for he is such a disagreeable man that it would be quite +a misfortune to be liked by him. Mrs. Long told me last night that he +sat close to her for half an hour without once opening his lips.” + +[Illustration: β€œWithout once opening his lips” + +[_Copyright 1894 by George Allen._]] + +β€œAre you quite sure, ma’am? Is not there a little mistake?” said Jane. +β€œI certainly saw Mr. Darcy speaking to her.” + +β€œAy, because she asked him at last how he liked Netherfield, and he +could not help answering her; but she said he seemed very angry at being +spoke to.” + +β€œMiss Bingley told me,” said Jane, β€œthat he never speaks much unless +among his intimate acquaintance. With _them_ he is remarkably +agreeable.” + +β€œI do not believe a word of it, my dear. If he had been so very +agreeable, he would have talked to Mrs. Long. But I can guess how it +was; everybody says that he is eat up with pride, and I dare say he had +heard somehow that Mrs. Long does not keep a carriage, and had to come +to the ball in a hack chaise.” + +β€œI do not mind his not talking to Mrs. Long,” said Miss Lucas, β€œbut I +wish he had danced with Eliza.” + +β€œAnother time, Lizzy,” said her mother, β€œI would not dance with _him_, +if I were you.” + +β€œI believe, ma’am, I may safely promise you _never_ to dance with him.” + +β€œHis pride,” said Miss Lucas, β€œdoes not offend _me_ so much as pride +often does, because there is an excuse for it. One cannot wonder that so +very fine a young man, with family, fortune, everything in his favour, +should think highly of himself. If I may so express it, he has a _right_ +to be proud.” + +β€œThat is very true,” replied Elizabeth, β€œand I could easily forgive +_his_ pride, if he had not mortified _mine_.” + +β€œPride,” observed Mary, who piqued herself upon the solidity of her +reflections, β€œis a very common failing, I believe. By all that I have +ever read, I am convinced that it is very common indeed; that human +nature is particularly prone to it, and that there are very few of us +who do not cherish a feeling of self-complacency on the score of some +quality or other, real or imaginary. Vanity and pride are different +things, though the words are often used synonymously. A person may be +proud without being vain. Pride relates more to our opinion of +ourselves; vanity to what we would have others think of us.” + +β€œIf I were as rich as Mr. Darcy,” cried a young Lucas, who came with his +sisters, β€œI should not care how proud I was. I would keep a pack of +foxhounds, and drink a bottle of wine every day.” + +β€œThen you would drink a great deal more than you ought,” said Mrs. +Bennet; β€œand if I were to see you at it, I should take away your bottle +directly.” + +The boy protested that she should not; she continued to declare that she +would; and the argument ended only with the visit. + +[Illustration] + + + + +[Illustration] + + + + +CHAPTER VI. + + +[Illustration] + +The ladies of Longbourn soon waited on those of Netherfield. The visit +was returned in due form. Miss Bennet’s pleasing manners grew on the +good-will of Mrs. Hurst and Miss Bingley; and though the mother was +found to be intolerable, and the younger sisters not worth speaking to, +a wish of being better acquainted with _them_ was expressed towards the +two eldest. By Jane this attention was received with the greatest +pleasure; but Elizabeth still saw superciliousness in their treatment of +everybody, hardly excepting even her sister, and could not like them; +though their kindness to Jane, such as it was, had a value, as arising, +in all probability, from the influence of their brother’s admiration. It +was generally evident, whenever they met, that he _did_ admire her; and +to _her_ it was equally evident that Jane was yielding to the preference +which she had begun to entertain for him from the first, and was in a +way to be very much in love; but she considered with pleasure that it +was not likely to be discovered by the world in general, since Jane +united with great strength of feeling, a composure of temper and an +uniform cheerfulness of manner, which would guard her from the +suspicions of the impertinent. She mentioned this to her friend, Miss +Lucas. + +β€œIt may, perhaps, be pleasant,” replied Charlotte, β€œto be able to impose +on the public in such a case; but it is sometimes a disadvantage to be +so very guarded. If a woman conceals her affection with the same skill +from the object of it, she may lose the opportunity of fixing him; and +it will then be but poor consolation to believe the world equally in the +dark. There is so much of gratitude or vanity in almost every +attachment, that it is not safe to leave any to itself. We can all +_begin_ freely--a slight preference is natural enough; but there are +very few of us who have heart enough to be really in love without +encouragement. In nine cases out of ten, a woman had better show _more_ +affection than she feels. Bingley likes your sister undoubtedly; but he +may never do more than like her, if she does not help him on.” + +β€œBut she does help him on, as much as her nature will allow. If _I_ can +perceive her regard for him, he must be a simpleton indeed not to +discover it too.” + +β€œRemember, Eliza, that he does not know Jane’s disposition as you do.” + +β€œBut if a woman is partial to a man, and does not endeavor to conceal +it, he must find it out.” + +β€œPerhaps he must, if he sees enough of her. But though Bingley and Jane +meet tolerably often, it is never for many hours together; and as they +always see each other in large mixed parties, it is impossible that +every moment should be employed in conversing together. Jane should +therefore make the most of every half hour in which she can command his +attention. When she is secure of him, there will be leisure for falling +in love as much as she chooses.” + +β€œYour plan is a good one,” replied Elizabeth, β€œwhere nothing is in +question but the desire of being well married; and if I were determined +to get a rich husband, or any husband, I dare say I should adopt it. But +these are not Jane’s feelings; she is not acting by design. As yet she +cannot even be certain of the degree of her own regard, nor of its +reasonableness. She has known him only a fortnight. She danced four +dances with him at Meryton; she saw him one morning at his own house, +and has since dined in company with him four times. This is not quite +enough to make her understand his character.” + +β€œNot as you represent it. Had she merely _dined_ with him, she might +only have discovered whether he had a good appetite; but you must +remember that four evenings have been also spent together--and four +evenings may do a great deal.” + +β€œYes: these four evenings have enabled them to ascertain that they both +like Vingt-un better than Commerce, but with respect to any other +leading characteristic, I do not imagine that much has been unfolded.” + +β€œWell,” said Charlotte, β€œI wish Jane success with all my heart; and if +she were married to him to-morrow, I should think she had as good a +chance of happiness as if she were to be studying his character for a +twelvemonth. Happiness in marriage is entirely a matter of chance. If +the dispositions of the parties are ever so well known to each other, or +ever so similar beforehand, it does not advance their felicity in the +least. They always continue to grow sufficiently unlike afterwards to +have their share of vexation; and it is better to know as little as +possible of the defects of the person with whom you are to pass your +life.” + +β€œYou make me laugh, Charlotte; but it is not sound. You know it is not +sound, and that you would never act in this way yourself.” + +Occupied in observing Mr. Bingley’s attention to her sister, Elizabeth +was far from suspecting that she was herself becoming an object of some +interest in the eyes of his friend. Mr. Darcy had at first scarcely +allowed her to be pretty: he had looked at her without admiration at the +ball; and when they next met, he looked at her only to criticise. But no +sooner had he made it clear to himself and his friends that she had +hardly a good feature in her face, than he began to find it was rendered +uncommonly intelligent by the beautiful expression of her dark eyes. To +this discovery succeeded some others equally mortifying. Though he had +detected with a critical eye more than one failure of perfect symmetry +in her form, he was forced to acknowledge her figure to be light and +pleasing; and in spite of his asserting that her manners were not those +of the fashionable world, he was caught by their easy playfulness. Of +this she was perfectly unaware: to her he was only the man who made +himself agreeable nowhere, and who had not thought her handsome enough +to dance with. + +He began to wish to know more of her; and, as a step towards conversing +with her himself, attended to her conversation with others. His doing so +drew her notice. It was at Sir William Lucas’s, where a large party were +assembled. + +β€œWhat does Mr. Darcy mean,” said she to Charlotte, β€œby listening to my +conversation with Colonel Forster?” + +β€œThat is a question which Mr. Darcy only can answer.” + +β€œBut if he does it any more, I shall certainly let him know that I see +what he is about. He has a very satirical eye, and if I do not begin by +being impertinent myself, I shall soon grow afraid of him.” + +[Illustration: β€œThe entreaties of several” [_Copyright 1894 by George +Allen._]] + +On his approaching them soon afterwards, though without seeming to have +any intention of speaking, Miss Lucas defied her friend to mention such +a subject to him, which immediately provoking Elizabeth to do it, she +turned to him and said,-- + +β€œDid not you think, Mr. Darcy, that I expressed myself uncommonly well +just now, when I was teasing Colonel Forster to give us a ball at +Meryton?” + +β€œWith great energy; but it is a subject which always makes a lady +energetic.” + +β€œYou are severe on us.” + +β€œIt will be _her_ turn soon to be teased,” said Miss Lucas. β€œI am going +to open the instrument, Eliza, and you know what follows.” + +β€œYou are a very strange creature by way of a friend!--always wanting me +to play and sing before anybody and everybody! If my vanity had taken a +musical turn, you would have been invaluable; but as it is, I would +really rather not sit down before those who must be in the habit of +hearing the very best performers.” On Miss Lucas’s persevering, however, +she added, β€œVery well; if it must be so, it must.” And gravely glancing +at Mr. Darcy, β€œThere is a very fine old saying, which everybody here is +of course familiar with--β€˜Keep your breath to cool your porridge,’--and +I shall keep mine to swell my song.” + +Her performance was pleasing, though by no means capital. After a song +or two, and before she could reply to the entreaties of several that she +would sing again, she was eagerly succeeded at the instrument by her +sister Mary, who having, in consequence of being the only plain one in +the family, worked hard for knowledge and accomplishments, was always +impatient for display. + +Mary had neither genius nor taste; and though vanity had given her +application, it had given her likewise a pedantic air and conceited +manner, which would have injured a higher degree of excellence than she +had reached. Elizabeth, easy and unaffected, had been listened to with +much more pleasure, though not playing half so well; and Mary, at the +end of a long concerto, was glad to purchase praise and gratitude by +Scotch and Irish airs, at the request of her younger sisters, who with +some of the Lucases, and two or three officers, joined eagerly in +dancing at one end of the room. + +Mr. Darcy stood near them in silent indignation at such a mode of +passing the evening, to the exclusion of all conversation, and was too +much engrossed by his own thoughts to perceive that Sir William Lucas +was his neighbour, till Sir William thus began:-- + +β€œWhat a charming amusement for young people this is, Mr. Darcy! There is +nothing like dancing, after all. I consider it as one of the first +refinements of polished societies.” + +β€œCertainly, sir; and it has the advantage also of being in vogue amongst +the less polished societies of the world: every savage can dance.” + +Sir William only smiled. β€œYour friend performs delightfully,” he +continued, after a pause, on seeing Bingley join the group; β€œand I doubt +not that you are an adept in the science yourself, Mr. Darcy.” + +β€œYou saw me dance at Meryton, I believe, sir.” + +β€œYes, indeed, and received no inconsiderable pleasure from the sight. Do +you often dance at St. James’s?” + +β€œNever, sir.” + +β€œDo you not think it would be a proper compliment to the place?” + +β€œIt is a compliment which I never pay to any place if I can avoid it.” + +β€œYou have a house in town, I conclude?” + +Mr. Darcy bowed. + +β€œI had once some thoughts of fixing in town myself, for I am fond of +superior society; but I did not feel quite certain that the air of +London would agree with Lady Lucas.” + +He paused in hopes of an answer: but his companion was not disposed to +make any; and Elizabeth at that instant moving towards them, he was +struck with the notion of doing a very gallant thing, and called out to +her,-- + +β€œMy dear Miss Eliza, why are not you dancing? Mr. Darcy, you must allow +me to present this young lady to you as a very desirable partner. You +cannot refuse to dance, I am sure, when so much beauty is before you.” +And, taking her hand, he would have given it to Mr. Darcy, who, though +extremely surprised, was not unwilling to receive it, when she instantly +drew back, and said with some discomposure to Sir William,-- + +β€œIndeed, sir, I have not the least intention of dancing. I entreat you +not to suppose that I moved this way in order to beg for a partner.” + +Mr. Darcy, with grave propriety, requested to be allowed the honour of +her hand, but in vain. Elizabeth was determined; nor did Sir William at +all shake her purpose by his attempt at persuasion. + +β€œYou excel so much in the dance, Miss Eliza, that it is cruel to deny me +the happiness of seeing you; and though this gentleman dislikes the +amusement in general, he can have no objection, I am sure, to oblige us +for one half hour.” + +β€œMr. Darcy is all politeness,” said Elizabeth, smiling. + +β€œHe is, indeed: but considering the inducement, my dear Miss Eliza, we +cannot wonder at his complaisance; for who would object to such a +partner?” + +Elizabeth looked archly, and turned away. Her resistance had not injured +her with the gentleman, and he was thinking of her with some +complacency, when thus accosted by Miss Bingley,-- + +β€œI can guess the subject of your reverie.” + +β€œI should imagine not.” + +β€œYou are considering how insupportable it would be to pass many +evenings in this manner,--in such society; and, indeed, I am quite of +your opinion. I was never more annoyed! The insipidity, and yet the +noise--the nothingness, and yet the self-importance, of all these +people! What would I give to hear your strictures on them!” + +β€œYour conjecture is totally wrong, I assure you. My mind was more +agreeably engaged. I have been meditating on the very great pleasure +which a pair of fine eyes in the face of a pretty woman can bestow.” + +Miss Bingley immediately fixed her eyes on his face, and desired he +would tell her what lady had the credit of inspiring such reflections. +Mr. Darcy replied, with great intrepidity,-- + +β€œMiss Elizabeth Bennet.” + +β€œMiss Elizabeth Bennet!” repeated Miss Bingley. β€œI am all astonishment. +How long has she been such a favourite? and pray when am I to wish you +joy?” + +β€œThat is exactly the question which I expected you to ask. A lady’s +imagination is very rapid; it jumps from admiration to love, from love +to matrimony, in a moment. I knew you would be wishing me joy.” + +β€œNay, if you are so serious about it, I shall consider the matter as +absolutely settled. You will have a charming mother-in-law, indeed, and +of course she will be always at Pemberley with you.” + +He listened to her with perfect indifference, while she chose to +entertain herself in this manner; and as his composure convinced her +that all was safe, her wit flowed along. + + + + +[Illustration: + + β€œA note for Miss Bennet” + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER VII. + + +[Illustration] + +Mr. Bennet’s property consisted almost entirely in an estate of two +thousand a year, which, unfortunately for his daughters, was entailed, +in default of heirs male, on a distant relation; and their mother’s +fortune, though ample for her situation in life, could but ill supply +the deficiency of his. Her father had been an attorney in Meryton, and +had left her four thousand pounds. + +She had a sister married to a Mr. Philips, who had been a clerk to their +father and succeeded him in the business, and a brother settled in +London in a respectable line of trade. + +The village of Longbourn was only one mile from Meryton; a most +convenient distance for the young ladies, who were usually tempted +thither three or four times a week, to pay their duty to their aunt, and +to a milliner’s shop just over the way. The two youngest of the family, +Catherine and Lydia, were particularly frequent in these attentions: +their minds were more vacant than their sisters’, and when nothing +better offered, a walk to Meryton was necessary to amuse their morning +hours and furnish conversation for the evening; and, however bare of +news the country in general might be, they always contrived to learn +some from their aunt. At present, indeed, they were well supplied both +with news and happiness by the recent arrival of a militia regiment in +the neighbourhood; it was to remain the whole winter, and Meryton was +the head-quarters. + +Their visits to Mrs. Philips were now productive of the most interesting +intelligence. Every day added something to their knowledge of the +officers’ names and connections. Their lodgings were not long a secret, +and at length they began to know the officers themselves. Mr. Philips +visited them all, and this opened to his nieces a source of felicity +unknown before. They could talk of nothing but officers; and Mr. +Bingley’s large fortune, the mention of which gave animation to their +mother, was worthless in their eyes when opposed to the regimentals of +an ensign. + +After listening one morning to their effusions on this subject, Mr. +Bennet coolly observed,-- + +β€œFrom all that I can collect by your manner of talking, you must be two +of the silliest girls in the country. I have suspected it some time, but +I am now convinced.” + +Catherine was disconcerted, and made no answer; but Lydia, with perfect +indifference, continued to express her admiration of Captain Carter, and +her hope of seeing him in the course of the day, as he was going the +next morning to London. + +β€œI am astonished, my dear,” said Mrs. Bennet, β€œthat you should be so +ready to think your own children silly. If I wished to think slightingly +of anybody’s children, it should not be of my own, however.” + +β€œIf my children are silly, I must hope to be always sensible of it.” + +β€œYes; but as it happens, they are all of them very clever.” + +β€œThis is the only point, I flatter myself, on which we do not agree. I +had hoped that our sentiments coincided in every particular, but I must +so far differ from you as to think our two youngest daughters uncommonly +foolish.” + +β€œMy dear Mr. Bennet, you must not expect such girls to have the sense of +their father and mother. When they get to our age, I dare say they will +not think about officers any more than we do. I remember the time when I +liked a red coat myself very well--and, indeed, so I do still at my +heart; and if a smart young colonel, with five or six thousand a year, +should want one of my girls, I shall not say nay to him; and I thought +Colonel Forster looked very becoming the other night at Sir William’s in +his regimentals.” + +β€œMamma,” cried Lydia, β€œmy aunt says that Colonel Forster and Captain +Carter do not go so often to Miss Watson’s as they did when they first +came; she sees them now very often standing in Clarke’s library.” + +Mrs. Bennet was prevented replying by the entrance of the footman with a +note for Miss Bennet; it came from Netherfield, and the servant waited +for an answer. Mrs. Bennet’s eyes sparkled with pleasure, and she was +eagerly calling out, while her daughter read,-- + +β€œWell, Jane, who is it from? What is it about? What does he say? Well, +Jane, make haste and tell us; make haste, my love.” + +β€œIt is from Miss Bingley,” said Jane, and then read it aloud. + + /* NIND β€œMy dear friend, */ + + β€œIf you are not so compassionate as to dine to-day with Louisa and + me, we shall be in danger of hating each other for the rest of our + lives; for a whole day’s _tΓͺte-Γ -tΓͺte_ between two women can never + end without a quarrel. Come as soon as you can on the receipt of + this. My brother and the gentlemen are to dine with the officers. + Yours ever, + +β€œCAROLINE BINGLEY.” + +β€œWith the officers!” cried Lydia: β€œI wonder my aunt did not tell us of +_that_.” + +β€œDining out,” said Mrs. Bennet; β€œthat is very unlucky.” + +β€œCan I have the carriage?” said Jane. + +β€œNo, my dear, you had better go on horseback, because it seems likely to +rain; and then you must stay all night.” + +β€œThat would be a good scheme,” said Elizabeth, β€œif you were sure that +they would not offer to send her home.” + +β€œOh, but the gentlemen will have Mr. Bingley’s chaise to go to Meryton; +and the Hursts have no horses to theirs.” + +β€œI had much rather go in the coach.” + +β€œBut, my dear, your father cannot spare the horses, I am sure. They are +wanted in the farm, Mr. Bennet, are not they?” + +[Illustration: Cheerful prognostics] + +β€œThey are wanted in the farm much oftener than I can get them.” + +β€œBut if you have got them to-day,” said Elizabeth, β€œmy mother’s purpose +will be answered.” + +She did at last extort from her father an acknowledgment that the horses +were engaged; Jane was therefore obliged to go on horseback, and her +mother attended her to the door with many cheerful prognostics of a bad +day. Her hopes were answered; Jane had not been gone long before it +rained hard. Her sisters were uneasy for her, but her mother was +delighted. The rain continued the whole evening without intermission; +Jane certainly could not come back. + +β€œThis was a lucky idea of mine, indeed!” said Mrs. Bennet, more than +once, as if the credit of making it rain were all her own. Till the next +morning, however, she was not aware of all the felicity of her +contrivance. Breakfast was scarcely over when a servant from Netherfield +brought the following note for Elizabeth:-- + + /* NIND β€œMy dearest Lizzie, */ + + β€œI find myself very unwell this morning, which, I suppose, is to be + imputed to my getting wet through yesterday. My kind friends will + not hear of my returning home till I am better. They insist also on + my seeing Mr. Jones--therefore do not be alarmed if you should hear + of his having been to me--and, excepting a sore throat and a + headache, there is not much the matter with me. + +β€œYours, etc.” + +β€œWell, my dear,” said Mr. Bennet, when Elizabeth had read the note +aloud, β€œif your daughter should have a dangerous fit of illness--if she +should die--it would be a comfort to know that it was all in pursuit of +Mr. Bingley, and under your orders.” + +β€œOh, I am not at all afraid of her dying. People do not die of little +trifling colds. She will be taken good care of. As long as she stays +there, it is all very well. I would go and see her if I could have the +carriage.” + +Elizabeth, feeling really anxious, determined to go to her, though the +carriage was not to be had: and as she was no horsewoman, walking was +her only alternative. She declared her resolution. + +β€œHow can you be so silly,” cried her mother, β€œas to think of such a +thing, in all this dirt! You will not be fit to be seen when you get +there.” + +β€œI shall be very fit to see Jane--which is all I want.” + +β€œIs this a hint to me, Lizzy,” said her father, β€œto send for the +horses?” + +β€œNo, indeed. I do not wish to avoid the walk. The distance is nothing, +when one has a motive; only three miles. I shall be back by dinner.” + +β€œI admire the activity of your benevolence,” observed Mary, β€œbut every +impulse of feeling should be guided by reason; and, in my opinion, +exertion should always be in proportion to what is required.” + +β€œWe will go as far as Meryton with you,” said Catherine and Lydia. +Elizabeth accepted their company, and the three young ladies set off +together. + +β€œIf we make haste,” said Lydia, as they walked along, β€œperhaps we may +see something of Captain Carter, before he goes.” + +In Meryton they parted: the two youngest repaired to the lodgings of one +of the officers’ wives, and Elizabeth continued her walk alone, crossing +field after field at a quick pace, jumping over stiles and springing +over puddles, with impatient activity, and finding herself at last +within view of the house, with weary ancles, dirty stockings, and a face +glowing with the warmth of exercise. + +She was shown into the breakfast parlour, where all but Jane were +assembled, and where her appearance created a great deal of surprise. +That she should have walked three miles so early in the day in such +dirty weather, and by herself, was almost incredible to Mrs. Hurst and +Miss Bingley; and Elizabeth was convinced that they held her in contempt +for it. She was received, however, very politely by them; and in their +brother’s manners there was something better than politeness--there was +good-humour and kindness. Mr. Darcy said very little, and Mr. Hurst +nothing at all. The former was divided between admiration of the +brilliancy which exercise had given to her complexion and doubt as to +the occasion’s justifying her coming so far alone. The latter was +thinking only of his breakfast. + +Her inquiries after her sister were not very favourably answered. Miss +Bennet had slept ill, and though up, was very feverish, and not well +enough to leave her room. Elizabeth was glad to be taken to her +immediately; and Jane, who had only been withheld by the fear of giving +alarm or inconvenience, from expressing in her note how much she longed +for such a visit, was delighted at her entrance. She was not equal, +however, to much conversation; and when Miss Bingley left them together, +could attempt little beside expressions of gratitude for the +extraordinary kindness she was treated with. Elizabeth silently attended +her. + +When breakfast was over, they were joined by the sisters; and Elizabeth +began to like them herself, when she saw how much affection and +solicitude they showed for Jane. The apothecary came; and having +examined his patient, said, as might be supposed, that she had caught a +violent cold, and that they must endeavour to get the better of it; +advised her to return to bed, and promised her some draughts. The advice +was followed readily, for the feverish symptoms increased, and her head +ached acutely. Elizabeth did not quit her room for a moment, nor were +the other ladies often absent; the gentlemen being out, they had in fact +nothing to do elsewhere. + +When the clock struck three, Elizabeth felt that she must go, and very +unwillingly said so. Miss Bingley offered her the carriage, and she only +wanted a little pressing to accept it, when Jane testified such concern +at parting with her that Miss Bingley was obliged to convert the offer +of the chaise into an invitation to remain at Netherfield for the +present. Elizabeth most thankfully consented, and a servant was +despatched to Longbourn, to acquaint the family with her stay, and bring +back a supply of clothes. + +[Illustration: + +β€œThe Apothecary came” +] + + + + +[Illustration: + +β€œcovering a screen” +] + + + + +CHAPTER VIII. + + +[Illustration] + +At five o’clock the two ladies retired to dress, and at half-past six +Elizabeth was summoned to dinner. To the civil inquiries which then +poured in, and amongst which she had the pleasure of distinguishing the +much superior solicitude of Mr. Bingley, she could not make a very +favourable answer. Jane was by no means better. The sisters, on hearing +this, repeated three or four times how much they were grieved, how +shocking it was to have a bad cold, and how excessively they disliked +being ill themselves; and then thought no more of the matter: and their +indifference towards Jane, when not immediately before them, restored +Elizabeth to the enjoyment of all her original dislike. + +Their brother, indeed, was the only one of the party whom she could +regard with any complacency. His anxiety for Jane was evident, and his +attentions to herself most pleasing; and they prevented her feeling +herself so much an intruder as she believed she was considered by the +others. She had very little notice from any but him. Miss Bingley was +engrossed by Mr. Darcy, her sister scarcely less so; and as for Mr. +Hurst, by whom Elizabeth sat, he was an indolent man, who lived only to +eat, drink, and play at cards, who, when he found her prefer a plain +dish to a ragout, had nothing to say to her. + +When dinner was over, she returned directly to Jane, and Miss Bingley +began abusing her as soon as she was out of the room. Her manners were +pronounced to be very bad indeed,--a mixture of pride and impertinence: +she had no conversation, no style, no taste, no beauty. Mrs. Hurst +thought the same, and added,-- + +β€œShe has nothing, in short, to recommend her, but being an excellent +walker. I shall never forget her appearance this morning. She really +looked almost wild.” + +β€œShe did indeed, Louisa. I could hardly keep my countenance. Very +nonsensical to come at all! Why must _she_ be scampering about the +country, because her sister had a cold? Her hair so untidy, so blowzy!” + +β€œYes, and her petticoat; I hope you saw her petticoat, six inches deep +in mud, I am absolutely certain, and the gown which had been let down to +hide it not doing its office.” + +β€œYour picture may be very exact, Louisa,” said Bingley; β€œbut this was +all lost upon me. I thought Miss Elizabeth Bennet looked remarkably well +when she came into the room this morning. Her dirty petticoat quite +escaped my notice.” + +β€œ_You_ observed it, Mr. Darcy, I am sure,” said Miss Bingley; β€œand I am +inclined to think that you would not wish to see _your sister_ make such +an exhibition.” + +β€œCertainly not.” + +β€œTo walk three miles, or four miles, or five miles, or whatever it is, +above her ancles in dirt, and alone, quite alone! what could she mean by +it? It seems to me to show an abominable sort of conceited independence, +a most country-town indifference to decorum.” + +β€œIt shows an affection for her sister that is very pleasing,” said +Bingley. + +β€œI am afraid, Mr. Darcy,” observed Miss Bingley, in a half whisper, +β€œthat this adventure has rather affected your admiration of her fine +eyes.” + +β€œNot at all,” he replied: β€œthey were brightened by the exercise.” A +short pause followed this speech, and Mrs. Hurst began again,-- + +β€œI have an excessive regard for Jane Bennet,--she is really a very sweet +girl,--and I wish with all my heart she were well settled. But with such +a father and mother, and such low connections, I am afraid there is no +chance of it.” + +β€œI think I have heard you say that their uncle is an attorney in +Meryton?” + +β€œYes; and they have another, who lives somewhere near Cheapside.” + +β€œThat is capital,” added her sister; and they both laughed heartily. + +β€œIf they had uncles enough to fill _all_ Cheapside,” cried Bingley, β€œit +would not make them one jot less agreeable.” + +β€œBut it must very materially lessen their chance of marrying men of any +consideration in the world,” replied Darcy. + +To this speech Bingley made no answer; but his sisters gave it their +hearty assent, and indulged their mirth for some time at the expense of +their dear friend’s vulgar relations. + +With a renewal of tenderness, however, they repaired to her room on +leaving the dining-parlour, and sat with her till summoned to coffee. +She was still very poorly, and Elizabeth would not quit her at all, till +late in the evening, when she had the comfort of seeing her asleep, and +when it appeared to her rather right than pleasant that she should go +down stairs herself. On entering the drawing-room, she found the whole +party at loo, and was immediately invited to join them; but suspecting +them to be playing high, she declined it, and making her sister the +excuse, said she would amuse herself, for the short time she could stay +below, with a book. Mr. Hurst looked at her with astonishment. + +β€œDo you prefer reading to cards?” said he; β€œthat is rather singular.” + +β€œMiss Eliza Bennet,” said Miss Bingley, β€œdespises cards. She is a great +reader, and has no pleasure in anything else.” + +β€œI deserve neither such praise nor such censure,” cried Elizabeth; β€œI +am _not_ a great reader, and I have pleasure in many things.” + +β€œIn nursing your sister I am sure you have pleasure,” said Bingley; β€œand +I hope it will soon be increased by seeing her quite well.” + +Elizabeth thanked him from her heart, and then walked towards a table +where a few books were lying. He immediately offered to fetch her +others; all that his library afforded. + +β€œAnd I wish my collection were larger for your benefit and my own +credit; but I am an idle fellow; and though I have not many, I have more +than I ever looked into.” + +Elizabeth assured him that she could suit herself perfectly with those +in the room. + +β€œI am astonished,” said Miss Bingley, β€œthat my father should have left +so small a collection of books. What a delightful library you have at +Pemberley, Mr. Darcy!” + +β€œIt ought to be good,” he replied: β€œit has been the work of many +generations.” + +β€œAnd then you have added so much to it yourself--you are always buying +books.” + +β€œI cannot comprehend the neglect of a family library in such days as +these.” + +β€œNeglect! I am sure you neglect nothing that can add to the beauties of +that noble place. Charles, when you build _your_ house, I wish it may be +half as delightful as Pemberley.” + +β€œI wish it may.” + +β€œBut I would really advise you to make your purchase in that +neighbourhood, and take Pemberley for a kind of model. There is not a +finer county in England than Derbyshire.” + +β€œWith all my heart: I will buy Pemberley itself, if Darcy will sell it.” + +β€œI am talking of possibilities, Charles.” + +β€œUpon my word, Caroline, I should think it more possible to get +Pemberley by purchase than by imitation.” + +Elizabeth was so much caught by what passed, as to leave her very little +attention for her book; and, soon laying it wholly aside, she drew near +the card-table, and stationed herself between Mr. Bingley and his eldest +sister, to observe the game. + +β€œIs Miss Darcy much grown since the spring?” said Miss Bingley: β€œwill +she be as tall as I am?” + +β€œI think she will. She is now about Miss Elizabeth Bennet’s height, or +rather taller.” + +β€œHow I long to see her again! I never met with anybody who delighted me +so much. Such a countenance, such manners, and so extremely accomplished +for her age! Her performance on the pianoforte is exquisite.” + +β€œIt is amazing to me,” said Bingley, β€œhow young ladies can have patience +to be so very accomplished as they all are.” + +β€œAll young ladies accomplished! My dear Charles, what do you mean?” + +β€œYes, all of them, I think. They all paint tables, cover screens, and +net purses. I scarcely know any one who cannot do all this; and I am +sure I never heard a young lady spoken of for the first time, without +being informed that she was very accomplished.” + +β€œYour list of the common extent of accomplishments,” said Darcy, β€œhas +too much truth. The word is applied to many a woman who deserves it no +otherwise than by netting a purse or covering a screen; but I am very +far from agreeing with you in your estimation of ladies in general. I +cannot boast of knowing more than half-a-dozen in the whole range of my +acquaintance that are really accomplished.” + +β€œNor I, I am sure,” said Miss Bingley. + +β€œThen,” observed Elizabeth, β€œyou must comprehend a great deal in your +idea of an accomplished woman.” + +β€œYes; I do comprehend a great deal in it.” + +β€œOh, certainly,” cried his faithful assistant, β€œno one can be really +esteemed accomplished who does not greatly surpass what is usually met +with. A woman must have a thorough knowledge of music, singing, drawing, +dancing, and the modern languages, to deserve the word; and, besides all +this, she must possess a certain something in her air and manner of +walking, the tone of her voice, her address and expressions, or the word +will be but half deserved.” + +β€œAll this she must possess,” added Darcy; β€œand to all she must yet add +something more substantial in the improvement of her mind by extensive +reading.” + +β€œI am no longer surprised at your knowing _only_ six accomplished women. +I rather wonder now at your knowing _any_.” + +β€œAre you so severe upon your own sex as to doubt the possibility of all +this?” + +β€œ_I_ never saw such a woman. _I_ never saw such capacity, and taste, and +application, and elegance, as you describe, united.” + +Mrs. Hurst and Miss Bingley both cried out against the injustice of her +implied doubt, and were both protesting that they knew many women who +answered this description, when Mr. Hurst called them to order, with +bitter complaints of their inattention to what was going forward. As all +conversation was thereby at an end, Elizabeth soon afterwards left the +room. + +β€œEliza Bennet,” said Miss Bingley, when the door was closed on her, β€œis +one of those young ladies who seek to recommend themselves to the other +sex by undervaluing their own; and with many men, I daresay, it +succeeds; but, in my opinion, it is a paltry device, a very mean art.” + +β€œUndoubtedly,” replied Darcy, to whom this remark was chiefly addressed, +β€œthere is meanness in _all_ the arts which ladies sometimes condescend +to employ for captivation. Whatever bears affinity to cunning is +despicable.” + +Miss Bingley was not so entirely satisfied with this reply as to +continue the subject. + +Elizabeth joined them again only to say that her sister was worse, and +that she could not leave her. Bingley urged Mr. Jones’s being sent for +immediately; while his sisters, convinced that no country advice could +be of any service, recommended an express to town for one of the most +eminent physicians. This she would not hear of; but she was not so +unwilling to comply with their brother’s proposal; and it was settled +that Mr. Jones should be sent for early in the morning, if Miss Bennet +were not decidedly better. Bingley was quite uncomfortable; his sisters +declared that they were miserable. They solaced their wretchedness, +however, by duets after supper; while he could find no better relief to +his feelings than by giving his housekeeper directions that every +possible attention might be paid to the sick lady and her sister. + + + + +[Illustration: + +M^{rs} Bennet and her two youngest girls + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER IX. + + +[Illustration] + +Elizabeth passed the chief of the night in her sister’s room, and in the +morning had the pleasure of being able to send a tolerable answer to the +inquiries which she very early received from Mr. Bingley by a housemaid, +and some time afterwards from the two elegant ladies who waited on his +sisters. In spite of this amendment, however, she requested to have a +note sent to Longbourn, desiring her mother to visit Jane, and form her +own judgment of her situation. The note was immediately despatched, and +its contents as quickly complied with. Mrs. Bennet, accompanied by her +two youngest girls, reached Netherfield soon after the family breakfast. + +Had she found Jane in any apparent danger, Mrs. Bennet would have been +very miserable; but being satisfied on seeing her that her illness was +not alarming, she had no wish of her recovering immediately, as her +restoration to health would probably remove her from Netherfield. She +would not listen, therefore, to her daughter’s proposal of being carried +home; neither did the apothecary, who arrived about the same time, think +it at all advisable. After sitting a little while with Jane, on Miss +Bingley’s appearance and invitation, the mother and three daughters all +attended her into the breakfast parlour. Bingley met them with hopes +that Mrs. Bennet had not found Miss Bennet worse than she expected. + +β€œIndeed I have, sir,” was her answer. β€œShe is a great deal too ill to be +moved. Mr. Jones says we must not think of moving her. We must trespass +a little longer on your kindness.” + +β€œRemoved!” cried Bingley. β€œIt must not be thought of. My sister, I am +sure, will not hear of her removal.” + +β€œYou may depend upon it, madam,” said Miss Bingley, with cold civility, +β€œthat Miss Bennet shall receive every possible attention while she +remains with us.” + +Mrs. Bennet was profuse in her acknowledgments. + +β€œI am sure,” she added, β€œif it was not for such good friends, I do not +know what would become of her, for she is very ill indeed, and suffers a +vast deal, though with the greatest patience in the world, which is +always the way with her, for she has, without exception, the sweetest +temper I ever met with. I often tell my other girls they are nothing to +_her_. You have a sweet room here, Mr. Bingley, and a charming prospect +over that gravel walk. I do not know a place in the country that is +equal to Netherfield. You will not think of quitting it in a hurry, I +hope, though you have but a short lease.” + +β€œWhatever I do is done in a hurry,” replied he; β€œand therefore if I +should resolve to quit Netherfield, I should probably be off in five +minutes. At present, however, I consider myself as quite fixed here.” + +β€œThat is exactly what I should have supposed of you,” said Elizabeth. + +β€œYou begin to comprehend me, do you?” cried he, turning towards her. + +β€œOh yes--I understand you perfectly.” + +β€œI wish I might take this for a compliment; but to be so easily seen +through, I am afraid, is pitiful.” + +β€œThat is as it happens. It does not necessarily follow that a deep, +intricate character is more or less estimable than such a one as yours.” + +β€œLizzy,” cried her mother, β€œremember where you are, and do not run on in +the wild manner that you are suffered to do at home.” + +β€œI did not know before,” continued Bingley, immediately, β€œthat you were +a studier of character. It must be an amusing study.” + +β€œYes; but intricate characters are the _most_ amusing. They have at +least that advantage.” + +β€œThe country,” said Darcy, β€œcan in general supply but few subjects for +such a study. In a country neighbourhood you move in a very confined and +unvarying society.” + +β€œBut people themselves alter so much, that there is something new to be +observed in them for ever.” + +β€œYes, indeed,” cried Mrs. Bennet, offended by his manner of mentioning a +country neighbourhood. β€œI assure you there is quite as much of _that_ +going on in the country as in town.” + +Everybody was surprised; and Darcy, after looking at her for a moment, +turned silently away. Mrs. Bennet, who fancied she had gained a complete +victory over him, continued her triumph,-- + +β€œI cannot see that London has any great advantage over the country, for +my part, except the shops and public places. The country is a vast deal +pleasanter, is not it, Mr. Bingley?” + +β€œWhen I am in the country,” he replied, β€œI never wish to leave it; and +when I am in town, it is pretty much the same. They have each their +advantages, and I can be equally happy in either.” + +β€œAy, that is because you have the right disposition. But that +gentleman,” looking at Darcy, β€œseemed to think the country was nothing +at all.” + +β€œIndeed, mamma, you are mistaken,” said Elizabeth, blushing for her +mother. β€œYou quite mistook Mr. Darcy. He only meant that there was not +such a variety of people to be met with in the country as in town, which +you must acknowledge to be true.” + +β€œCertainly, my dear, nobody said there were; but as to not meeting with +many people in this neighbourhood, I believe there are few +neighbourhoods larger. I know we dine with four-and-twenty families.” + +Nothing but concern for Elizabeth could enable Bingley to keep his +countenance. His sister was less delicate, and directed her eye towards +Mr. Darcy with a very expressive smile. Elizabeth, for the sake of +saying something that might turn her mother’s thoughts, now asked her if +Charlotte Lucas had been at Longbourn since _her_ coming away. + +β€œYes, she called yesterday with her father. What an agreeable man Sir +William is, Mr. Bingley--is not he? so much the man of fashion! so +genteel and so easy! He has always something to say to everybody. _That_ +is my idea of good breeding; and those persons who fancy themselves very +important and never open their mouths quite mistake the matter.” + +β€œDid Charlotte dine with you?” + +β€œNo, she would go home. I fancy she was wanted about the mince-pies. For +my part, Mr. Bingley, _I_ always keep servants that can do their own +work; _my_ daughters are brought up differently. But everybody is to +judge for themselves, and the Lucases are a very good sort of girls, I +assure you. It is a pity they are not handsome! Not that _I_ think +Charlotte so _very_ plain; but then she is our particular friend.” + +β€œShe seems a very pleasant young woman,” said Bingley. + +β€œOh dear, yes; but you must own she is very plain. Lady Lucas herself +has often said so, and envied me Jane’s beauty. I do not like to boast +of my own child; but to be sure, Jane--one does not often see anybody +better looking. It is what everybody says. I do not trust my own +partiality. When she was only fifteen there was a gentleman at my +brother Gardiner’s in town so much in love with her, that my +sister-in-law was sure he would make her an offer before we came away. +But, however, he did not. Perhaps he thought her too young. However, he +wrote some verses on her, and very pretty they were.” + +β€œAnd so ended his affection,” said Elizabeth, impatiently. β€œThere has +been many a one, I fancy, overcome in the same way. I wonder who first +discovered the efficacy of poetry in driving away love!” + +β€œI have been used to consider poetry as the _food_ of love,” said Darcy. + +β€œOf a fine, stout, healthy love it may. Everything nourishes what is +strong already. But if it be only a slight, thin sort of inclination, I +am convinced that one good sonnet will starve it entirely away.” + +Darcy only smiled; and the general pause which ensued made Elizabeth +tremble lest her mother should be exposing herself again. She longed to +speak, but could think of nothing to say; and after a short silence Mrs. +Bennet began repeating her thanks to Mr. Bingley for his kindness to +Jane, with an apology for troubling him also with Lizzy. Mr. Bingley was +unaffectedly civil in his answer, and forced his younger sister to be +civil also, and say what the occasion required. She performed her part, +indeed, without much graciousness, but Mrs. Bennet was satisfied, and +soon afterwards ordered her carriage. Upon this signal, the youngest of +her daughters put herself forward. The two girls had been whispering to +each other during the whole visit; and the result of it was, that the +youngest should tax Mr. Bingley with having promised on his first coming +into the country to give a ball at Netherfield. + +Lydia was a stout, well-grown girl of fifteen, with a fine complexion +and good-humoured countenance; a favourite with her mother, whose +affection had brought her into public at an early age. She had high +animal spirits, and a sort of natural self-consequence, which the +attentions of the officers, to whom her uncle’s good dinners and her +own easy manners recommended her, had increased into assurance. She was +very equal, therefore, to address Mr. Bingley on the subject of the +ball, and abruptly reminded him of his promise; adding, that it would be +the most shameful thing in the world if he did not keep it. His answer +to this sudden attack was delightful to her mother’s ear. + +β€œI am perfectly ready, I assure you, to keep my engagement; and, when +your sister is recovered, you shall, if you please, name the very day of +the ball. But you would not wish to be dancing while she is ill?” + +Lydia declared herself satisfied. β€œOh yes--it would be much better to +wait till Jane was well; and by that time, most likely, Captain Carter +would be at Meryton again. And when you have given _your_ ball,” she +added, β€œI shall insist on their giving one also. I shall tell Colonel +Forster it will be quite a shame if he does not.” + +Mrs. Bennet and her daughters then departed, and Elizabeth returned +instantly to Jane, leaving her own and her relations’ behaviour to the +remarks of the two ladies and Mr. Darcy; the latter of whom, however, +could not be prevailed on to join in their censure of _her_, in spite of +all Miss Bingley’s witticisms on _fine eyes_. + + + + +[Illustration] + + + + +CHAPTER X. + + +[Illustration] + +The day passed much as the day before had done. Mrs. Hurst and Miss +Bingley had spent some hours of the morning with the invalid, who +continued, though slowly, to mend; and, in the evening, Elizabeth joined +their party in the drawing-room. The loo table, however, did not appear. +Mr. Darcy was writing, and Miss Bingley, seated near him, was watching +the progress of his letter, and repeatedly calling off his attention by +messages to his sister. Mr. Hurst and Mr. Bingley were at piquet, and +Mrs. Hurst was observing their game. + +Elizabeth took up some needlework, and was sufficiently amused in +attending to what passed between Darcy and his companion. The perpetual +commendations of the lady either on his hand-writing, or on the evenness +of his lines, or on the length of his letter, with the perfect unconcern +with which her praises were received, formed a curious dialogue, and was +exactly in unison with her opinion of each. + +β€œHow delighted Miss Darcy will be to receive such a letter!” + +He made no answer. + +β€œYou write uncommonly fast.” + +β€œYou are mistaken. I write rather slowly.” + +β€œHow many letters you must have occasion to write in the course of a +year! Letters of business, too! How odious I should think them!” + +β€œIt is fortunate, then, that they fall to my lot instead of to yours.” + +β€œPray tell your sister that I long to see her.” + +β€œI have already told her so once, by your desire.” + +β€œI am afraid you do not like your pen. Let me mend it for you. I mend +pens remarkably well.” + +β€œThank you--but I always mend my own.” + +β€œHow can you contrive to write so even?” + +He was silent. + +β€œTell your sister I am delighted to hear of her improvement on the harp, +and pray let her know that I am quite in raptures with her beautiful +little design for a table, and I think it infinitely superior to Miss +Grantley’s.” + +β€œWill you give me leave to defer your raptures till I write again? At +present I have not room to do them justice.” + +β€œOh, it is of no consequence. I shall see her in January. But do you +always write such charming long letters to her, Mr. Darcy?” + +β€œThey are generally long; but whether always charming, it is not for me +to determine.” + +β€œIt is a rule with me, that a person who can write a long letter with +ease cannot write ill.” + +β€œThat will not do for a compliment to Darcy, Caroline,” cried her +brother, β€œbecause he does _not_ write with ease. He studies too much +for words of four syllables. Do not you, Darcy?” + +β€œMy style of writing is very different from yours.” + +β€œOh,” cried Miss Bingley, β€œCharles writes in the most careless way +imaginable. He leaves out half his words, and blots the rest.” + +β€œMy ideas flow so rapidly that I have not time to express them; by which +means my letters sometimes convey no ideas at all to my correspondents.” + +β€œYour humility, Mr. Bingley,” said Elizabeth, β€œmust disarm reproof.” + +β€œNothing is more deceitful,” said Darcy, β€œthan the appearance of +humility. It is often only carelessness of opinion, and sometimes an +indirect boast.” + +β€œAnd which of the two do you call _my_ little recent piece of modesty?” + +β€œThe indirect boast; for you are really proud of your defects in +writing, because you consider them as proceeding from a rapidity of +thought and carelessness of execution, which, if not estimable, you +think at least highly interesting. The power of doing anything with +quickness is always much prized by the possessor, and often without any +attention to the imperfection of the performance. When you told Mrs. +Bennet this morning, that if you ever resolved on quitting Netherfield +you should be gone in five minutes, you meant it to be a sort of +panegyric, of compliment to yourself; and yet what is there so very +laudable in a precipitance which must leave very necessary business +undone, and can be of no real advantage to yourself or anyone else?” + +β€œNay,” cried Bingley, β€œthis is too much, to remember at night all the +foolish things that were said in the morning. And yet, upon my honour, I +believed what I said of myself to be true, and I believe it at this +moment. At least, therefore, I did not assume the character of needless +precipitance merely to show off before the ladies.” + +β€œI daresay you believed it; but I am by no means convinced that you +would be gone with such celerity. Your conduct would be quite as +dependent on chance as that of any man I know; and if, as you were +mounting your horse, a friend were to say, β€˜Bingley, you had better stay +till next week,’ you would probably do it--you would probably not +go--and, at another word, might stay a month.” + +β€œYou have only proved by this,” cried Elizabeth, β€œthat Mr. Bingley did +not do justice to his own disposition. You have shown him off now much +more than he did himself.” + +β€œI am exceedingly gratified,” said Bingley, β€œby your converting what my +friend says into a compliment on the sweetness of my temper. But I am +afraid you are giving it a turn which that gentleman did by no means +intend; for he would certainly think the better of me if, under such a +circumstance, I were to give a flat denial, and ride off as fast as I +could.” + +β€œWould Mr. Darcy then consider the rashness of your original intention +as atoned for by your obstinacy in adhering to it?” + +β€œUpon my word, I cannot exactly explain the matter--Darcy must speak for +himself.” + +β€œYou expect me to account for opinions which you choose to call mine, +but which I have never acknowledged. Allowing the case, however, to +stand according to your representation, you must remember, Miss Bennet, +that the friend who is supposed to desire his return to the house, and +the delay of his plan, has merely desired it, asked it without offering +one argument in favour of its propriety.” + +β€œTo yield readily--easily--to the _persuasion_ of a friend is no merit +with you.” + +β€œTo yield without conviction is no compliment to the understanding of +either.” + +β€œYou appear to me, Mr. Darcy, to allow nothing for the influence of +friendship and affection. A regard for the requester would often make +one readily yield to a request, without waiting for arguments to reason +one into it. I am not particularly speaking of such a case as you have +supposed about Mr. Bingley. We may as well wait, perhaps, till the +circumstance occurs, before we discuss the discretion of his behaviour +thereupon. But in general and ordinary cases, between friend and friend, +where one of them is desired by the other to change a resolution of no +very great moment, should you think ill of that person for complying +with the desire, without waiting to be argued into it?” + +β€œWill it not be advisable, before we proceed on this subject, to arrange +with rather more precision the degree of importance which is to +appertain to this request, as well as the degree of intimacy subsisting +between the parties?” + +β€œBy all means,” cried Bingley; β€œlet us hear all the particulars, not +forgetting their comparative height and size, for that will have more +weight in the argument, Miss Bennet, than you may be aware of. I assure +you that if Darcy were not such a great tall fellow, in comparison with +myself, I should not pay him half so much deference. I declare I do not +know a more awful object than Darcy on particular occasions, and in +particular places; at his own house especially, and of a Sunday evening, +when he has nothing to do.” + +Mr. Darcy smiled; but Elizabeth thought she could perceive that he was +rather offended, and therefore checked her laugh. Miss Bingley warmly +resented the indignity he had received, in an expostulation with her +brother for talking such nonsense. + +β€œI see your design, Bingley,” said his friend. β€œYou dislike an argument, +and want to silence this.” + +β€œPerhaps I do. Arguments are too much like disputes. If you and Miss +Bennet will defer yours till I am out of the room, I shall be very +thankful; and then you may say whatever you like of me.” + +β€œWhat you ask,” said Elizabeth, β€œis no sacrifice on my side; and Mr. +Darcy had much better finish his letter.” + +Mr. Darcy took her advice, and did finish his letter. + +When that business was over, he applied to Miss Bingley and Elizabeth +for the indulgence of some music. Miss Bingley moved with alacrity to +the pianoforte, and after a polite request that Elizabeth would lead the +way, which the other as politely and more earnestly negatived, she +seated herself. + +Mrs. Hurst sang with her sister; and while they were thus employed, +Elizabeth could not help observing, as she turned over some music-books +that lay on the instrument, how frequently Mr. Darcy’s eyes were fixed +on her. She hardly knew how to suppose that she could be an object of +admiration to so great a man, and yet that he should look at her because +he disliked her was still more strange. She could only imagine, however, +at last, that she drew his notice because there was something about her +more wrong and reprehensible, according to his ideas of right, than in +any other person present. The supposition did not pain her. She liked +him too little to care for his approbation. + +After playing some Italian songs, Miss Bingley varied the charm by a +lively Scotch air; and soon afterwards Mr. Darcy, drawing near +Elizabeth, said to her,-- + +β€œDo you not feel a great inclination, Miss Bennet, to seize such an +opportunity of dancing a reel?” + +She smiled, but made no answer. He repeated the question, with some +surprise at her silence. + +β€œOh,” said she, β€œI heard you before; but I could not immediately +determine what to say in reply. You wanted me, I know, to say β€˜Yes,’ +that you might have the pleasure of despising my taste; but I always +delight in overthrowing those kind of schemes, and cheating a person of +their premeditated contempt. I have, therefore, made up my mind to tell +you that I do not want to dance a reel at all; and now despise me if you +dare.” + +β€œIndeed I do not dare.” + +Elizabeth, having rather expected to affront him, was amazed at his +gallantry; but there was a mixture of sweetness and archness in her +manner which made it difficult for her to affront anybody, and Darcy had +never been so bewitched by any woman as he was by her. He really +believed that, were it not for the inferiority of her connections, he +should be in some danger. + +Miss Bingley saw, or suspected, enough to be jealous; and her great +anxiety for the recovery of her dear friend Jane received some +assistance from her desire of getting rid of Elizabeth. + +She often tried to provoke Darcy into disliking her guest, by talking of +their supposed marriage, and planning his happiness in such an alliance. + +β€œI hope,” said she, as they were walking together in the shrubbery the +next day, β€œyou will give your mother-in-law a few hints, when this +desirable event takes place, as to the advantage of holding her tongue; +and if you can compass it, to cure the younger girls of running after +the officers. And, if I may mention so delicate a subject, endeavour to +check that little something, bordering on conceit and impertinence, +which your lady possesses.” + +[Illustration: + + β€œNo, no; stay where you are” + +[_Copyright 1894 by George Allen._]] + +β€œHave you anything else to propose for my domestic felicity?” + +β€œOh yes. Do let the portraits of your uncle and aunt Philips be placed +in the gallery at Pemberley. Put them next to your great-uncle the +judge. They are in the same profession, you know, only in different +lines. As for your Elizabeth’s picture, you must not attempt to have it +taken, for what painter could do justice to those beautiful eyes?” + +β€œIt would not be easy, indeed, to catch their expression; but their +colour and shape, and the eyelashes, so remarkably fine, might be +copied.” + +At that moment they were met from another walk by Mrs. Hurst and +Elizabeth herself. + +β€œI did not know that you intended to walk,” said Miss Bingley, in some +confusion, lest they had been overheard. + +β€œYou used us abominably ill,” answered Mrs. Hurst, β€œrunning away without +telling us that you were coming out.” + +Then taking the disengaged arm of Mr. Darcy, she left Elizabeth to walk +by herself. The path just admitted three. Mr. Darcy felt their rudeness, +and immediately said,-- + +β€œThis walk is not wide enough for our party. We had better go into the +avenue.” + +But Elizabeth, who had not the least inclination to remain with them, +laughingly answered,-- + +β€œNo, no; stay where you are. You are charmingly grouped, and appear to +uncommon advantage. The picturesque would be spoilt by admitting a +fourth. Good-bye.” + +She then ran gaily off, rejoicing, as she rambled about, in the hope of +being at home again in a day or two. Jane was already so much recovered +as to intend leaving her room for a couple of hours that evening. + + + + +[Illustration: + + β€œPiling up the fire” + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER XI. + + +[Illustration] + +When the ladies removed after dinner Elizabeth ran up to her sister, and +seeing her well guarded from cold, attended her into the drawing-room, +where she was welcomed by her two friends with many professions of +pleasure; and Elizabeth had never seen them so agreeable as they were +during the hour which passed before the gentlemen appeared. Their powers +of conversation were considerable. They could describe an entertainment +with accuracy, relate an anecdote with humour, and laugh at their +acquaintance with spirit. + +But when the gentlemen entered, Jane was no longer the first object; +Miss Bingley’s eyes were instantly turned towards Darcy, and she had +something to say to him before he had advanced many steps. He addressed +himself directly to Miss Bennet with a polite congratulation; Mr. Hurst +also made her a slight bow, and said he was β€œvery glad;” but diffuseness +and warmth remained for Bingley’s salutation. He was full of joy and +attention. The first half hour was spent in piling up the fire, lest she +should suffer from the change of room; and she removed, at his desire, +to the other side of the fireplace, that she might be farther from the +door. He then sat down by her, and talked scarcely to anyone else. +Elizabeth, at work in the opposite corner, saw it all with great +delight. + +When tea was over Mr. Hurst reminded his sister-in-law of the +card-table--but in vain. She had obtained private intelligence that Mr. +Darcy did not wish for cards, and Mr. Hurst soon found even his open +petition rejected. She assured him that no one intended to play, and the +silence of the whole party on the subject seemed to justify her. Mr. +Hurst had, therefore, nothing to do but to stretch himself on one of the +sofas and go to sleep. Darcy took up a book. Miss Bingley did the same; +and Mrs. Hurst, principally occupied in playing with her bracelets and +rings, joined now and then in her brother’s conversation with Miss +Bennet. + +Miss Bingley’s attention was quite as much engaged in watching Mr. +Darcy’s progress through _his_ book, as in reading her own; and she was +perpetually either making some inquiry, or looking at his page. She +could not win him, however, to any conversation; he merely answered her +question and read on. At length, quite exhausted by the attempt to be +amused with her own book, which she had only chosen because it was the +second volume of his, she gave a great yawn and said, β€œHow pleasant it +is to spend an evening in this way! I declare, after all, there is no +enjoyment like reading! How much sooner one tires of anything than of a +book! When I have a house of my own, I shall be miserable if I have not +an excellent library.” + +No one made any reply. She then yawned again, threw aside her book, and +cast her eyes round the room in quest of some amusement; when, hearing +her brother mentioning a ball to Miss Bennet, she turned suddenly +towards him and said,-- + +β€œBy the bye Charles, are you really serious in meditating a dance at +Netherfield? I would advise you, before you determine on it, to consult +the wishes of the present party; I am much mistaken if there are not +some among us to whom a ball would be rather a punishment than a +pleasure.” + +β€œIf you mean Darcy,” cried her brother, β€œhe may go to bed, if he +chooses, before it begins; but as for the ball, it is quite a settled +thing, and as soon as Nicholls has made white soup enough I shall send +round my cards.” + +β€œI should like balls infinitely better,” she replied, β€œif they were +carried on in a different manner; but there is something insufferably +tedious in the usual process of such a meeting. It would surely be much +more rational if conversation instead of dancing made the order of the +day.” + +β€œMuch more rational, my dear Caroline, I dare say; but it would not be +near so much like a ball.” + +Miss Bingley made no answer, and soon afterwards got up and walked about +the room. Her figure was elegant, and she walked well; but Darcy, at +whom it was all aimed, was still inflexibly studious. In the +desperation of her feelings, she resolved on one effort more; and, +turning to Elizabeth, said,-- + +β€œMiss Eliza Bennet, let me persuade you to follow my example, and take a +turn about the room. I assure you it is very refreshing after sitting so +long in one attitude.” + +Elizabeth was surprised, but agreed to it immediately. Miss Bingley +succeeded no less in the real object of her civility: Mr. Darcy looked +up. He was as much awake to the novelty of attention in that quarter as +Elizabeth herself could be, and unconsciously closed his book. He was +directly invited to join their party, but he declined it, observing that +he could imagine but two motives for their choosing to walk up and down +the room together, with either of which motives his joining them would +interfere. What could he mean? She was dying to know what could be his +meaning--and asked Elizabeth whether she could at all understand him. + +β€œNot at all,” was her answer; β€œbut, depend upon it, he means to be +severe on us, and our surest way of disappointing him will be to ask +nothing about it.” + +Miss Bingley, however, was incapable of disappointing Mr. Darcy in +anything, and persevered, therefore, in requiring an explanation of his +two motives. + +β€œI have not the smallest objection to explaining them,” said he, as soon +as she allowed him to speak. β€œYou either choose this method of passing +the evening because you are in each other’s confidence, and have secret +affairs to discuss, or because you are conscious that your figures +appear to the greatest advantage in walking: if the first, I should be +completely in your way; and if the second, I can admire you much better +as I sit by the fire.” + +β€œOh, shocking!” cried Miss Bingley. β€œI never heard anything so +abominable. How shall we punish him for such a speech?” + +β€œNothing so easy, if you have but the inclination,” said Elizabeth. β€œWe +can all plague and punish one another. Tease him--laugh at him. Intimate +as you are, you must know how it is to be done.” + +β€œBut upon my honour I do _not_. I do assure you that my intimacy has not +yet taught me _that_. Tease calmness of temper and presence of mind! No, +no; I feel he may defy us there. And as to laughter, we will not expose +ourselves, if you please, by attempting to laugh without a subject. Mr. +Darcy may hug himself.” + +β€œMr. Darcy is not to be laughed at!” cried Elizabeth. β€œThat is an +uncommon advantage, and uncommon I hope it will continue, for it would +be a great loss to _me_ to have many such acquaintance. I dearly love a +laugh.” + +β€œMiss Bingley,” said he, β€œhas given me credit for more than can be. The +wisest and best of men,--nay, the wisest and best of their actions,--may +be rendered ridiculous by a person whose first object in life is a +joke.” + +β€œCertainly,” replied Elizabeth, β€œthere are such people, but I hope I am +not one of _them_. I hope I never ridicule what is wise or good. Follies +and nonsense, whims and inconsistencies, _do_ divert me, I own, and I +laugh at them whenever I can. But these, I suppose, are precisely what +you are without.” + +β€œPerhaps that is not possible for anyone. But it has been the study of +my life to avoid those weaknesses which often expose a strong +understanding to ridicule.” + +β€œSuch as vanity and pride.” + +β€œYes, vanity is a weakness indeed. But pride--where there is a real +superiority of mind--pride will be always under good regulation.” + +Elizabeth turned away to hide a smile. + +β€œYour examination of Mr. Darcy is over, I presume,” said Miss Bingley; +β€œand pray what is the result?” + +β€œI am perfectly convinced by it that Mr. Darcy has no defect. He owns it +himself without disguise.” + +β€œNo,” said Darcy, β€œI have made no such pretension. I have faults enough, +but they are not, I hope, of understanding. My temper I dare not vouch +for. It is, I believe, too little yielding; certainly too little for the +convenience of the world. I cannot forget the follies and vices of +others so soon as I ought, nor their offences against myself. My +feelings are not puffed about with every attempt to move them. My temper +would perhaps be called resentful. My good opinion once lost is lost for +ever.” + +β€œ_That_ is a failing, indeed!” cried Elizabeth. β€œImplacable resentment +_is_ a shade in a character. But you have chosen your fault well. I +really cannot _laugh_ at it. You are safe from me.” + +β€œThere is, I believe, in every disposition a tendency to some particular +evil, a natural defect, which not even the best education can overcome.” + +β€œAnd _your_ defect is a propensity to hate everybody.” + +β€œAnd yours,” he replied, with a smile, β€œis wilfully to misunderstand +them.” + +β€œDo let us have a little music,” cried Miss Bingley, tired of a +conversation in which she had no share. β€œLouisa, you will not mind my +waking Mr. Hurst.” + +Her sister made not the smallest objection, and the pianoforte was +opened; and Darcy, after a few moments’ recollection, was not sorry for +it. He began to feel the danger of paying Elizabeth too much attention. + + + + +[Illustration] + + + + +CHAPTER XII. + + +[Illustration] + +In consequence of an agreement between the sisters, Elizabeth wrote the +next morning to her mother, to beg that the carriage might be sent for +them in the course of the day. But Mrs. Bennet, who had calculated on +her daughters remaining at Netherfield till the following Tuesday, which +would exactly finish Jane’s week, could not bring herself to receive +them with pleasure before. Her answer, therefore, was not propitious, at +least not to Elizabeth’s wishes, for she was impatient to get home. Mrs. +Bennet sent them word that they could not possibly have the carriage +before Tuesday; and in her postscript it was added, that if Mr. Bingley +and his sister pressed them to stay longer, she could spare them very +well. Against staying longer, however, Elizabeth was positively +resolved--nor did she much expect it would be asked; and fearful, on the +contrary, of being considered as intruding themselves needlessly long, +she urged Jane to borrow Mr. Bingley’s carriage immediately, and at +length it was settled that their original design of leaving Netherfield +that morning should be mentioned, and the request made. + +The communication excited many professions of concern; and enough was +said of wishing them to stay at least till the following day to work on +Jane; and till the morrow their going was deferred. Miss Bingley was +then sorry that she had proposed the delay; for her jealousy and dislike +of one sister much exceeded her affection for the other. + +The master of the house heard with real sorrow that they were to go so +soon, and repeatedly tried to persuade Miss Bennet that it would not be +safe for her--that she was not enough recovered; but Jane was firm where +she felt herself to be right. + +To Mr. Darcy it was welcome intelligence: Elizabeth had been at +Netherfield long enough. She attracted him more than he liked; and Miss +Bingley was uncivil to _her_ and more teasing than usual to himself. He +wisely resolved to be particularly careful that no sign of admiration +should _now_ escape him--nothing that could elevate her with the hope of +influencing his felicity; sensible that, if such an idea had been +suggested, his behaviour during the last day must have material weight +in confirming or crushing it. Steady to his purpose, he scarcely spoke +ten words to her through the whole of Saturday: and though they were at +one time left by themselves for half an hour, he adhered most +conscientiously to his book, and would not even look at her. + +On Sunday, after morning service, the separation, so agreeable to almost +all, took place. Miss Bingley’s civility to Elizabeth increased at last +very rapidly, as well as her affection for Jane; and when they parted, +after assuring the latter of the pleasure it would always give her to +see her either at Longbourn or Netherfield, and embracing her most +tenderly, she even shook hands with the former. Elizabeth took leave of +the whole party in the liveliest spirits. + +They were not welcomed home very cordially by their mother. Mrs. Bennet +wondered at their coming, and thought them very wrong to give so much +trouble, and was sure Jane would have caught cold again. But their +father, though very laconic in his expressions of pleasure, was really +glad to see them; he had felt their importance in the family circle. The +evening conversation, when they were all assembled, had lost much of its +animation, and almost all its sense, by the absence of Jane and +Elizabeth. + +They found Mary, as usual, deep in the study of thorough bass and human +nature; and had some new extracts to admire and some new observations of +threadbare morality to listen to. Catherine and Lydia had information +for them of a different sort. Much had been done, and much had been said +in the regiment since the preceding Wednesday; several of the officers +had dined lately with their uncle; a private had been flogged; and it +had actually been hinted that Colonel Forster was going to be married. + + + + +[Illustration] + + + + +CHAPTER XIII + + +[Illustration] + +β€œI hope, my dear,” said Mr. Bennet to his wife, as they were at +breakfast the next morning, β€œthat you have ordered a good dinner to-day, +because I have reason to expect an addition to our family party.” + +β€œWho do you mean, my dear? I know of nobody that is coming, I am sure, +unless Charlotte Lucas should happen to call in; and I hope _my_ dinners +are good enough for her. I do not believe she often sees such at home.” + +β€œThe person of whom I speak is a gentleman and a stranger.” + +Mrs. Bennet’s eyes sparkled. β€œA gentleman and a stranger! It is Mr. +Bingley, I am sure. Why, Jane--you never dropped a word of this--you sly +thing! Well, I am sure I shall be extremely glad to see Mr. Bingley. +But--good Lord! how unlucky! there is not a bit of fish to be got +to-day. Lydia, my love, ring the bell. I must speak to Hill this +moment.” + +β€œIt is _not_ Mr. Bingley,” said her husband; β€œit is a person whom I +never saw in the whole course of my life.” + +This roused a general astonishment; and he had the pleasure of being +eagerly questioned by his wife and five daughters at once. + +After amusing himself some time with their curiosity, he thus +explained:--β€œAbout a month ago I received this letter, and about a +fortnight ago I answered it; for I thought it a case of some delicacy, +and requiring early attention. It is from my cousin, Mr. Collins, who, +when I am dead, may turn you all out of this house as soon as he +pleases.” + +β€œOh, my dear,” cried his wife, β€œI cannot bear to hear that mentioned. +Pray do not talk of that odious man. I do think it is the hardest thing +in the world, that your estate should be entailed away from your own +children; and I am sure, if I had been you, I should have tried long ago +to do something or other about it.” + +Jane and Elizabeth attempted to explain to her the nature of an entail. +They had often attempted it before: but it was a subject on which Mrs. +Bennet was beyond the reach of reason; and she continued to rail +bitterly against the cruelty of settling an estate away from a family of +five daughters, in favour of a man whom nobody cared anything about. + +β€œIt certainly is a most iniquitous affair,” said Mr. Bennet; β€œand +nothing can clear Mr. Collins from the guilt of inheriting Longbourn. +But if you will listen to his letter, you may, perhaps, be a little +softened by his manner of expressing himself.” + +β€œNo, that I am sure I shall not: and I think it was very impertinent of +him to write to you at all, and very hypocritical. I hate such false +friends. Why could not he keep on quarrelling with you, as his father +did before him?” + +β€œWhy, indeed, he does seem to have had some filial scruples on that +head, as you will hear.” + + /* RIGHT β€œHunsford, near Westerham, Kent, _15th October_. */ + +β€œDear Sir, + + β€œThe disagreement subsisting between yourself and my late honoured + father always gave me much uneasiness; and, since I have had the + misfortune to lose him, I have frequently wished to heal the + breach: but, for some time, I was kept back by my own doubts, + fearing lest it might seem disrespectful to his memory for me to be + on good terms with anyone with whom it had always pleased him to be + at variance.”--β€˜There, Mrs. Bennet.’--β€œMy mind, however, is now + made up on the subject; for, having received ordination at Easter, + I have been so fortunate as to be distinguished by the patronage of + the Right Honourable Lady Catherine de Bourgh, widow of Sir Lewis + de Bourgh, whose bounty and beneficence has preferred me to the + valuable rectory of this parish, where it shall be my earnest + endeavour to demean myself with grateful respect towards her + Ladyship, and be ever ready to perform those rites and ceremonies + which are instituted by the Church of England. As a clergyman, + moreover, I feel it my duty to promote and establish the blessing + of peace in all families within the reach of my influence; and on + these grounds I flatter myself that my present overtures of + good-will are highly commendable, and that the circumstance of my + being next in the entail of Longbourn estate will be kindly + overlooked on your side, and not lead you to reject the offered + olive branch. I cannot be otherwise than concerned at being the + means of injuring your amiable daughters, and beg leave to + apologize for it, as well as to assure you of my readiness to make + them every possible amends; but of this hereafter. If you should + have no objection to receive me into your house, I propose myself + the satisfaction of waiting on you and your family, Monday, + November 18th, by four o’clock, and shall probably trespass on your + hospitality till the Saturday se’nnight following, which I can do + without any inconvenience, as Lady Catherine is far from objecting + to my occasional absence on a Sunday, provided that some other + clergyman is engaged to do the duty of the day. I remain, dear sir, + with respectful compliments to your lady and daughters, your + well-wisher and friend, + +β€œWILLIAM COLLINS.” + +β€œAt four o’clock, therefore, we may expect this peace-making gentleman,” +said Mr. Bennet, as he folded up the letter. β€œHe seems to be a most +conscientious and polite young man, upon my word; and, I doubt not, will +prove a valuable acquaintance, especially if Lady Catherine should be so +indulgent as to let him come to us again.” + +β€œThere is some sense in what he says about the girls, however; and, if +he is disposed to make them any amends, I shall not be the person to +discourage him.” + +β€œThough it is difficult,” said Jane, β€œto guess in what way he can mean +to make us the atonement he thinks our due, the wish is certainly to his +credit.” + +Elizabeth was chiefly struck with his extraordinary deference for Lady +Catherine, and his kind intention of christening, marrying, and burying +his parishioners whenever it were required. + +β€œHe must be an oddity, I think,” said she. β€œI cannot make him out. There +is something very pompous in his style. And what can he mean by +apologizing for being next in the entail? We cannot suppose he would +help it, if he could. Can he be a sensible man, sir?” + +β€œNo, my dear; I think not. I have great hopes of finding him quite the +reverse. There is a mixture of servility and self-importance in his +letter which promises well. I am impatient to see him.” + +β€œIn point of composition,” said Mary, β€œhis letter does not seem +defective. The idea of the olive branch perhaps is not wholly new, yet I +think it is well expressed.” + +To Catherine and Lydia neither the letter nor its writer were in any +degree interesting. It was next to impossible that their cousin should +come in a scarlet coat, and it was now some weeks since they had +received pleasure from the society of a man in any other colour. As for +their mother, Mr. Collins’s letter had done away much of her ill-will, +and she was preparing to see him with a degree of composure which +astonished her husband and daughters. + +Mr. Collins was punctual to his time, and was received with great +politeness by the whole family. Mr. Bennet indeed said little; but the +ladies were ready enough to talk, and Mr. Collins seemed neither in need +of encouragement, nor inclined to be silent himself. He was a tall, +heavy-looking young man of five-and-twenty. His air was grave and +stately, and his manners were very formal. He had not been long seated +before he complimented Mrs. Bennet on having so fine a family of +daughters, said he had heard much of their beauty, but that, in this +instance, fame had fallen short of the truth; and added, that he did not +doubt her seeing them all in due time well disposed of in marriage. This +gallantry was not much to the taste of some of his hearers; but Mrs. +Bennet, who quarrelled with no compliments, answered most readily,-- + +β€œYou are very kind, sir, I am sure; and I wish with all my heart it may +prove so; for else they will be destitute enough. Things are settled so +oddly.” + +β€œYou allude, perhaps, to the entail of this estate.” + +β€œAh, sir, I do indeed. It is a grievous affair to my poor girls, you +must confess. Not that I mean to find fault with _you_, for such things, +I know, are all chance in this world. There is no knowing how estates +will go when once they come to be entailed.” + +β€œI am very sensible, madam, of the hardship to my fair cousins, and +could say much on the subject, but that I am cautious of appearing +forward and precipitate. But I can assure the young ladies that I come +prepared to admire them. At present I will not say more, but, perhaps, +when we are better acquainted----” + +He was interrupted by a summons to dinner; and the girls smiled on each +other. They were not the only objects of Mr. Collins’s admiration. The +hall, the dining-room, and all its furniture, were examined and praised; +and his commendation of everything would have touched Mrs. Bennet’s +heart, but for the mortifying supposition of his viewing it all as his +own future property. The dinner, too, in its turn, was highly admired; +and he begged to know to which of his fair cousins the excellence of its +cookery was owing. But here he was set right by Mrs. Bennet, who assured +him, with some asperity, that they were very well able to keep a good +cook, and that her daughters had nothing to do in the kitchen. He begged +pardon for having displeased her. In a softened tone she declared +herself not at all offended; but he continued to apologize for about a +quarter of an hour. + + + + +[Illustration] + + + + +CHAPTER XIV + + +[Illustration] + +During dinner, Mr. Bennet scarcely spoke at all; but when the servants +were withdrawn, he thought it time to have some conversation with his +guest, and therefore started a subject in which he expected him to +shine, by observing that he seemed very fortunate in his patroness. Lady +Catherine de Bourgh’s attention to his wishes, and consideration for his +comfort, appeared very remarkable. Mr. Bennet could not have chosen +better. Mr. Collins was eloquent in her praise. The subject elevated him +to more than usual solemnity of manner; and with a most important aspect +he protested that he had never in his life witnessed such behaviour in a +person of rank--such affability and condescension, as he had himself +experienced from Lady Catherine. She had been graciously pleased to +approve of both the discourses which he had already had the honour of +preaching before her. She had also asked him twice to dine at Rosings, +and had sent for him only the Saturday before, to make up her pool of +quadrille in the evening. Lady Catherine was reckoned proud by many +people, he knew, but _he_ had never seen anything but affability in her. +She had always spoken to him as she would to any other gentleman; she +made not the smallest objection to his joining in the society of the +neighbourhood, nor to his leaving his parish occasionally for a week or +two to visit his relations. She had even condescended to advise him to +marry as soon as he could, provided he chose with discretion; and had +once paid him a visit in his humble parsonage, where she had perfectly +approved all the alterations he had been making, and had even vouchsafed +to suggest some herself,--some shelves in the closets upstairs. + +β€œThat is all very proper and civil, I am sure,” said Mrs. Bennet, β€œand I +dare say she is a very agreeable woman. It is a pity that great ladies +in general are not more like her. Does she live near you, sir?” + +β€œThe garden in which stands my humble abode is separated only by a lane +from Rosings Park, her Ladyship’s residence.” + +β€œI think you said she was a widow, sir? has she any family?” + +β€œShe has one only daughter, the heiress of Rosings, and of very +extensive property.” + +β€œAh,” cried Mrs. Bennet, shaking her head, β€œthen she is better off than +many girls. And what sort of young lady is she? Is she handsome?” + +β€œShe is a most charming young lady, indeed. Lady Catherine herself says +that, in point of true beauty, Miss de Bourgh is far superior to the +handsomest of her sex; because there is that in her features which marks +the young woman of distinguished birth. She is unfortunately of a sickly +constitution, which has prevented her making that progress in many +accomplishments which she could not otherwise have failed of, as I am +informed by the lady who superintended her education, and who still +resides with them. But she is perfectly amiable, and often condescends +to drive by my humble abode in her little phaeton and ponies.” + +β€œHas she been presented? I do not remember her name among the ladies at +court.” + +β€œHer indifferent state of health unhappily prevents her being in town; +and by that means, as I told Lady Catherine myself one day, has deprived +the British Court of its brightest ornament. Her Ladyship seemed pleased +with the idea; and you may imagine that I am happy on every occasion to +offer those little delicate compliments which are always acceptable to +ladies. I have more than once observed to Lady Catherine, that her +charming daughter seemed born to be a duchess; and that the most +elevated rank, instead of giving her consequence, would be adorned by +her. These are the kind of little things which please her Ladyship, and +it is a sort of attention which I conceive myself peculiarly bound to +pay.” + +β€œYou judge very properly,” said Mr. Bennet; β€œand it is happy for you +that you possess the talent of flattering with delicacy. May I ask +whether these pleasing attentions proceed from the impulse of the +moment, or are the result of previous study?” + +β€œThey arise chiefly from what is passing at the time; and though I +sometimes amuse myself with suggesting and arranging such little elegant +compliments as may be adapted to ordinary occasions, I always wish to +give them as unstudied an air as possible.” + +Mr. Bennet’s expectations were fully answered. His cousin was as absurd +as he had hoped; and he listened to him with the keenest enjoyment, +maintaining at the same time the most resolute composure of countenance, +and, except in an occasional glance at Elizabeth, requiring no partner +in his pleasure. + +By tea-time, however, the dose had been enough, and Mr. Bennet was glad +to take his guest into the drawing-room again, and when tea was over, +glad to invite him + +[Illustration: + +β€œProtested +that he never read novels” H.T Feb 94 +] + +to read aloud to the ladies. Mr. Collins readily assented, and a book +was produced; but on beholding it (for everything announced it to be +from a circulating library) he started back, and, begging pardon, +protested that he never read novels. Kitty stared at him, and Lydia +exclaimed. Other books were produced, and after some deliberation he +chose β€œFordyce’s Sermons.” Lydia gaped as he opened the volume; and +before he had, with very monotonous solemnity, read three pages, she +interrupted him with,-- + +β€œDo you know, mamma, that my uncle Philips talks of turning away +Richard? and if he does, Colonel Forster will hire him. My aunt told me +so herself on Saturday. I shall walk to Meryton to-morrow to hear more +about it, and to ask when Mr. Denny comes back from town.” + +Lydia was bid by her two eldest sisters to hold her tongue; but Mr. +Collins, much offended, laid aside his book, and said,-- + +β€œI have often observed how little young ladies are interested by books +of a serious stamp, though written solely for their benefit. It amazes +me, I confess; for certainly there can be nothing so advantageous to +them as instruction. But I will no longer importune my young cousin.” + +Then, turning to Mr. Bennet, he offered himself as his antagonist at +backgammon. Mr. Bennet accepted the challenge, observing that he acted +very wisely in leaving the girls to their own trifling amusements. Mrs. +Bennet and her daughters apologized most civilly for Lydia’s +interruption, and promised that it should not occur again, if he would +resume his book; but Mr. Collins, after assuring them that he bore his +young cousin no ill-will, and should never resent her behaviour as any +affront, seated himself at another table with Mr. Bennet, and prepared +for backgammon. + + + + +[Illustration] + + + + +CHAPTER XV. + + +[Illustration] + +Mr. Collins was not a sensible man, and the deficiency of nature had +been but little assisted by education or society; the greatest part of +his life having been spent under the guidance of an illiterate and +miserly father; and though he belonged to one of the universities, he +had merely kept the necessary terms without forming at it any useful +acquaintance. The subjection in which his father had brought him up had +given him originally great humility of manner; but it was now a good +deal counteracted by the self-conceit of a weak head, living in +retirement, and the consequential feelings of early and unexpected +prosperity. A fortunate chance had recommended him to Lady Catherine de +Bourgh when the living of Hunsford was vacant; and the respect which he +felt for her high rank, and his veneration for her as his patroness, +mingling with a very good opinion of himself, of his authority as a +clergyman, and his right as a rector, made him altogether a mixture of +pride and obsequiousness, self-importance and humility. + +Having now a good house and a very sufficient income, he intended to +marry; and in seeking a reconciliation with the Longbourn family he had +a wife in view, as he meant to choose one of the daughters, if he found +them as handsome and amiable as they were represented by common report. +This was his plan of amends--of atonement--for inheriting their father’s +estate; and he thought it an excellent one, full of eligibility and +suitableness, and excessively generous and disinterested on his own +part. + +His plan did not vary on seeing them. Miss Bennet’s lovely face +confirmed his views, and established all his strictest notions of what +was due to seniority; and for the first evening _she_ was his settled +choice. The next morning, however, made an alteration; for in a quarter +of an hour’s _tΓͺte-Γ -tΓͺte_ with Mrs. Bennet before breakfast, a +conversation beginning with his parsonage-house, and leading naturally +to the avowal of his hopes, that a mistress for it might be found at +Longbourn, produced from her, amid very complaisant smiles and general +encouragement, a caution against the very Jane he had fixed on. β€œAs to +her _younger_ daughters, she could not take upon her to say--she could +not positively answer--but she did not _know_ of any prepossession;--her +_eldest_ daughter she must just mention--she felt it incumbent on her to +hint, was likely to be very soon engaged.” + +Mr. Collins had only to change from Jane to Elizabeth--and it was soon +done--done while Mrs. Bennet was stirring the fire. Elizabeth, equally +next to Jane in birth and beauty, succeeded her of course. + +Mrs. Bennet treasured up the hint, and trusted that she might soon have +two daughters married; and the man whom she could not bear to speak of +the day before, was now high in her good graces. + +Lydia’s intention of walking to Meryton was not forgotten: every sister +except Mary agreed to go with her; and Mr. Collins was to attend them, +at the request of Mr. Bennet, who was most anxious to get rid of him, +and have his library to himself; for thither Mr. Collins had followed +him after breakfast, and there he would continue, nominally engaged with +one of the largest folios in the collection, but really talking to Mr. +Bennet, with little cessation, of his house and garden at Hunsford. Such +doings discomposed Mr. Bennet exceedingly. In his library he had been +always sure of leisure and tranquillity; and though prepared, as he told +Elizabeth, to meet with folly and conceit in every other room in the +house, he was used to be free from them there: his civility, therefore, +was most prompt in inviting Mr. Collins to join his daughters in their +walk; and Mr. Collins, being in fact much better fitted for a walker +than a reader, was extremely well pleased to close his large book, and +go. + +In pompous nothings on his side, and civil assents on that of his +cousins, their time passed till they entered Meryton. The attention of +the younger ones was then no longer to be gained by _him_. Their eyes +were immediately wandering up the street in quest of the officers, and +nothing less than a very smart bonnet, indeed, or a really new muslin in +a shop window, could recall them. + +But the attention of every lady was soon caught by a young man, whom +they had never seen before, of most gentlemanlike appearance, walking +with an officer on the other side of the way. The officer was the very +Mr. Denny concerning whose return from London Lydia came to inquire, and +he bowed as they passed. All were struck with the stranger’s air, all +wondered who he could be; and Kitty and Lydia, determined if possible +to find out, led the way across the street, under pretence of wanting +something in an opposite shop, and fortunately had just gained the +pavement, when the two gentlemen, turning back, had reached the same +spot. Mr. Denny addressed them directly, and entreated permission to +introduce his friend, Mr. Wickham, who had returned with him the day +before from town, and, he was happy to say, had accepted a commission in +their corps. This was exactly as it should be; for the young man wanted +only regimentals to make him completely charming. His appearance was +greatly in his favour: he had all the best parts of beauty, a fine +countenance, a good figure, and very pleasing address. The introduction +was followed up on his side by a happy readiness of conversation--a +readiness at the same time perfectly correct and unassuming; and the +whole party were still standing and talking together very agreeably, +when the sound of horses drew their notice, and Darcy and Bingley were +seen riding down the street. On distinguishing the ladies of the group +the two gentlemen came directly towards them, and began the usual +civilities. Bingley was the principal spokesman, and Miss Bennet the +principal object. He was then, he said, on his way to Longbourn on +purpose to inquire after her. Mr. Darcy corroborated it with a bow, and +was beginning to determine not to fix his eyes on Elizabeth, when they +were suddenly arrested by the sight of the stranger; and Elizabeth +happening to see the countenance of both as they looked at each other, +was all astonishment at the effect of the meeting. Both changed colour, +one looked white, the other red. Mr. Wickham, after a few moments, +touched his hat--a salutation which Mr. Darcy just deigned to return. +What could be the meaning of it? It was impossible to imagine; it was +impossible not to long to know. + +In another minute Mr. Bingley, but without seeming to have noticed what +passed, took leave and rode on with his friend. + +Mr. Denny and Mr. Wickham walked with the young ladies to the door of +Mr. Philips’s house, and then made their bows, in spite of Miss Lydia’s +pressing entreaties that they would come in, and even in spite of Mrs. +Philips’s throwing up the parlour window, and loudly seconding the +invitation. + +Mrs. Philips was always glad to see her nieces; and the two eldest, from +their recent absence, were particularly welcome; and she was eagerly +expressing her surprise at their sudden return home, which, as their own +carriage had not fetched them, she should have known nothing about, if +she had not happened to see Mr. Jones’s shopboy in the street, who had +told her that they were not to send any more draughts to Netherfield, +because the Miss Bennets were come away, when her civility was claimed +towards Mr. Collins by Jane’s introduction of him. She received him with +her very best politeness, which he returned with as much more, +apologizing for his intrusion, without any previous acquaintance with +her, which he could not help flattering himself, however, might be +justified by his relationship to the young ladies who introduced him to +her notice. Mrs. Philips was quite awed by such an excess of good +breeding; but her contemplation of one stranger was soon put an end to +by exclamations and inquiries about the other, of whom, however, she +could only tell her nieces what they already knew, that Mr. Denny had +brought him from London, and that he was to have a lieutenant’s +commission in the ----shire. She had been watching him the last hour, +she said, as he walked up and down the street,--and had Mr. Wickham +appeared, Kitty and Lydia would certainly have continued the occupation; +but unluckily no one passed the windows now except a few of the +officers, who, in comparison with the stranger, were become β€œstupid, +disagreeable fellows.” Some of them were to dine with the Philipses the +next day, and their aunt promised to make her husband call on Mr. +Wickham, and give him an invitation also, if the family from Longbourn +would come in the evening. This was agreed to; and Mrs. Philips +protested that they would have a nice comfortable noisy game of lottery +tickets, and a little bit of hot supper afterwards. The prospect of such +delights was very cheering, and they parted in mutual good spirits. Mr. +Collins repeated his apologies in quitting the room, and was assured, +with unwearying civility, that they were perfectly needless. + +As they walked home, Elizabeth related to Jane what she had seen pass +between the two gentlemen; but though Jane would have defended either or +both, had they appeared to be wrong, she could no more explain such +behaviour than her sister. + +Mr. Collins on his return highly gratified Mrs. Bennet by admiring Mrs. +Philips’s manners and politeness. He protested that, except Lady +Catherine and her daughter, he had never seen a more elegant woman; for +she had not only received him with the utmost civility, but had even +pointedly included him in her invitation for the next evening, although +utterly unknown to her before. Something, he supposed, might be +attributed to his connection with them, but yet he had never met with so +much attention in the whole course of his life. + + + + +[Illustration] + + + + +CHAPTER XVI. + + +[Illustration] + +As no objection was made to the young people’s engagement with their +aunt, and all Mr. Collins’s scruples of leaving Mr. and Mrs. Bennet for +a single evening during his visit were most steadily resisted, the coach +conveyed him and his five cousins at a suitable hour to Meryton; and the +girls had the pleasure of hearing, as they entered the drawing-room, +that Mr. Wickham had accepted their uncle’s invitation, and was then in +the house. + +When this information was given, and they had all taken their seats, Mr. +Collins was at leisure to look around him and admire, and he was so much +struck with the size and furniture of the apartment, that he declared he +might almost have supposed himself in the small summer breakfast parlour +at Rosings; a comparison that did not at first convey much +gratification; but when Mrs. Philips understood from him what Rosings +was, and who was its proprietor, when she had listened to the +description of only one of Lady Catherine’s drawing-rooms, and found +that the chimney-piece alone had cost eight hundred pounds, she felt all +the force of the compliment, and would hardly have resented a comparison +with the housekeeper’s room. + +In describing to her all the grandeur of Lady Catherine and her mansion, +with occasional digressions in praise of his own humble abode, and the +improvements it was receiving, he was happily employed until the +gentlemen joined them; and he found in Mrs. Philips a very attentive +listener, whose opinion of his consequence increased with what she +heard, and who was resolving to retail it all among her neighbours as +soon as she could. To the girls, who could not listen to their cousin, +and who had nothing to do but to wish for an instrument, and examine +their own indifferent imitations of china on the mantel-piece, the +interval of waiting appeared very long. It was over at last, however. +The gentlemen did approach: and when Mr. Wickham walked into the room, +Elizabeth felt that she had neither been seeing him before, nor thinking +of him since, with the smallest degree of unreasonable admiration. The +officers of the ----shire were in general a very creditable, +gentlemanlike set and the best of them were of the present party; but +Mr, Wickham was as far beyond them all in person, countenance, air, and +walk, as _they_ were superior to the broad-faced stuffy uncle Philips, +breathing port wine, who followed them into the room. + +[Illustration: + +β€œThe officers of the ----shire” + +[_Copyright 1894 by George Allen._]] + +Mr. Wickham was the happy man towards whom almost every female eye was +turned, and Elizabeth was the happy woman by whom he finally seated +himself; and the agreeable manner in which he immediately fell into +conversation, though it was only on its being a wet night, and on the +probability of a rainy season, made her feel that the commonest, +dullest, most threadbare topic might be rendered interesting by the +skill of the speaker. + +With such rivals for the notice of the fair as Mr. Wickham and the +officers, Mr. Collins seemed to sink into insignificance; to the young +ladies he certainly was nothing; but he had still at intervals a kind +listener in Mrs. Philips, and was, by her watchfulness, most abundantly +supplied with coffee and muffin. + +When the card tables were placed, he had an opportunity of obliging her, +in return, by sitting down to whist. + +β€œI know little of the game at present,” said he, β€œbut I shall be glad to +improve myself; for in my situation of life----” Mrs. Philips was very +thankful for his compliance, but could not wait for his reason. + +Mr. Wickham did not play at whist, and with ready delight was he +received at the other table between Elizabeth and Lydia. At first there +seemed danger of Lydia’s engrossing him entirely, for she was a most +determined talker; but being likewise extremely fond of lottery tickets, +she soon grew too much interested in the game, too eager in making bets +and exclaiming after prizes, to have attention for anyone in particular. +Allowing for the common demands of the game, Mr. Wickham was therefore +at leisure to talk to Elizabeth, and she was very willing to hear him, +though what she chiefly wished to hear she could not hope to be told, +the history of his acquaintance with Mr. Darcy. She dared not even +mention that gentleman. Her curiosity, however, was unexpectedly +relieved. Mr. Wickham began the subject himself. He inquired how far +Netherfield was from Meryton; and, after receiving her answer, asked in +a hesitating manner how long Mr. Darcy had been staying there. + +β€œAbout a month,” said Elizabeth; and then, unwilling to let the subject +drop, added, β€œhe is a man of very large property in Derbyshire, I +understand.” + +β€œYes,” replied Wickham; β€œhis estate there is a noble one. A clear ten +thousand per annum. You could not have met with a person more capable of +giving you certain information on that head than myself--for I have been +connected with his family, in a particular manner, from my infancy.” + +Elizabeth could not but look surprised. + +β€œYou may well be surprised, Miss Bennet, at such an assertion, after +seeing, as you probably might, the very cold manner of our meeting +yesterday. Are you much acquainted with Mr. Darcy?” + +β€œAs much as I ever wish to be,” cried Elizabeth, warmly. β€œI have spent +four days in the same house with him, and I think him very +disagreeable.” + +β€œI have no right to give _my_ opinion,” said Wickham, β€œas to his being +agreeable or otherwise. I am not qualified to form one. I have known him +too long and too well to be a fair judge. It is impossible for _me_ to +be impartial. But I believe your opinion of him would in general +astonish--and, perhaps, you would not express it quite so strongly +anywhere else. Here you are in your own family.” + +β€œUpon my word I say no more _here_ than I might say in any house in the +neighbourhood, except Netherfield. He is not at all liked in +Hertfordshire. Everybody is disgusted with his pride. You will not find +him more favourably spoken of by anyone.” + +β€œI cannot pretend to be sorry,” said Wickham, after a short +interruption, β€œthat he or that any man should not be estimated beyond +their deserts; but with _him_ I believe it does not often happen. The +world is blinded by his fortune and consequence, or frightened by his +high and imposing manners, and sees him only as he chooses to be seen.” + +β€œI should take him, even on _my_ slight acquaintance, to be an +ill-tempered man.” + +Wickham only shook his head. + +β€œI wonder,” said he, at the next opportunity of speaking, β€œwhether he is +likely to be in this country much longer.” + +β€œI do not at all know; but I _heard_ nothing of his going away when I +was at Netherfield. I hope your plans in favour of the ----shire will +not be affected by his being in the neighbourhood.” + +β€œOh no--it is not for _me_ to be driven away by Mr. Darcy. If _he_ +wishes to avoid seeing _me_ he must go. We are not on friendly terms, +and it always gives me pain to meet him, but I have no reason for +avoiding _him_ but what I might proclaim to all the world--a sense of +very great ill-usage, and most painful regrets at his being what he is. +His father, Miss Bennet, the late Mr. Darcy, was one of the best men +that ever breathed, and the truest friend I ever had; and I can never be +in company with this Mr. Darcy without being grieved to the soul by a +thousand tender recollections. His behaviour to myself has been +scandalous; but I verily believe I could forgive him anything and +everything, rather than his disappointing the hopes and disgracing the +memory of his father.” + +Elizabeth found the interest of the subject increase, and listened with +all her heart; but the delicacy of it prevented further inquiry. + +Mr. Wickham began to speak on more general topics, Meryton, the +neighbourhood, the society, appearing highly pleased with all that he +had yet seen, and speaking of the latter, especially, with gentle but +very intelligible gallantry. + +β€œIt was the prospect of constant society, and good society,” he added, +β€œwhich was my chief inducement to enter the ----shire. I know it to be a +most respectable, agreeable corps; and my friend Denny tempted me +further by his account of their present quarters, and the very great +attentions and excellent acquaintance Meryton had procured them. +Society, I own, is necessary to me. I have been a disappointed man, and +my spirits will not bear solitude. I _must_ have employment and society. +A military life is not what I was intended for, but circumstances have +now made it eligible. The church _ought_ to have been my profession--I +was brought up for the church; and I should at this time have been in +possession of a most valuable living, had it pleased the gentleman we +were speaking of just now.” + +β€œIndeed!” + +β€œYes--the late Mr. Darcy bequeathed me the next presentation of the best +living in his gift. He was my godfather, and excessively attached to me. +I cannot do justice to his kindness. He meant to provide for me amply, +and thought he had done it; but when the living fell, it was given +elsewhere.” + +β€œGood heavens!” cried Elizabeth; β€œbut how could _that_ be? How could his +will be disregarded? Why did not you seek legal redress?” + +β€œThere was just such an informality in the terms of the bequest as to +give me no hope from law. A man of honour could not have doubted the +intention, but Mr. Darcy chose to doubt it--or to treat it as a merely +conditional recommendation, and to assert that I had forfeited all claim +to it by extravagance, imprudence, in short, anything or nothing. +Certain it is that the living became vacant two years ago, exactly as I +was of an age to hold it, and that it was given to another man; and no +less certain is it, that I cannot accuse myself of having really done +anything to deserve to lose it. I have a warm unguarded temper, and I +may perhaps have sometimes spoken my opinion _of_ him, and _to_ him, too +freely. I can recall nothing worse. But the fact is, that we are very +different sort of men, and that he hates me.” + +β€œThis is quite shocking! He deserves to be publicly disgraced.” + +β€œSome time or other he _will_ be--but it shall not be by _me_. Till I +can forget his father, I can never defy or expose _him_.” + +Elizabeth honoured him for such feelings, and thought him handsomer than +ever as he expressed them. + +β€œBut what,” said she, after a pause, β€œcan have been his motive? what can +have induced him to behave so cruelly?” + +β€œA thorough, determined dislike of me--a dislike which I cannot but +attribute in some measure to jealousy. Had the late Mr. Darcy liked me +less, his son might have borne with me better; but his father’s uncommon +attachment to me irritated him, I believe, very early in life. He had +not a temper to bear the sort of competition in which we stood--the sort +of preference which was often given me.” + +β€œI had not thought Mr. Darcy so bad as this--though I have never liked +him, I had not thought so very ill of him--I had supposed him to be +despising his fellow-creatures in general, but did not suspect him of +descending to such malicious revenge, such injustice, such inhumanity as +this!” + +After a few minutes’ reflection, however, she continued, β€œI _do_ +remember his boasting one day, at Netherfield, of the implacability of +his resentments, of his having an unforgiving temper. His disposition +must be dreadful.” + +β€œI will not trust myself on the subject,” replied Wickham; β€œ_I_ can +hardly be just to him.” + +Elizabeth was again deep in thought, and after a time exclaimed, β€œTo +treat in such a manner the godson, the friend, the favourite of his +father!” She could have added, β€œA young man, too, like _you_, whose very +countenance may vouch for your being amiable.” But she contented herself +with--β€œAnd one, too, who had probably been his own companion from +childhood, connected together, as I think you said, in the closest +manner.” + +β€œWe were born in the same parish, within the same park; the greatest +part of our youth was passed together: inmates of the same house, +sharing the same amusements, objects of the same parental care. _My_ +father began life in the profession which your uncle, Mr. Philips, +appears to do so much credit to; but he gave up everything to be of use +to the late Mr. Darcy, and devoted all his time to the care of the +Pemberley property. He was most highly esteemed by Mr. Darcy, a most +intimate, confidential friend. Mr. Darcy often acknowledged himself to +be under the greatest obligations to my father’s active superintendence; +and when, immediately before my father’s death, Mr. Darcy gave him a +voluntary promise of providing for me, I am convinced that he felt it +to be as much a debt of gratitude to _him_ as of affection to myself.” + +β€œHow strange!” cried Elizabeth. β€œHow abominable! I wonder that the very +pride of this Mr. Darcy has not made him just to you. If from no better +motive, that he should not have been too proud to be dishonest,--for +dishonesty I must call it.” + +β€œIt _is_ wonderful,” replied Wickham; β€œfor almost all his actions may be +traced to pride; and pride has often been his best friend. It has +connected him nearer with virtue than any other feeling. But we are none +of us consistent; and in his behaviour to me there were stronger +impulses even than pride.” + +β€œCan such abominable pride as his have ever done him good?” + +β€œYes; it has often led him to be liberal and generous; to give his money +freely, to display hospitality, to assist his tenants, and relieve the +poor. Family pride, and _filial_ pride, for he is very proud of what his +father was, have done this. Not to appear to disgrace his family, to +degenerate from the popular qualities, or lose the influence of the +Pemberley House, is a powerful motive. He has also _brotherly_ pride, +which, with _some_ brotherly affection, makes him a very kind and +careful guardian of his sister; and you will hear him generally cried up +as the most attentive and best of brothers.” + +β€œWhat sort of a girl is Miss Darcy?” + +He shook his head. β€œI wish I could call her amiable. It gives me pain to +speak ill of a Darcy; but she is too much like her brother,--very, very +proud. As a child, she was affectionate and pleasing, and extremely fond +of me; and I have devoted hours and hours to her amusement. But she is +nothing to me now. She is a handsome girl, about fifteen or sixteen, +and, I understand, highly accomplished. Since her father’s death her +home has been London, where a lady lives with her, and superintends her +education.” + +After many pauses and many trials of other subjects, Elizabeth could not +help reverting once more to the first, and saying,-- + +β€œI am astonished at his intimacy with Mr. Bingley. How can Mr. Bingley, +who seems good-humour itself, and is, I really believe, truly amiable, +be in friendship with such a man? How can they suit each other? Do you +know Mr. Bingley?” + +β€œNot at all.” + +β€œHe is a sweet-tempered, amiable, charming man. He cannot know what Mr. +Darcy is.” + +β€œProbably not; but Mr. Darcy can please where he chooses. He does not +want abilities. He can be a conversible companion if he thinks it worth +his while. Among those who are at all his equals in consequence, he is a +very different man from what he is to the less prosperous. His pride +never deserts him; but with the rich he is liberal-minded, just, +sincere, rational, honourable, and, perhaps, agreeable,--allowing +something for fortune and figure.” + +The whist party soon afterwards breaking up, the players gathered round +the other table, and Mr. Collins took his station between his cousin +Elizabeth and Mrs. Philips. The usual inquiries as to his success were +made by the latter. It had not been very great; he had lost every point; +but when Mrs. Philips began to express her concern thereupon, he assured +her, with much earnest gravity, that it was not of the least importance; +that he considered the money as a mere trifle, and begged she would not +make herself uneasy. + +β€œI know very well, madam,” said he, β€œthat when persons sit down to a +card table they must take their chance of these things,--and happily I +am not in such circumstances as to make five shillings any object. There +are, undoubtedly, many who could not say the same; but, thanks to Lady +Catherine de Bourgh, I am removed far beyond the necessity of regarding +little matters.” + +Mr. Wickham’s attention was caught; and after observing Mr. Collins for +a few moments, he asked Elizabeth in a low voice whether her relations +were very intimately acquainted with the family of De Bourgh. + +β€œLady Catherine de Bourgh,” she replied, β€œhas very lately given him a +living. I hardly know how Mr. Collins was first introduced to her +notice, but he certainly has not known her long.” + +β€œYou know of course that Lady Catherine de Bourgh and Lady Anne Darcy +were sisters; consequently that she is aunt to the present Mr. Darcy.” + +β€œNo, indeed, I did not. I knew nothing at all of Lady Catherine’s +connections. I never heard of her existence till the day before +yesterday.” + +β€œHer daughter, Miss de Bourgh, will have a very large fortune, and it is +believed that she and her cousin will unite the two estates.” + +This information made Elizabeth smile, as she thought of poor Miss +Bingley. Vain indeed must be all her attentions, vain and useless her +affection for his sister and her praise of himself, if he were already +self-destined to another. + +β€œMr. Collins,” said she, β€œspeaks highly both of Lady Catherine and her +daughter; but, from some particulars that he has related of her +Ladyship, I suspect his gratitude misleads him; and that, in spite of +her being his patroness, she is an arrogant, conceited woman.” + +β€œI believe her to be both in a great degree,” replied Wickham; β€œI have +not seen her for many years; but I very well remember that I never liked +her, and that her manners were dictatorial and insolent. She has the +reputation of being remarkably sensible and clever; but I rather believe +she derives part of her abilities from her rank and fortune, part from +her authoritative manner, and the rest from the pride of her nephew, who +chooses that everyone connected with him should have an understanding of +the first class.” + +Elizabeth allowed that he had given a very rational account of it, and +they continued talking together with mutual satisfaction till supper put +an end to cards, and gave the rest of the ladies their share of Mr. +Wickham’s attentions. There could be no conversation in the noise of +Mrs. Philips’s supper party, but his manners recommended him to +everybody. Whatever he said, was said well; and whatever he did, done +gracefully. Elizabeth went away with her head full of him. She could +think of nothing but of Mr. Wickham, and of what he had told her, all +the way home; but there was not time for her even to mention his name as +they went, for neither Lydia nor Mr. Collins were once silent. Lydia +talked incessantly of lottery tickets, of the fish she had lost and the +fish she had won; and Mr. Collins, in describing the civility of Mr. and +Mrs. Philips, protesting that he did not in the least regard his losses +at whist, enumerating all the dishes at supper, and repeatedly fearing +that he crowded his cousins, had more to say than he could well manage +before the carriage stopped at Longbourn House. + + + + +[Illustration: + + β€œdelighted to see their dear friend again” +] + + + + +CHAPTER XVII. + + +[Illustration] + +Elizabeth related to Jane, the next day, what had passed between Mr. +Wickham and herself. Jane listened with astonishment and concern: she +knew not how to believe that Mr. Darcy could be so unworthy of Mr. +Bingley’s regard; and yet it was not in her nature to question the +veracity of a young man of such amiable appearance as Wickham. The +possibility of his having really endured such unkindness was enough to +interest all her tender feelings; and nothing therefore remained to be +done but to think well of them both, to defend the conduct of each, and +throw into the account of accident or mistake whatever could not be +otherwise explained. + +β€œThey have both,” said she, β€œbeen deceived, I dare say, in some way or +other, of which we can form no idea. Interested people have perhaps +misrepresented each to the other. It is, in short, impossible for us to +conjecture the causes or circumstances which may have alienated them, +without actual blame on either side.” + +β€œVery true, indeed; and now, my dear Jane, what have you got to say in +behalf of the interested people who have probably been concerned in the +business? Do clear _them_, too, or we shall be obliged to think ill of +somebody.” + +β€œLaugh as much as you choose, but you will not laugh me out of my +opinion. My dearest Lizzy, do but consider in what a disgraceful light +it places Mr. Darcy, to be treating his father’s favourite in such a +manner,--one whom his father had promised to provide for. It is +impossible. No man of common humanity, no man who had any value for his +character, could be capable of it. Can his most intimate friends be so +excessively deceived in him? Oh no.” + +β€œI can much more easily believe Mr. Bingley’s being imposed on than that +Mr. Wickham should invent such a history of himself as he gave me last +night; names, facts, everything mentioned without ceremony. If it be not +so, let Mr. Darcy contradict it. Besides, there was truth in his looks.” + +β€œIt is difficult, indeed--it is distressing. One does not know what to +think.” + +β€œI beg your pardon;--one knows exactly what to think.” + +But Jane could think with certainty on only one point,--that Mr. +Bingley, if he _had been_ imposed on, would have much to suffer when +the affair became public. + +The two young ladies were summoned from the shrubbery, where this +conversation passed, by the arrival of some of the very persons of whom +they had been speaking; Mr. Bingley and his sisters came to give their +personal invitation for the long expected ball at Netherfield, which was +fixed for the following Tuesday. The two ladies were delighted to see +their dear friend again, called it an age since they had met, and +repeatedly asked what she had been doing with herself since their +separation. To the rest of the family they paid little attention; +avoiding Mrs. Bennet as much as possible, saying not much to Elizabeth, +and nothing at all to the others. They were soon gone again, rising from +their seats with an activity which took their brother by surprise, and +hurrying off as if eager to escape from Mrs. Bennet’s civilities. + +The prospect of the Netherfield ball was extremely agreeable to every +female of the family. Mrs. Bennet chose to consider it as given in +compliment to her eldest daughter, and was particularly flattered by +receiving the invitation from Mr. Bingley himself, instead of a +ceremonious card. Jane pictured to herself a happy evening in the +society of her two friends, and the attentions of their brother; and +Elizabeth thought with pleasure of dancing a great deal with Mr. +Wickham, and of seeing a confirmation of everything in Mr. Darcy’s look +and behaviour. The happiness anticipated by Catherine and Lydia depended +less on any single event, or any particular person; for though they +each, like Elizabeth, meant to dance half the evening with Mr. Wickham, +he was by no means the only partner who could satisfy them, and a ball +was, at any rate, a ball. And even Mary could assure her family that she +had no disinclination for it. + +β€œWhile I can have my mornings to myself,” said she, β€œit is enough. I +think it is no sacrifice to join occasionally in evening engagements. +Society has claims on us all; and I profess myself one of those who +consider intervals of recreation and amusement as desirable for +everybody.” + +Elizabeth’s spirits were so high on the occasion, that though she did +not often speak unnecessarily to Mr. Collins, she could not help asking +him whether he intended to accept Mr. Bingley’s invitation, and if he +did, whether he would think it proper to join in the evening’s +amusement; and she was rather surprised to find that he entertained no +scruple whatever on that head, and was very far from dreading a rebuke, +either from the Archbishop or Lady Catherine de Bourgh, by venturing to +dance. + +β€œI am by no means of opinion, I assure you,” said he, β€œthat a ball of +this kind, given by a young man of character, to respectable people, can +have any evil tendency; and I am so far from objecting to dancing +myself, that I shall hope to be honoured with the hands of all my fair +cousins in the course of the evening; and I take this opportunity of +soliciting yours, Miss Elizabeth, for the two first dances especially; a +preference which I trust my cousin Jane will attribute to the right +cause, and not to any disrespect for her.” + +Elizabeth felt herself completely taken in. She had fully proposed being +engaged by Wickham for those very dances; and to have Mr. Collins +instead!--her liveliness had been never worse timed. There was no help +for it, however. Mr. Wickham’s happiness and her own was perforce +delayed a little longer, and Mr. Collins’s proposal accepted with as +good a grace as she could. She was not the better pleased with his +gallantry, from the idea it suggested of something more. It now first +struck her, that _she_ was selected from among her sisters as worthy of +being the mistress of Hunsford Parsonage, and of assisting to form a +quadrille table at Rosings, in the absence of more eligible visitors. +The idea soon reached to conviction, as she observed his increasing +civilities towards herself, and heard his frequent attempt at a +compliment on her wit and vivacity; and though more astonished than +gratified herself by this effect of her charms, it was not long before +her mother gave her to understand that the probability of their marriage +was exceedingly agreeable to _her_. Elizabeth, however, did not choose +to take the hint, being well aware that a serious dispute must be the +consequence of any reply. Mr. Collins might never make the offer, and, +till he did, it was useless to quarrel about him. + +If there had not been a Netherfield ball to prepare for and talk of, the +younger Miss Bennets would have been in a pitiable state at this time; +for, from the day of the invitation to the day of the ball, there was +such a succession of rain as prevented their walking to Meryton once. No +aunt, no officers, no news could be sought after; the very shoe-roses +for Netherfield were got by proxy. Even Elizabeth might have found some +trial of her patience in weather which totally suspended the improvement +of her acquaintance with Mr. Wickham; and nothing less than a dance on +Tuesday could have made such a Friday, Saturday, Sunday, and Monday +endurable to Kitty and Lydia. + + + + +[Illustration] + + + + +CHAPTER XVIII. + + +[Illustration] + +Till Elizabeth entered the drawing-room at Netherfield, and looked in +vain for Mr. Wickham among the cluster of red coats there assembled, a +doubt of his being present had never occurred to her. The certainty of +meeting him had not been checked by any of those recollections that +might not unreasonably have alarmed her. She had dressed with more than +usual care, and prepared in the highest spirits for the conquest of all +that remained unsubdued of his heart, trusting that it was not more than +might be won in the course of the evening. But in an instant arose the +dreadful suspicion of his being purposely omitted, for Mr. Darcy’s +pleasure, in the Bingleys’ invitation to the officers; and though this +was not exactly the case, the absolute fact of his absence was +pronounced by his friend Mr. Denny, to whom Lydia eagerly applied, and +who told them that Wickham had been obliged to go to town on business +the day before, and was not yet returned; adding, with a significant +smile,-- + +β€œI do not imagine his business would have called him away just now, if +he had not wished to avoid a certain gentleman here.” + +This part of his intelligence, though unheard by Lydia, was caught by +Elizabeth; and, as it assured her that Darcy was not less answerable for +Wickham’s absence than if her first surmise had been just, every feeling +of displeasure against the former was so sharpened by immediate +disappointment, that she could hardly reply with tolerable civility to +the polite inquiries which he directly afterwards approached to make. +Attention, forbearance, patience with Darcy, was injury to Wickham. She +was resolved against any sort of conversation with him, and turned away +with a degree of ill-humour which she could not wholly surmount even in +speaking to Mr. Bingley, whose blind partiality provoked her. + +But Elizabeth was not formed for ill-humour; and though every prospect +of her own was destroyed for the evening, it could not dwell long on her +spirits; and, having told all her griefs to Charlotte Lucas, whom she +had not seen for a week, she was soon able to make a voluntary +transition to the oddities of her cousin, and to point him out to her +particular notice. The two first dances, however, brought a return of +distress: they were dances of mortification. Mr. Collins, awkward and +solemn, apologizing instead of attending, and often moving wrong +without being aware of it, gave her all the shame and misery which a +disagreeable partner for a couple of dances can give. The moment of her +release from him was ecstasy. + +She danced next with an officer, and had the refreshment of talking of +Wickham, and of hearing that he was universally liked. When those dances +were over, she returned to Charlotte Lucas, and was in conversation with +her, when she found herself suddenly addressed by Mr. Darcy, who took +her so much by surprise in his application for her hand, that, without +knowing what she did, she accepted him. He walked away again +immediately, and she was left to fret over her own want of presence of +mind: Charlotte tried to console her. + +β€œI dare say you will find him very agreeable.” + +β€œHeaven forbid! _That_ would be the greatest misfortune of all! To find +a man agreeable whom one is determined to hate! Do not wish me such an +evil.” + +When the dancing recommenced, however, and Darcy approached to claim her +hand, Charlotte could not help cautioning her, in a whisper, not to be a +simpleton, and allow her fancy for Wickham to make her appear unpleasant +in the eyes of a man often times his consequence. Elizabeth made no +answer, and took her place in the set, amazed at the dignity to which +she was arrived in being allowed to stand opposite to Mr. Darcy, and +reading in her neighbours’ looks their equal amazement in beholding it. +They stood for some time without speaking a word; and she began to +imagine that their silence was to last through the two dances, and, at +first, was resolved not to break it; till suddenly fancying that it +would be the greater punishment to her partner to oblige him to talk, +she made some slight observation on the dance. He replied, and was again +silent. After a pause of some minutes, she addressed him a second time, +with-- + +β€œIt is _your_ turn to say something now, Mr. Darcy. _I_ talked about the +dance, and _you_ ought to make some kind of remark on the size of the +room, or the number of couples.” + +He smiled, and assured her that whatever she wished him to say should be +said. + +β€œVery well; that reply will do for the present. Perhaps, by-and-by, I +may observe that private balls are much pleasanter than public ones; but +_now_ we may be silent.” + +β€œDo you talk by rule, then, while you are dancing?” + +β€œSometimes. One must speak a little, you know. It would look odd to be +entirely silent for half an hour together; and yet, for the advantage of +_some_, conversation ought to be so arranged as that they may have the +trouble of saying as little as possible.” + +β€œAre you consulting your own feelings in the present case, or do you +imagine that you are gratifying mine?” + +β€œBoth,” replied Elizabeth archly; β€œfor I have always seen a great +similarity in the turn of our minds. We are each of an unsocial, +taciturn disposition, unwilling to speak, unless we expect to say +something that will amaze the whole room, and be handed down to +posterity with all the _Γ©clat_ of a proverb.” + +β€œThis is no very striking resemblance of your own character, I am sure,” +said he. β€œHow near it may be to _mine_, I cannot pretend to say. _You_ +think it a faithful portrait, undoubtedly.” + +β€œI must not decide on my own performance.” + +He made no answer; and they were again silent till they had gone down +the dance, when he asked her if she and her sisters did not very often +walk to Meryton. She answered in the affirmative; and, unable to resist +the temptation, added, β€œWhen you met us there the other day, we had just +been forming a new acquaintance.” + +The effect was immediate. A deeper shade of _hauteur_ overspread his +features, but he said not a word; and Elizabeth, though blaming herself +for her own weakness, could not go on. At length Darcy spoke, and in a +constrained manner said,-- + +β€œMr. Wickham is blessed with such happy manners as may insure his +_making_ friends; whether he may be equally capable of _retaining_ them, +is less certain.” + +β€œHe has been so unlucky as to lose your friendship,” replied Elizabeth, +with emphasis, β€œand in a manner which he is likely to suffer from all +his life.” + +Darcy made no answer, and seemed desirous of changing the subject. At +that moment Sir William Lucas appeared close to them, meaning to pass +through the set to the other side of the room; but, on perceiving Mr. +Darcy, he stopped, with a bow of superior courtesy, to compliment him on +his dancing and his partner. + +β€œI have been most highly gratified, indeed, my dear sir; such very +superior dancing is not often seen. It is evident that you belong to the +first circles. Allow me to say, however, that your fair partner does not +disgrace you: and that I must hope to have this pleasure often repeated, +especially when a certain desirable event, my dear Miss Eliza (glancing +at her sister and Bingley), shall take place. What congratulations will +then flow in! I appeal to Mr. Darcy;--but let me not interrupt you, sir. +You will not thank me for detaining you from the bewitching converse of +that young lady, whose bright eyes are also upbraiding me.” + +[Illustration: + +β€œSuch very superior dancing is not +often seen.” + +[_Copyright 1894 by George Allen._]] + +The latter part of this address was scarcely heard by Darcy; but Sir +William’s allusion to his friend seemed to strike him forcibly, and his +eyes were directed, with a very serious expression, towards Bingley and +Jane, who were dancing together. Recovering himself, however, shortly, +he turned to his partner, and said,-- + +β€œSir William’s interruption has made me forget what we were talking +of.” + +β€œI do not think we were speaking at all. Sir William could not have +interrupted any two people in the room who had less to say for +themselves. We have tried two or three subjects already without success, +and what we are to talk of next I cannot imagine.” + +β€œWhat think you of books?” said he, smiling. + +β€œBooks--oh no!--I am sure we never read the same, or not with the same +feelings.” + +β€œI am sorry you think so; but if that be the case, there can at least be +no want of subject. We may compare our different opinions.” + +β€œNo--I cannot talk of books in a ball-room; my head is always full of +something else.” + +β€œThe _present_ always occupies you in such scenes--does it?” said he, +with a look of doubt. + +β€œYes, always,” she replied, without knowing what she said; for her +thoughts had wandered far from the subject, as soon afterwards appeared +by her suddenly exclaiming, β€œI remember hearing you once say, Mr. Darcy, +that you hardly ever forgave;--that your resentment, once created, was +unappeasable. You are very cautious, I suppose, as to its _being +created_?” + +β€œI am,” said he, with a firm voice. + +β€œAnd never allow yourself to be blinded by prejudice?” + +β€œI hope not.” + +β€œIt is particularly incumbent on those who never change their opinion, +to be secure of judging properly at first.” + +β€œMay I ask to what these questions tend?” + +β€œMerely to the illustration of _your_ character,” said she, endeavouring +to shake off her gravity. β€œI am trying to make it out.” + +β€œAnd what is your success?” + +She shook her head. β€œI do not get on at all. I hear such different +accounts of you as puzzle me exceedingly.” + +β€œI can readily believe,” answered he, gravely, β€œthat reports may vary +greatly with respect to me; and I could wish, Miss Bennet, that you were +not to sketch my character at the present moment, as there is reason to +fear that the performance would reflect no credit on either.” + +β€œBut if I do not take your likeness now, I may never have another +opportunity.” + +β€œI would by no means suspend any pleasure of yours,” he coldly replied. +She said no more, and they went down the other dance and parted in +silence; on each side dissatisfied, though not to an equal degree; for +in Darcy’s breast there was a tolerably powerful feeling towards her, +which soon procured her pardon, and directed all his anger against +another. + +They had not long separated when Miss Bingley came towards her, and, +with an expression of civil disdain, thus accosted her,-- + +β€œSo, Miss Eliza, I hear you are quite delighted with George Wickham? +Your sister has been talking to me about him, and asking me a thousand +questions; and I find that the young man forgot to tell you, among his +other communications, that he was the son of old Wickham, the late Mr. +Darcy’s steward. Let me recommend you, however, as a friend, not to give +implicit confidence to all his assertions; for, as to Mr. Darcy’s using +him ill, it is perfectly false: for, on the contrary, he has been always +remarkably kind to him, though George Wickham has treated Mr. Darcy in a +most infamous manner. I do not know the particulars, but I know very +well that Mr. Darcy is not in the least to blame; that he cannot bear +to hear George Wickham mentioned; and that though my brother thought he +could not well avoid including him in his invitation to the officers, he +was excessively glad to find that he had taken himself out of the way. +His coming into the country at all is a most insolent thing, indeed, and +I wonder how he could presume to do it. I pity you, Miss Eliza, for this +discovery of your favourite’s guilt; but really, considering his +descent, one could not expect much better.” + +β€œHis guilt and his descent appear, by your account, to be the same,” +said Elizabeth, angrily; β€œfor I have heard you accuse him of nothing +worse than of being the son of Mr. Darcy’s steward, and of _that_, I can +assure you, he informed me himself.” + +β€œI beg your pardon,” replied Miss Bingley, turning away with a sneer. +β€œExcuse my interference; it was kindly meant.” + +β€œInsolent girl!” said Elizabeth to herself. β€œYou are much mistaken if +you expect to influence me by such a paltry attack as this. I see +nothing in it but your own wilful ignorance and the malice of Mr. +Darcy.” She then sought her eldest sister, who had undertaken to make +inquiries on the same subject of Bingley. Jane met her with a smile of +such sweet complacency, a glow of such happy expression, as sufficiently +marked how well she was satisfied with the occurrences of the evening. +Elizabeth instantly read her feelings; and, at that moment, solicitude +for Wickham, resentment against his enemies, and everything else, gave +way before the hope of Jane’s being in the fairest way for happiness. + +β€œI want to know,” said she, with a countenance no less smiling than her +sister’s, β€œwhat you have learnt about Mr. Wickham. But perhaps you have +been too pleasantly engaged to think of any third person, in which case +you may be sure of my pardon.” + +β€œNo,” replied Jane, β€œI have not forgotten him; but I have nothing +satisfactory to tell you. Mr. Bingley does not know the whole of his +history, and is quite ignorant of the circumstances which have +principally offended Mr. Darcy; but he will vouch for the good conduct, +the probity and honour, of his friend, and is perfectly convinced that +Mr. Wickham has deserved much less attention from Mr. Darcy than he has +received; and I am sorry to say that by his account, as well as his +sister’s, Mr. Wickham is by no means a respectable young man. I am +afraid he has been very imprudent, and has deserved to lose Mr. Darcy’s +regard.” + +β€œMr. Bingley does not know Mr. Wickham himself.” + +β€œNo; he never saw him till the other morning at Meryton.” + +β€œThis account then is what he has received from Mr. Darcy. I am +perfectly satisfied. But what does he say of the living?” + +β€œHe does not exactly recollect the circumstances, though he has heard +them from Mr. Darcy more than once, but he believes that it was left to +him _conditionally_ only.” + +β€œI have not a doubt of Mr. Bingley’s sincerity,” said Elizabeth warmly, +β€œbut you must excuse my not being convinced by assurances only. Mr. +Bingley’s defence of his friend was a very able one, I dare say; but +since he is unacquainted with several parts of the story, and has learnt +the rest from that friend himself, I shall venture still to think of +both gentlemen as I did before.” + +She then changed the discourse to one more gratifying to each, and on +which there could be no difference of sentiment. Elizabeth listened with +delight to the happy though modest hopes which Jane entertained of +Bingley’s regard, and said all in her power to heighten her confidence +in it. On their being joined by Mr. Bingley himself, Elizabeth withdrew +to Miss Lucas; to whose inquiry after the pleasantness of her last +partner she had scarcely replied, before Mr. Collins came up to them, +and told her with great exultation, that he had just been so fortunate +as to make a most important discovery. + +β€œI have found out,” said he, β€œby a singular accident, that there is now +in the room a near relation to my patroness. I happened to overhear the +gentleman himself mentioning to the young lady who does the honours of +this house the names of his cousin Miss De Bourgh, and of her mother, +Lady Catherine. How wonderfully these sort of things occur! Who would +have thought of my meeting with--perhaps--a nephew of Lady Catherine de +Bourgh in this assembly! I am most thankful that the discovery is made +in time for me to pay my respects to him, which I am now going to do, +and trust he will excuse my not having done it before. My total +ignorance of the connection must plead my apology.” + +β€œYou are not going to introduce yourself to Mr. Darcy?” + +β€œIndeed I am. I shall entreat his pardon for not having done it earlier. +I believe him to be Lady Catherine’s _nephew_. It will be in my power to +assure him that her Ladyship was quite well yesterday se’nnight.” + +Elizabeth tried hard to dissuade him from such a scheme; assuring him +that Mr. Darcy would consider his addressing him without introduction as +an impertinent freedom, rather than a compliment to his aunt; that it +was not in the least necessary there should be any notice on either +side, and that if it were, it must belong to Mr. Darcy, the superior in +consequence, to begin the acquaintance. Mr. Collins listened to her with +the determined air of following his own inclination, and when she ceased +speaking, replied thus,-- + +β€œMy dear Miss Elizabeth, I have the highest opinion in the world of your +excellent judgment in all matters within the scope of your +understanding, but permit me to say that there must be a wide difference +between the established forms of ceremony amongst the laity and those +which regulate the clergy; for, give me leave to observe that I consider +the clerical office as equal in point of dignity with the highest rank +in the kingdom--provided that a proper humility of behaviour is at the +same time maintained. You must, therefore, allow me to follow the +dictates of my conscience on this occasion, which lead me to perform +what I look on as a point of duty. Pardon me for neglecting to profit by +your advice, which on every other subject shall be my constant guide, +though in the case before us I consider myself more fitted by education +and habitual study to decide on what is right than a young lady like +yourself;” and with a low bow he left her to attack Mr. Darcy, whose +reception of his advances she eagerly watched, and whose astonishment at +being so addressed was very evident. Her cousin prefaced his speech with +a solemn bow, and though she could not hear a word of it, she felt as if +hearing it all, and saw in the motion of his lips the words β€œapology,” +β€œHunsford,” and β€œLady Catherine de Bourgh.” It vexed her to see him +expose himself to such a man. Mr. Darcy was eyeing him with +unrestrained wonder; and when at last Mr. Collins allowed him to speak, +replied with an air of distant civility. Mr. Collins, however, was not +discouraged from speaking again, and Mr. Darcy’s contempt seemed +abundantly increasing with the length of his second speech; and at the +end of it he only made him a slight bow, and moved another way: Mr. +Collins then returned to Elizabeth. + +β€œI have no reason, I assure you,” said he, β€œto be dissatisfied with my +reception. Mr. Darcy seemed much pleased with the attention. He answered +me with the utmost civility, and even paid me the compliment of saying, +that he was so well convinced of Lady Catherine’s discernment as to be +certain she could never bestow a favour unworthily. It was really a very +handsome thought. Upon the whole, I am much pleased with him.” + +As Elizabeth had no longer any interest of her own to pursue, she turned +her attention almost entirely on her sister and Mr. Bingley; and the +train of agreeable reflections which her observations gave birth to made +her perhaps almost as happy as Jane. She saw her in idea settled in that +very house, in all the felicity which a marriage of true affection could +bestow; and she felt capable, under such circumstances, of endeavouring +even to like Bingley’s two sisters. Her mother’s thoughts she plainly +saw were bent the same way, and she determined not to venture near her, +lest she might hear too much. When they sat down to supper, therefore, +she considered it a most unlucky perverseness which placed them within +one of each other; and deeply was she vexed to find that her mother was +talking to that one person (Lady Lucas) freely, openly, and of nothing +else but of her expectation that Jane would be soon married to Mr. +Bingley. It was an animating subject, and Mrs. Bennet seemed incapable +of fatigue while enumerating the advantages of the match. His being such +a charming young man, and so rich, and living but three miles from them, +were the first points of self-gratulation; and then it was such a +comfort to think how fond the two sisters were of Jane, and to be +certain that they must desire the connection as much as she could do. It +was, moreover, such a promising thing for her younger daughters, as +Jane’s marrying so greatly must throw them in the way of other rich men; +and, lastly, it was so pleasant at her time of life to be able to +consign her single daughters to the care of their sister, that she might +not be obliged to go into company more than she liked. It was necessary +to make this circumstance a matter of pleasure, because on such +occasions it is the etiquette; but no one was less likely than Mrs. +Bennet to find comfort in staying at home at any period of her life. She +concluded with many good wishes that Lady Lucas might soon be equally +fortunate, though evidently and triumphantly believing there was no +chance of it. + +In vain did Elizabeth endeavour to check the rapidity of her mother’s +words, or persuade her to describe her felicity in a less audible +whisper; for to her inexpressible vexation she could perceive that the +chief of it was overheard by Mr. Darcy, who sat opposite to them. Her +mother only scolded her for being nonsensical. + +β€œWhat is Mr. Darcy to me, pray, that I should be afraid of him? I am +sure we owe him no such particular civility as to be obliged to say +nothing _he_ may not like to hear.” + +β€œFor heaven’s sake, madam, speak lower. What advantage can it be to you +to offend Mr. Darcy? You will never recommend yourself to his friend by +so doing.” + +Nothing that she could say, however, had any influence. Her mother would +talk of her views in the same intelligible tone. Elizabeth blushed and +blushed again with shame and vexation. She could not help frequently +glancing her eye at Mr. Darcy, though every glance convinced her of what +she dreaded; for though he was not always looking at her mother, she was +convinced that his attention was invariably fixed by her. The expression +of his face changed gradually from indignant contempt to a composed and +steady gravity. + +At length, however, Mrs. Bennet had no more to say; and Lady Lucas, who +had been long yawning at the repetition of delights which she saw no +likelihood of sharing, was left to the comforts of cold ham and chicken. +Elizabeth now began to revive. But not long was the interval of +tranquillity; for when supper was over, singing was talked of, and she +had the mortification of seeing Mary, after very little entreaty, +preparing to oblige the company. By many significant looks and silent +entreaties did she endeavour to prevent such a proof of +complaisance,--but in vain; Mary would not understand them; such an +opportunity of exhibiting was delightful to her, and she began her song. +Elizabeth’s eyes were fixed on her, with most painful sensations; and +she watched her progress through the several stanzas with an impatience +which was very ill rewarded at their close; for Mary, on receiving +amongst the thanks of the table the hint of a hope that she might be +prevailed on to favour them again, after the pause of half a minute +began another. Mary’s powers were by no means fitted for such a display; +her voice was weak, and her manner affected. Elizabeth was in agonies. +She looked at Jane to see how she bore it; but Jane was very composedly +talking to Bingley. She looked at his two sisters, and saw them making +signs of derision at each other, and at Darcy, who continued, however, +impenetrably grave. She looked at her father to entreat his +interference, lest Mary should be singing all night. He took the hint, +and, when Mary had finished her second song, said aloud,-- + +β€œThat will do extremely well, child. You have delighted us long enough. +Let the other young ladies have time to exhibit.” + +Mary, though pretending not to hear, was somewhat disconcerted; and +Elizabeth, sorry for her, and sorry for her father’s speech, was afraid +her anxiety had done no good. Others of the party were now applied to. + +β€œIf I,” said Mr. Collins, β€œwere so fortunate as to be able to sing, I +should have great pleasure, I am sure, in obliging the company with an +air; for I consider music as a very innocent diversion, and perfectly +compatible with the profession of a clergyman. I do not mean, however, +to assert that we can be justified in devoting too much of our time to +music, for there are certainly other things to be attended to. The +rector of a parish has much to do. In the first place, he must make such +an agreement for tithes as may be beneficial to himself and not +offensive to his patron. He must write his own sermons; and the time +that remains will not be too much for his parish duties, and the care +and improvement of his dwelling, which he cannot be excused from making +as comfortable as possible. And I do not think it of light importance +that he should have attentive and conciliatory manners towards +everybody, especially towards those to whom he owes his preferment. I +cannot acquit him of that duty; nor could I think well of the man who +should omit an occasion of testifying his respect towards anybody +connected with the family.” And with a bow to Mr. Darcy, he concluded +his speech, which had been spoken so loud as to be heard by half the +room. Many stared--many smiled; but no one looked more amused than Mr. +Bennet himself, while his wife seriously commended Mr. Collins for +having spoken so sensibly, and observed, in a half-whisper to Lady +Lucas, that he was a remarkably clever, good kind of young man. + +To Elizabeth it appeared, that had her family made an agreement to +expose themselves as much as they could during the evening, it would +have been impossible for them to play their parts with more spirit, or +finer success; and happy did she think it for Bingley and her sister +that some of the exhibition had escaped his notice, and that his +feelings were not of a sort to be much distressed by the folly which he +must have witnessed. That his two sisters and Mr. Darcy, however, should +have such an opportunity of ridiculing her relations was bad enough; and +she could not determine whether the silent contempt of the gentleman, or +the insolent smiles of the ladies, were more intolerable. + +The rest of the evening brought her little amusement. She was teased by +Mr. Collins, who continued most perseveringly by her side; and though he +could not prevail with her to dance with him again, put it out of her +power to dance with others. In vain did she entreat him to stand up with +somebody else, and offered to introduce him to any young lady in the +room. He assured her that, as to dancing, he was perfectly indifferent +to it; that his chief object was, by delicate attentions, to recommend +himself to her; and that he should therefore make a point of remaining +close to her the whole evening. There was no arguing upon such a +project. She owed her greatest relief to her friend Miss Lucas, who +often joined them, and good-naturedly engaged Mr. Collins’s conversation +to herself. + +She was at least free from the offence of Mr. Darcy’s further notice: +though often standing within a very short distance of her, quite +disengaged, he never came near enough to speak. She felt it to be the +probable consequence of her allusions to Mr. Wickham, and rejoiced in +it. + +The Longbourn party were the last of all the company to depart; and by a +manΕ“uvre of Mrs. Bennet had to wait for their carriage a quarter of an +hour after everybody else was gone, which gave them time to see how +heartily they were wished away by some of the family. Mrs. Hurst and her +sister scarcely opened their mouths except to complain of fatigue, and +were evidently impatient to have the house to themselves. They repulsed +every attempt of Mrs. Bennet at conversation, and, by so doing, threw a +languor over the whole party, which was very little relieved by the long +speeches of Mr. Collins, who was complimenting Mr. Bingley and his +sisters on the elegance of their entertainment, and the hospitality and +politeness which had marked their behaviour to their guests. Darcy said +nothing at all. Mr. Bennet, in equal silence, was enjoying the scene. +Mr. Bingley and Jane were standing together a little detached from the +rest, and talked only to each other. Elizabeth preserved as steady a +silence as either Mrs. Hurst or Miss Bingley; and even Lydia was too +much fatigued to utter more than the occasional exclamation of β€œLord, +how tired I am!” accompanied by a violent yawn. + +When at length they arose to take leave, Mrs. Bennet was most pressingly +civil in her hope of seeing the whole family soon at Longbourn; and +addressed herself particularly to Mr. Bingley, to assure him how happy +he would make them, by eating a family dinner with them at any time, +without the ceremony of a formal invitation. Bingley was all grateful +pleasure; and he readily engaged for taking the earliest opportunity of +waiting on her after his return from London, whither he was obliged to +go the next day for a short time. + +Mrs. Bennet was perfectly satisfied; and quitted the house under the +delightful persuasion that, allowing for the necessary preparations of +settlements, new carriages, and wedding clothes, she should undoubtedly +see her daughter settled at Netherfield in the course of three or four +months. Of having another daughter married to Mr. Collins she thought +with equal certainty, and with considerable, though not equal, pleasure. +Elizabeth was the least dear to her of all her children; and though the +man and the match were quite good enough for _her_, the worth of each +was eclipsed by Mr. Bingley and Netherfield. + + + + +[Illustration: + + β€œto assure you in the most animated language” +] + + + + +CHAPTER XIX. + + +[Illustration] + +The next day opened a new scene at Longbourn. Mr. Collins made his +declaration in form. Having resolved to do it without loss of time, as +his leave of absence extended only to the following Saturday, and having +no feelings of diffidence to make it distressing to himself even at the +moment, he set about it in a very orderly manner, with all the +observances which he supposed a regular part of the business. On finding +Mrs. Bennet, Elizabeth, and one of the younger girls together, soon +after breakfast, he addressed the mother in these words,-- + +β€œMay I hope, madam, for your interest with your fair daughter Elizabeth, +when I solicit for the honour of a private audience with her in the +course of this morning?” + +Before Elizabeth had time for anything but a blush of surprise, Mrs. +Bennet instantly answered,-- + +β€œOh dear! Yes, certainly. I am sure Lizzy will be very happy--I am sure +she can have no objection. Come, Kitty, I want you upstairs.” And +gathering her work together, she was hastening away, when Elizabeth +called out,-- + +β€œDear ma’am, do not go. I beg you will not go. Mr. Collins must excuse +me. He can have nothing to say to me that anybody need not hear. I am +going away myself.” + +β€œNo, no, nonsense, Lizzy. I desire you will stay where you are.” And +upon Elizabeth’s seeming really, with vexed and embarrassed looks, about +to escape, she added, β€œLizzy, I _insist_ upon your staying and hearing +Mr. Collins.” + +Elizabeth would not oppose such an injunction; and a moment’s +consideration making her also sensible that it would be wisest to get it +over as soon and as quietly as possible, she sat down again, and tried +to conceal, by incessant employment, the feelings which were divided +between distress and diversion. Mrs. Bennet and Kitty walked off, and as +soon as they were gone, Mr. Collins began,-- + +β€œBelieve me, my dear Miss Elizabeth, that your modesty, so far from +doing you any disservice, rather adds to your other perfections. You +would have been less amiable in my eyes had there _not_ been this little +unwillingness; but allow me to assure you that I have your respected +mother’s permission for this address. You can hardly doubt the purport +of my discourse, however your natural delicacy may lead you to +dissemble; my attentions have been too marked to be mistaken. Almost as +soon as I entered the house I singled you out as the companion of my +future life. But before I am run away with by my feelings on this +subject, perhaps it will be advisable for me to state my reasons for +marrying--and, moreover, for coming into Hertfordshire with the design +of selecting a wife, as I certainly did.” + +The idea of Mr. Collins, with all his solemn composure, being run away +with by his feelings, made Elizabeth so near laughing that she could not +use the short pause he allowed in any attempt to stop him farther, and +he continued,-- + +β€œMy reasons for marrying are, first, that I think it a right thing for +every clergyman in easy circumstances (like myself) to set the example +of matrimony in his parish; secondly, that I am convinced it will add +very greatly to my happiness; and, thirdly, which perhaps I ought to +have mentioned earlier, that it is the particular advice and +recommendation of the very noble lady whom I have the honour of calling +patroness. Twice has she condescended to give me her opinion (unasked +too!) on this subject; and it was but the very Saturday night before I +left Hunsford,--between our pools at quadrille, while Mrs. Jenkinson was +arranging Miss De Bourgh’s footstool,--that she said, β€˜Mr. Collins, you +must marry. A clergyman like you must marry. Choose properly, choose a +gentlewoman for _my_ sake, and for your _own_; let her be an active, +useful sort of person, not brought up high, but able to make a small +income go a good way. This is my advice. Find such a woman as soon as +you can, bring her to Hunsford, and I will visit her.’ Allow me, by the +way, to observe, my fair cousin, that I do not reckon the notice and +kindness of Lady Catherine de Bourgh as among the least of the +advantages in my power to offer. You will find her manners beyond +anything I can describe; and your wit and vivacity, I think, must be +acceptable to her, especially when tempered with the silence and respect +which her rank will inevitably excite. Thus much for my general +intention in favour of matrimony; it remains to be told why my views +were directed to Longbourn instead of my own neighbourhood, where I +assure you there are many amiable young women. But the fact is, that +being, as I am, to inherit this estate after the death of your honoured +father (who, however, may live many years longer), I could not satisfy +myself without resolving to choose a wife from among his daughters, that +the loss to them might be as little as possible when the melancholy +event takes place--which, however, as I have already said, may not be +for several years. This has been my motive, my fair cousin, and I +flatter myself it will not sink me in your esteem. And now nothing +remains for me but to assure you in the most animated language of the +violence of my affection. To fortune I am perfectly indifferent, and +shall make no demand of that nature on your father, since I am well +aware that it could not be complied with; and that one thousand pounds +in the 4 per cents., which will not be yours till after your mother’s +decease, is all that you may ever be entitled to. On that head, +therefore, I shall be uniformly silent: and you may assure yourself that +no ungenerous reproach shall ever pass my lips when we are married.” + +It was absolutely necessary to interrupt him now. + +β€œYou are too hasty, sir,” she cried. β€œYou forget that I have made no +answer. Let me do it without further loss of time. Accept my thanks for +the compliment you are paying me. I am very sensible of the honour of +your proposals, but it is impossible for me to do otherwise than decline +them.” + +β€œI am not now to learn,” replied Mr. Collins, with a formal wave of the +hand, β€œthat it is usual with young ladies to reject the addresses of the +man whom they secretly mean to accept, when he first applies for their +favour; and that sometimes the refusal is repeated a second or even a +third time. I am, therefore, by no means discouraged by what you have +just said, and shall hope to lead you to the altar ere long.” + +β€œUpon my word, sir,” cried Elizabeth, β€œyour hope is rather an +extraordinary one after my declaration. I do assure you that I am not +one of those young ladies (if such young ladies there are) who are so +daring as to risk their happiness on the chance of being asked a second +time. I am perfectly serious in my refusal. You could not make _me_ +happy, and I am convinced that I am the last woman in the world who +would make _you_ so. Nay, were your friend Lady Catherine to know me, I +am persuaded she would find me in every respect ill qualified for the +situation.” + +β€œWere it certain that Lady Catherine would think so,” said Mr. Collins, +very gravely--β€œbut I cannot imagine that her Ladyship would at all +disapprove of you. And you may be certain that when I have the honour of +seeing her again I shall speak in the highest terms of your modesty, +economy, and other amiable qualifications.” + +β€œIndeed, Mr. Collins, all praise of me will be unnecessary. You must +give me leave to judge for myself, and pay me the compliment of +believing what I say. I wish you very happy and very rich, and by +refusing your hand, do all in my power to prevent your being otherwise. +In making me the offer, you must have satisfied the delicacy of your +feelings with regard to my family, and may take possession of Longbourn +estate whenever it falls, without any self-reproach. This matter may be +considered, therefore, as finally settled.” And rising as she thus +spoke, she would have quitted the room, had not Mr. Collins thus +addressed her,-- + +β€œWhen I do myself the honour of speaking to you next on the subject, I +shall hope to receive a more favourable answer than you have now given +me; though I am far from accusing you of cruelty at present, because I +know it to be the established custom of your sex to reject a man on the +first application, and, perhaps, you have even now said as much to +encourage my suit as would be consistent with the true delicacy of the +female character.” + +β€œReally, Mr. Collins,” cried Elizabeth, with some warmth, β€œyou puzzle me +exceedingly. If what I have hitherto said can appear to you in the form +of encouragement, I know not how to express my refusal in such a way as +may convince you of its being one.” + +β€œYou must give me leave to flatter myself, my dear cousin, that your +refusal of my addresses are merely words of course. My reasons for +believing it are briefly these:--It does not appear to me that my hand +is unworthy of your acceptance, or that the establishment I can offer +would be any other than highly desirable. My situation in life, my +connections with the family of De Bourgh, and my relationship to your +own, are circumstances highly in my favour; and you should take it into +further consideration that, in spite of your manifold attractions, it is +by no means certain that another offer of marriage may ever be made you. +Your portion is unhappily so small, that it will in all likelihood undo +the effects of your loveliness and amiable qualifications. As I must, +therefore, conclude that you are not serious in your rejection of me, I +shall choose to attribute it to your wish of increasing my love by +suspense, according to the usual practice of elegant females.” + +β€œI do assure you, sir, that I have no pretensions whatever to that kind +of elegance which consists in tormenting a respectable man. I would +rather be paid the compliment of being believed sincere. I thank you +again and again for the honour you have done me in your proposals, but +to accept them is absolutely impossible. My feelings in every respect +forbid it. Can I speak plainer? Do not consider me now as an elegant +female intending to plague you, but as a rational creature speaking the +truth from her heart.” + +β€œYou are uniformly charming!” cried he, with an air of awkward +gallantry; β€œand I am persuaded that, when sanctioned by the express +authority of both your excellent parents, my proposals will not fail of +being acceptable.” + +To such perseverance in wilful self-deception Elizabeth would make no +reply, and immediately and in silence withdrew; determined, that if he +persisted in considering her repeated refusals as flattering +encouragement, to apply to her father, whose negative might be uttered +in such a manner as must be decisive, and whose behaviour at least could +not be mistaken for the affectation and coquetry of an elegant female. + + + + +[Illustration] + + + + +CHAPTER XX. + + +[Illustration] + +Mr. Collins was not left long to the silent contemplation of his +successful love; for Mrs. Bennet, having dawdled about in the vestibule +to watch for the end of the conference, no sooner saw Elizabeth open the +door and with quick step pass her towards the staircase, than she +entered the breakfast-room, and congratulated both him and herself in +warm terms on the happy prospect of their nearer connection. Mr. Collins +received and returned these felicitations with equal pleasure, and then +proceeded to relate the particulars of their interview, with the result +of which he trusted he had every reason to be satisfied, since the +refusal which his cousin had steadfastly given him would naturally flow +from her bashful modesty and the genuine delicacy of her character. + +This information, however, startled Mrs. Bennet: she would have been +glad to be equally satisfied that her daughter had meant to encourage +him by protesting against his proposals, but she dared not believe it, +and could not help saying so. + +β€œBut depend upon it, Mr. Collins,” she added, β€œthat Lizzy shall be +brought to reason. I will speak to her about it myself directly. She is +a very headstrong, foolish girl, and does not know her own interest; but +I will _make_ her know it.” + +β€œPardon me for interrupting you, madam,” cried Mr. Collins; β€œbut if she +is really headstrong and foolish, I know not whether she would +altogether be a very desirable wife to a man in my situation, who +naturally looks for happiness in the marriage state. If, therefore, she +actually persists in rejecting my suit, perhaps it were better not to +force her into accepting me, because, if liable to such defects of +temper, she could not contribute much to my felicity.” + +β€œSir, you quite misunderstand me,” said Mrs. Bennet, alarmed. β€œLizzy is +only headstrong in such matters as these. In everything else she is as +good-natured a girl as ever lived. I will go directly to Mr. Bennet, and +we shall very soon settle it with her, I am sure.” + +She would not give him time to reply, but hurrying instantly to her +husband, called out, as she entered the library,-- + +β€œOh, Mr. Bennet, you are wanted immediately; we are all in an uproar. +You must come and make Lizzy marry Mr. Collins, for she vows she will +not have him; and if you do not make haste he will change his mind and +not have _her_.” + +Mr. Bennet raised his eyes from his book as she entered, and fixed them +on her face with a calm unconcern, which was not in the least altered by +her communication. + +β€œI have not the pleasure of understanding you,” said he, when she had +finished her speech. β€œOf what are you talking?” + +β€œOf Mr. Collins and Lizzy. Lizzy declares she will not have Mr. Collins, +and Mr. Collins begins to say that he will not have Lizzy.” + +β€œAnd what am I to do on the occasion? It seems a hopeless business.” + +β€œSpeak to Lizzy about it yourself. Tell her that you insist upon her +marrying him.” + +β€œLet her be called down. She shall hear my opinion.” + +Mrs. Bennet rang the bell, and Miss Elizabeth was summoned to the +library. + +β€œCome here, child,” cried her father as she appeared. β€œI have sent for +you on an affair of importance. I understand that Mr. Collins has made +you an offer of marriage. Is it true?” + +Elizabeth replied that it was. + +β€œVery well--and this offer of marriage you have refused?” + +β€œI have, sir.” + +β€œVery well. We now come to the point. Your mother insists upon your +accepting it. Is it not so, Mrs. Bennet?” + +β€œYes, or I will never see her again.” + +β€œAn unhappy alternative is before you, Elizabeth. From this day you must +be a stranger to one of your parents. Your mother will never see you +again if you do _not_ marry Mr. Collins, and I will never see you again +if you _do_.” + +Elizabeth could not but smile at such a conclusion of such a beginning; +but Mrs. Bennet, who had persuaded herself that her husband regarded the +affair as she wished, was excessively disappointed. + +β€œWhat do you mean, Mr. Bennet, by talking in this way? You promised me +to _insist_ upon her marrying him.” + +β€œMy dear,” replied her husband, β€œI have two small favours to request. +First, that you will allow me the free use of my understanding on the +present occasion; and, secondly, of my room. I shall be glad to have the +library to myself as soon as may be.” + +Not yet, however, in spite of her disappointment in her husband, did +Mrs. Bennet give up the point. She talked to Elizabeth again and again; +coaxed and threatened her by turns. She endeavoured to secure Jane in +her interest, but Jane, with all possible mildness, declined +interfering; and Elizabeth, sometimes with real earnestness, and +sometimes with playful gaiety, replied to her attacks. Though her manner +varied, however, her determination never did. + +Mr. Collins, meanwhile, was meditating in solitude on what had passed. +He thought too well of himself to comprehend on what motive his cousin +could refuse him; and though his pride was hurt, he suffered in no other +way. His regard for her was quite imaginary; and the possibility of her +deserving her mother’s reproach prevented his feeling any regret. + +While the family were in this confusion, Charlotte Lucas came to spend +the day with them. She was met in the vestibule by Lydia, who, flying to +her, cried in a half whisper, β€œI am glad you are come, for there is such +fun here! What do you think has happened this morning? Mr. Collins has +made an offer to Lizzy, and she will not have him.” + +[Illustration: + + β€œthey entered the breakfast room” +] + +Charlotte had hardly time to answer before they were joined by Kitty, +who came to tell the same news; and no sooner had they entered the +breakfast-room, where Mrs. Bennet was alone, than she likewise began on +the subject, calling on Miss Lucas for her compassion, and entreating +her to persuade her friend Lizzy to comply with the wishes of her +family. β€œPray do, my dear Miss Lucas,” she added, in a melancholy tone; +β€œfor nobody is on my side, nobody takes part with me; I am cruelly used, +nobody feels for my poor nerves.” + +Charlotte’s reply was spared by the entrance of Jane and Elizabeth. + +β€œAy, there she comes,” continued Mrs. Bennet, β€œlooking as unconcerned as +may be, and caring no more for us than if we were at York, provided she +can have her own way. But I tell you what, Miss Lizzy, if you take it +into your head to go on refusing every offer of marriage in this way, +you will never get a husband at all--and I am sure I do not know who is +to maintain you when your father is dead. _I_ shall not be able to keep +you--and so I warn you. I have done with you from this very day. I told +you in the library, you know, that I should never speak to you again, +and you will find me as good as my word. I have no pleasure in talking +to undutiful children. Not that I have much pleasure, indeed, in talking +to anybody. People who suffer as I do from nervous complaints can have +no great inclination for talking. Nobody can tell what I suffer! But it +is always so. Those who do not complain are never pitied.” + +Her daughters listened in silence to this effusion, sensible that any +attempt to reason with or soothe her would only increase the irritation. +She talked on, therefore, without interruption from any of them till +they were joined by Mr. Collins, who entered with an air more stately +than usual, and on perceiving whom, she said to the girls,-- + +β€œNow, I do insist upon it, that you, all of you, hold your tongues, and +let Mr. Collins and me have a little conversation together.” + +Elizabeth passed quietly out of the room, Jane and Kitty followed, but +Lydia stood her ground, determined to hear all she could; and Charlotte, +detained first by the civility of Mr. Collins, whose inquiries after +herself and all her family were very minute, and then by a little +curiosity, satisfied herself with walking to the window and pretending +not to hear. In a doleful voice Mrs. Bennet thus began the projected +conversation:-- + +β€œOh, Mr. Collins!” + +β€œMy dear madam,” replied he, β€œlet us be for ever silent on this point. +Far be it from me,” he presently continued, in a voice that marked his +displeasure, β€œto resent the behaviour of your daughter. Resignation to +inevitable evils is the duty of us all: the peculiar duty of a young man +who has been so fortunate as I have been, in early preferment; and, I +trust, I am resigned. Perhaps not the less so from feeling a doubt of my +positive happiness had my fair cousin honoured me with her hand; for I +have often observed, that resignation is never so perfect as when the +blessing denied begins to lose somewhat of its value in our estimation. +You will not, I hope, consider me as showing any disrespect to your +family, my dear madam, by thus withdrawing my pretensions to your +daughter’s favour, without having paid yourself and Mr. Bennet the +compliment of requesting you to interpose your authority in my behalf. +My conduct may, I fear, be objectionable in having accepted my +dismission from your daughter’s lips instead of your own; but we are all +liable to error. I have certainly meant well through the whole affair. +My object has been to secure an amiable companion for myself, with due +consideration for the advantage of all your family; and if my _manner_ +has been at all reprehensible, I here beg leave to apologize.” + + + + +[Illustration] + + + + +CHAPTER XXI. + + +[Illustration] + +The discussion of Mr. Collins’s offer was now nearly at an end, and +Elizabeth had only to suffer from the uncomfortable feelings necessarily +attending it, and occasionally from some peevish allusion of her mother. +As for the gentleman himself, _his_ feelings were chiefly expressed, not +by embarrassment or dejection, or by trying to avoid her, but by +stiffness of manner and resentful silence. He scarcely ever spoke to +her; and the assiduous attentions which he had been so sensible of +himself were transferred for the rest of the day to Miss Lucas, whose +civility in listening to him was a seasonable relief to them all, and +especially to her friend. + +The morrow produced no abatement of Mrs. Bennet’s ill humour or ill +health. Mr. Collins was also in the same state of angry pride. Elizabeth +had hoped that his resentment might shorten his visit, but his plan did +not appear in the least affected by it. He was always to have gone on +Saturday, and to Saturday he still meant to stay. + +After breakfast, the girls walked to Meryton, to inquire if Mr. Wickham +were returned, and to lament over his absence from the Netherfield ball. +He joined them on their entering the town, and attended them to their +aunt’s, where his regret and vexation and the concern of everybody were +well talked over. To Elizabeth, however, he voluntarily acknowledged +that the necessity of his absence _had_ been self-imposed. + +β€œI found,” said he, β€œas the time drew near, that I had better not meet +Mr. Darcy;--that to be in the same room, the same party with him for so +many hours together, might be more than I could bear, and that scenes +might arise unpleasant to more than myself.” + +She highly approved his forbearance; and they had leisure for a full +discussion of it, and for all the commendations which they civilly +bestowed on each other, as Wickham and another officer walked back with +them to Longbourn, and during the walk he particularly attended to her. +His accompanying them was a double advantage: she felt all the +compliment it offered to herself; and it was most acceptable as an +occasion of introducing him to her father and mother. + +[Illustration: β€œWalked back with them” + +[_Copyright 1894 by George Allen._]] + +Soon after their return, a letter was delivered to Miss Bennet; it came +from Netherfield, and was opened immediately. The envelope contained a +sheet of elegant, little, hot-pressed paper, well covered with a lady’s +fair, flowing hand; and Elizabeth saw her sister’s countenance change as +she read it, and saw her dwelling intently on some particular passages. +Jane recollected herself soon; and putting the letter away, tried to +join, with her usual cheerfulness, in the general conversation: but +Elizabeth felt an anxiety on the subject which drew off her attention +even from Wickham; and no sooner had he and his companion taken leave, +than a glance from Jane invited her to follow her upstairs. When they +had gained their own room, Jane, taking out her letter, said, β€œThis is +from Caroline Bingley: what it contains has surprised me a good deal. +The whole party have left Netherfield by this time, and are on their way +to town; and without any intention of coming back again. You shall hear +what she says.” + +She then read the first sentence aloud, which comprised the information +of their having just resolved to follow their brother to town directly, +and of their meaning to dine that day in Grosvenor Street, where Mr. +Hurst had a house. The next was in these words:--β€œβ€˜I do not pretend to +regret anything I shall leave in Hertfordshire except your society, my +dearest friend; but we will hope, at some future period, to enjoy many +returns of that delightful intercourse we have known, and in the +meanwhile may lessen the pain of separation by a very frequent and most +unreserved correspondence. I depend on you for that.’” To these +high-flown expressions Elizabeth listened with all the insensibility of +distrust; and though the suddenness of their removal surprised her, she +saw nothing in it really to lament: it was not to be supposed that their +absence from Netherfield would prevent Mr. Bingley’s being there; and as +to the loss of their society, she was persuaded that Jane must soon +cease to regard it in the enjoyment of his. + +β€œIt is unlucky,” said she, after a short pause, β€œthat you should not be +able to see your friends before they leave the country. But may we not +hope that the period of future happiness, to which Miss Bingley looks +forward, may arrive earlier than she is aware, and that the delightful +intercourse you have known as friends will be renewed with yet greater +satisfaction as sisters? Mr. Bingley will not be detained in London by +them.” + +β€œCaroline decidedly says that none of the party will return into +Hertfordshire this winter. I will read it to you. + +β€œβ€˜When my brother left us yesterday, he imagined that the business which +took him to London might be concluded in three or four days; but as we +are certain it cannot be so, and at the same time convinced that when +Charles gets to town he will be in no hurry to leave it again, we have +determined on following him thither, that he may not be obliged to spend +his vacant hours in a comfortless hotel. Many of my acquaintance are +already there for the winter: I wish I could hear that you, my dearest +friend, had any intention of making one in the crowd, but of that I +despair. I sincerely hope your Christmas in Hertfordshire may abound in +the gaieties which that season generally brings, and that your beaux +will be so numerous as to prevent your feeling the loss of the three of +whom we shall deprive you.’ + +β€œIt is evident by this,” added Jane, β€œthat he comes back no more this +winter.” + +β€œIt is only evident that Miss Bingley does not mean he _should_.” + +β€œWhy will you think so? It must be his own doing; he is his own master. +But you do not know _all_. I _will_ read you the passage which +particularly hurts me. I will have no reserves from _you_. β€˜Mr. Darcy is +impatient to see his sister; and to confess the truth, _we_ are scarcely +less eager to meet her again. I really do not think Georgiana Darcy has +her equal for beauty, elegance, and accomplishments; and the affection +she inspires in Louisa and myself is heightened into something still +more interesting from the hope we dare to entertain of her being +hereafter our sister. I do not know whether I ever before mentioned to +you my feelings on this subject, but I will not leave the country +without confiding them, and I trust you will not esteem them +unreasonable. My brother admires her greatly already; he will have +frequent opportunity now of seeing her on the most intimate footing; her +relations all wish the connection as much as his own; and a sister’s +partiality is not misleading me, I think, when I call Charles most +capable of engaging any woman’s heart. With all these circumstances to +favour an attachment, and nothing to prevent it, am I wrong, my dearest +Jane, in indulging the hope of an event which will secure the happiness +of so many?’ What think you of _this_ sentence, my dear Lizzy?” said +Jane, as she finished it. β€œIs it not clear enough? Does it not expressly +declare that Caroline neither expects nor wishes me to be her sister; +that she is perfectly convinced of her brother’s indifference; and that +if she suspects the nature of my feelings for him she means (most +kindly!) to put me on my guard. Can there be any other opinion on the +subject?” + +β€œYes, there can; for mine is totally different. Will you hear it?” + +β€œMost willingly.” + +β€œYou shall have it in a few words. Miss Bingley sees that her brother is +in love with you and wants him to marry Miss Darcy. She follows him to +town in the hope of keeping him there, and tries to persuade you that he +does not care about you.” + +Jane shook her head. + +β€œIndeed, Jane, you ought to believe me. No one who has ever seen you +together can doubt his affection; Miss Bingley, I am sure, cannot: she +is not such a simpleton. Could she have seen half as much love in Mr. +Darcy for herself, she would have ordered her wedding clothes. But the +case is this:--we are not rich enough or grand enough for them; and she +is the more anxious to get Miss Darcy for her brother, from the notion +that when there has been _one_ inter-marriage, she may have less trouble +in achieving a second; in which there is certainly some ingenuity, and I +dare say it would succeed if Miss de Bourgh were out of the way. But, my +dearest Jane, you cannot seriously imagine that, because Miss Bingley +tells you her brother greatly admires Miss Darcy, he is in the smallest +degree less sensible of _your_ merit than when he took leave of you on +Tuesday; or that it will be in her power to persuade him that, instead +of being in love with you, he is very much in love with her friend.” + +β€œIf we thought alike of Miss Bingley,” replied Jane, β€œyour +representation of all this might make me quite easy. But I know the +foundation is unjust. Caroline is incapable of wilfully deceiving +anyone; and all that I can hope in this case is, that she is deceived +herself.” + +β€œThat is right. You could not have started a more happy idea, since you +will not take comfort in mine: believe her to be deceived, by all means. +You have now done your duty by her, and must fret no longer.” + +β€œBut, my dear sister, can I be happy, even supposing the best, in +accepting a man whose sisters and friends are all wishing him to marry +elsewhere?” + +β€œYou must decide for yourself,” said Elizabeth; β€œand if, upon mature +deliberation, you find that the misery of disobliging his two sisters is +more than equivalent to the happiness of being his wife, I advise you, +by all means, to refuse him.” + +β€œHow can you talk so?” said Jane, faintly smiling; β€œyou must know, that, +though I should be exceedingly grieved at their disapprobation, I could +not hesitate.” + +β€œI did not think you would; and that being the case, I cannot consider +your situation with much compassion.” + +β€œBut if he returns no more this winter, my choice will never be +required. A thousand things may arise in six months.” + +The idea of his returning no more Elizabeth treated with the utmost +contempt. It appeared to her merely the suggestion of Caroline’s +interested wishes; and she could not for a moment suppose that those +wishes, however openly or artfully spoken, could influence a young man +so totally independent of everyone. + +She represented to her sister, as forcibly as possible, what she felt on +the subject, and had soon the pleasure of seeing its happy effect. +Jane’s temper was not desponding; and she was gradually led to hope, +though the diffidence of affection sometimes overcame the hope, that +Bingley would return to Netherfield, and answer every wish of her heart. + +They agreed that Mrs. Bennet should only hear of the departure of the +family, without being alarmed on the score of the gentleman’s conduct; +but even this partial communication gave her a great deal of concern, +and she bewailed it as exceedingly unlucky that the ladies should happen +to go away just as they were all getting so intimate together. After +lamenting it, however, at some length, she had the consolation of +thinking that Mr. Bingley would be soon down again, and soon dining at +Longbourn; and the conclusion of all was the comfortable declaration, +that, though he had been invited only to a family dinner, she would take +care to have two full courses. + + + + +[Illustration] + + + + +CHAPTER XXII. + + +[Illustration] + +The Bennets were engaged to dine with the Lucases; and again, during the +chief of the day, was Miss Lucas so kind as to listen to Mr. Collins. +Elizabeth took an opportunity of thanking her. β€œIt keeps him in good +humour,” said she, β€œand I am more obliged to you than I can express.” + +Charlotte assured her friend of her satisfaction in being useful, and +that it amply repaid her for the little sacrifice of her time. This was +very amiable; but Charlotte’s kindness extended farther than Elizabeth +had any conception of:--its object was nothing less than to secure her +from any return of Mr. Collins’s addresses, by engaging them towards +herself. Such was Miss Lucas’s scheme; and appearances were so +favourable, that when they parted at night, she would have felt almost +sure of success if he had not been to leave Hertfordshire so very soon. +But here she did injustice to the fire and independence of his +character; for it led him to escape out of Longbourn House the next +morning with admirable slyness, and hasten to Lucas Lodge to throw +himself at her feet. He was anxious to avoid the notice of his cousins, +from a conviction that, if they saw him depart, they could not fail to +conjecture his design, and he was not willing to have the attempt known +till its success could be known likewise; for, though feeling almost +secure, and with reason, for Charlotte had been tolerably encouraging, +he was comparatively diffident since the adventure of Wednesday. His +reception, however, was of the most flattering kind. Miss Lucas +perceived him from an upper window as he walked towards the house, and +instantly set out to meet him accidentally in the lane. But little had +she dared to hope that so much love and eloquence awaited her there. + +In as short a time as Mr. Collins’s long speeches would allow, +everything was settled between them to the satisfaction of both; and as +they entered the house, he earnestly entreated her to name the day that +was to make him the happiest of men; and though such a solicitation must +be waived for the present, the lady felt no inclination to trifle with +his happiness. The stupidity with which he was favoured by nature must +guard his courtship from any charm that could make a woman wish for its +continuance; and Miss Lucas, who accepted him solely from the pure and +disinterested desire of an establishment, cared not how soon that +establishment were gained. + +Sir William and Lady Lucas were speedily applied to for their consent; +and it was bestowed with a most joyful alacrity. Mr. Collins’s present +circumstances made it a most eligible match for their daughter, to whom +they could give little fortune; and his prospects of future wealth were +exceedingly fair. Lady Lucas began directly to calculate, with more +interest than the matter had ever + +[Illustration: + + β€œSo much love and eloquence” + +[_Copyright 1894 by George Allen._]] + +excited before, how many years longer Mr. Bennet was likely to live; and +Sir William gave it as his decided opinion, that whenever Mr. Collins +should be in possession of the Longbourn estate, it would be highly +expedient that both he and his wife should make their appearance at St. +James’s. The whole family in short were properly overjoyed on the +occasion. The younger girls formed hopes of _coming out_ a year or two +sooner than they might otherwise have done; and the boys were relieved +from their apprehension of Charlotte’s dying an old maid. Charlotte +herself was tolerably composed. She had gained her point, and had time +to consider of it. Her reflections were in general satisfactory. Mr. +Collins, to be sure, was neither sensible nor agreeable: his society was +irksome, and his attachment to her must be imaginary. But still he would +be her husband. Without thinking highly either of men or of matrimony, +marriage had always been her object: it was the only honourable +provision for well-educated young women of small fortune, and, however +uncertain of giving happiness, must be their pleasantest preservative +from want. This preservative she had now obtained; and at the age of +twenty-seven, without having ever been handsome, she felt all the good +luck of it. The least agreeable circumstance in the business was the +surprise it must occasion to Elizabeth Bennet, whose friendship she +valued beyond that of any other person. Elizabeth would wonder, and +probably would blame her; and though her resolution was not to be +shaken, her feelings must be hurt by such a disapprobation. She resolved +to give her the information herself; and therefore charged Mr. Collins, +when he returned to Longbourn to dinner, to drop no hint of what had +passed before any of the family. A promise of secrecy was of course very +dutifully given, but it could not be kept without difficulty; for the +curiosity excited by his long absence burst forth in such very direct +questions on his return, as required some ingenuity to evade, and he was +at the same time exercising great self-denial, for he was longing to +publish his prosperous love. + +As he was to begin his journey too early on the morrow to see any of +the family, the ceremony of leave-taking was performed when the ladies +moved for the night; and Mrs. Bennet, with great politeness and +cordiality, said how happy they should be to see him at Longbourn again, +whenever his other engagements might allow him to visit them. + +β€œMy dear madam,” he replied, β€œthis invitation is particularly +gratifying, because it is what I have been hoping to receive; and you +may be very certain that I shall avail myself of it as soon as +possible.” + +They were all astonished; and Mr. Bennet, who could by no means wish for +so speedy a return, immediately said,-- + +β€œBut is there not danger of Lady Catherine’s disapprobation here, my +good sir? You had better neglect your relations than run the risk of +offending your patroness.” + +β€œMy dear sir,” replied Mr. Collins, β€œI am particularly obliged to you +for this friendly caution, and you may depend upon my not taking so +material a step without her Ladyship’s concurrence.” + +β€œYou cannot be too much on your guard. Risk anything rather than her +displeasure; and if you find it likely to be raised by your coming to us +again, which I should think exceedingly probable, stay quietly at home, +and be satisfied that _we_ shall take no offence.” + +β€œBelieve me, my dear sir, my gratitude is warmly excited by such +affectionate attention; and, depend upon it, you will speedily receive +from me a letter of thanks for this as well as for every other mark of +your regard during my stay in Hertfordshire. As for my fair cousins, +though my absence may not be long enough to render it necessary, I shall +now take the liberty of wishing them health and happiness, not excepting +my cousin Elizabeth.” + +With proper civilities, the ladies then withdrew; all of them equally +surprised to find that he meditated a quick return. Mrs. Bennet wished +to understand by it that he thought of paying his addresses to one of +her younger girls, and Mary might have been prevailed on to accept him. +She rated his abilities much higher than any of the others: there was a +solidity in his reflections which often struck her; and though by no +means so clever as herself, she thought that, if encouraged to read and +improve himself by such an example as hers, he might become a very +agreeable companion. But on the following morning every hope of this +kind was done away. Miss Lucas called soon after breakfast, and in a +private conference with Elizabeth related the event of the day before. + +The possibility of Mr. Collins’s fancying himself in love with her +friend had once occurred to Elizabeth within the last day or two: but +that Charlotte could encourage him seemed almost as far from possibility +as that she could encourage him herself; and her astonishment was +consequently so great as to overcome at first the bounds of decorum, and +she could not help crying out,-- + +β€œEngaged to Mr. Collins! my dear Charlotte, impossible!” + +The steady countenance which Miss Lucas had commanded in telling her +story gave way to a momentary confusion here on receiving so direct a +reproach; though, as it was no more than she expected, she soon regained +her composure, and calmly replied,-- + +β€œWhy should you be surprised, my dear Eliza? Do you think it incredible +that Mr. Collins should be able to procure any woman’s good opinion, +because he was not so happy as to succeed with you?” + +But Elizabeth had now recollected herself; and, making a strong effort +for it, was able to assure her, with tolerable firmness, that the +prospect of their relationship was highly grateful to her, and that she +wished her all imaginable happiness. + +β€œI see what you are feeling,” replied Charlotte; β€œyou must be surprised, +very much surprised, so lately as Mr. Collins was wishing to marry you. +But when you have had time to think it all over, I hope you will be +satisfied with what I have done. I am not romantic, you know. I never +was. I ask only a comfortable home; and, considering Mr. Collins’s +character, connections, and situation in life, I am convinced that my +chance of happiness with him is as fair as most people can boast on +entering the marriage state.” + +Elizabeth quietly answered β€œundoubtedly;” and, after an awkward pause, +they returned to the rest of the family. Charlotte did not stay much +longer; and Elizabeth was then left to reflect on what she had heard. It +was a long time before she became at all reconciled to the idea of so +unsuitable a match. The strangeness of Mr. Collins’s making two offers +of marriage within three days was nothing in comparison of his being now +accepted. She had always felt that Charlotte’s opinion of matrimony was +not exactly like her own; but she could not have supposed it possible +that, when called into action, she would have sacrificed every better +feeling to worldly advantage. Charlotte, the wife of Mr. Collins, was a +most humiliating picture! And to the pang of a friend disgracing +herself, and sunk in her esteem, was added the distressing conviction +that it was impossible for that friend to be tolerably happy in the lot +she had chosen. + + + + +[Illustration: + + β€œProtested he must be entirely mistaken.” + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER XXIII. + + +[Illustration] + +Elizabeth was sitting with her mother and sisters, reflecting on what +she had heard, and doubting whether she was authorized to mention it, +when Sir William Lucas himself appeared, sent by his daughter to +announce her engagement to the family. With many compliments to them, +and much self-gratulation on the prospect of a connection between the +houses, he unfolded the matter,--to an audience not merely wondering, +but incredulous; for Mrs. Bennet, with more perseverance than +politeness, protested he must be entirely mistaken; and Lydia, always +unguarded and often uncivil, boisterously exclaimed,-- + +β€œGood Lord! Sir William, how can you tell such a story? Do not you know +that Mr. Collins wants to marry Lizzy?” + +Nothing less than the complaisance of a courtier could have borne +without anger such treatment: but Sir William’s good-breeding carried +him through it all; and though he begged leave to be positive as to the +truth of his information, he listened to all their impertinence with the +most forbearing courtesy. + +Elizabeth, feeling it incumbent on her to relieve him from so unpleasant +a situation, now put herself forward to confirm his account, by +mentioning her prior knowledge of it from Charlotte herself; and +endeavoured to put a stop to the exclamations of her mother and sisters, +by the earnestness of her congratulations to Sir William, in which she +was readily joined by Jane, and by making a variety of remarks on the +happiness that might be expected from the match, the excellent character +of Mr. Collins, and the convenient distance of Hunsford from London. + +Mrs. Bennet was, in fact, too much overpowered to say a great deal while +Sir William remained; but no sooner had he left them than her feelings +found a rapid vent. In the first place, she persisted in disbelieving +the whole of the matter; secondly, she was very sure that Mr. Collins +had been taken in; thirdly, she trusted that they would never be happy +together; and, fourthly, that the match might be broken off. Two +inferences, however, were plainly deduced from the whole: one, that +Elizabeth was the real cause of all the mischief; and the other, that +she herself had been barbarously used by them all; and on these two +points she principally dwelt during the rest of the day. Nothing could +console and nothing appease her. Nor did that day wear out her +resentment. A week elapsed before she could see Elizabeth without +scolding her: a month passed away before she could speak to Sir William +or Lady Lucas without being rude; and many months were gone before she +could at all forgive their daughter. + +Mr. Bennet’s emotions were much more tranquil on the occasion, and such +as he did experience he pronounced to be of a most agreeable sort; for +it gratified him, he said, to discover that Charlotte Lucas, whom he had +been used to think tolerably sensible, was as foolish as his wife, and +more foolish than his daughter! + +Jane confessed herself a little surprised at the match: but she said +less of her astonishment than of her earnest desire for their happiness; +nor could Elizabeth persuade her to consider it as improbable. Kitty and +Lydia were far from envying Miss Lucas, for Mr. Collins was only a +clergyman; and it affected them in no other way than as a piece of news +to spread at Meryton. + +Lady Lucas could not be insensible of triumph on being able to retort on +Mrs. Bennet the comfort of having a daughter well married; and she +called at Longbourn rather oftener than usual to say how happy she was, +though Mrs. Bennet’s sour looks and ill-natured remarks might have been +enough to drive happiness away. + +Between Elizabeth and Charlotte there was a restraint which kept them +mutually silent on the subject; and Elizabeth felt persuaded that no +real confidence could ever subsist between them again. Her +disappointment in Charlotte made her turn with fonder regard to her +sister, of whose rectitude and delicacy she was sure her opinion could +never be shaken, and for whose happiness she grew daily more anxious, as +Bingley had now been gone a week, and nothing was heard of his return. + +Jane had sent Caroline an early answer to her letter, and was counting +the days till she might reasonably hope to hear again. The promised +letter of thanks from Mr. Collins arrived on Tuesday, addressed to their +father, and written with all the solemnity of gratitude which a +twelve-month’s abode in the family might have prompted. After +discharging his conscience on that head, he proceeded to inform them, +with many rapturous expressions, of his happiness in having obtained the +affection of their amiable neighbour, Miss Lucas, and then explained +that it was merely with the view of enjoying her society that he had +been so ready to close with their kind wish of seeing him again at +Longbourn, whither he hoped to be able to return on Monday fortnight; +for Lady Catherine, he added, so heartily approved his marriage, that +she wished it to take place as soon as possible, which he trusted would +be an unanswerable argument with his amiable Charlotte to name an early +day for making him the happiest of men. + +Mr. Collins’s return into Hertfordshire was no longer a matter of +pleasure to Mrs. Bennet. On the contrary, she was as much disposed to +complain of it as her husband. It was very strange that he should come +to Longbourn instead of to Lucas Lodge; it was also very inconvenient +and exceedingly troublesome. She hated having visitors in the house +while her health was so indifferent, and lovers were of all people the +most disagreeable. Such were the gentle murmurs of Mrs. Bennet, and they +gave way only to the greater distress of Mr. Bingley’s continued +absence. + +Neither Jane nor Elizabeth were comfortable on this subject. Day after +day passed away without bringing any other tidings of him than the +report which shortly prevailed in Meryton of his coming no more to +Netherfield the whole winter; a report which highly incensed Mrs. +Bennet, and which she never failed to contradict as a most scandalous +falsehood. + +Even Elizabeth began to fear--not that Bingley was indifferent--but that +his sisters would be successful in keeping him away. Unwilling as she +was to admit an idea so destructive to Jane’s happiness, and so +dishonourable to the stability of her lover, she could not prevent its +frequently recurring. The united efforts of his two unfeeling sisters, +and of his overpowering friend, assisted by the attractions of Miss +Darcy and the amusements of London, might be too much, she feared, for +the strength of his attachment. + +As for Jane, _her_ anxiety under this suspense was, of course, more +painful than Elizabeth’s: but whatever she felt she was desirous of +concealing; and between herself and Elizabeth, therefore, the subject +was never alluded to. But as no such delicacy restrained her mother, an +hour seldom passed in which she did not talk of Bingley, express her +impatience for his arrival, or even require Jane to confess that if he +did not come back she should think herself very ill-used. It needed all +Jane’s steady mildness to bear these attacks with tolerable +tranquillity. + +Mr. Collins returned most punctually on the Monday fortnight, but his +reception at Longbourn was not quite so gracious as it had been on his +first introduction. He was too happy, however, to need much attention; +and, luckily for the others, the business of love-making relieved them +from a great deal of his company. The chief of every day was spent by +him at Lucas Lodge, and he sometimes returned to Longbourn only in time +to make an apology for his absence before the family went to bed. + +[Illustration: + + β€œ_Whenever she spoke in a low voice_” +] + +Mrs. Bennet was really in a most pitiable state. The very mention of +anything concerning the match threw her into an agony of ill-humour, and +wherever she went she was sure of hearing it talked of. The sight of +Miss Lucas was odious to her. As her successor in that house, she +regarded her with jealous abhorrence. Whenever Charlotte came to see +them, she concluded her to be anticipating the hour of possession; and +whenever she spoke in a low voice to Mr. Collins, was convinced that +they were talking of the Longbourn estate, and resolving to turn herself +and her daughters out of the house as soon as Mr. Bennet was dead. She +complained bitterly of all this to her husband. + +β€œIndeed, Mr. Bennet,” said she, β€œit is very hard to think that Charlotte +Lucas should ever be mistress of this house, that _I_ should be forced +to make way for _her_, and live to see her take my place in it!” + +β€œMy dear, do not give way to such gloomy thoughts. Let us hope for +better things. Let us flatter ourselves that _I_ may be the survivor.” + +This was not very consoling to Mrs. Bennet; and, therefore, instead of +making any answer, she went on as before. + +β€œI cannot bear to think that they should have all this estate. If it was +not for the entail, I should not mind it.” + +β€œWhat should not you mind?” + +β€œI should not mind anything at all.” + +β€œLet us be thankful that you are preserved from a state of such +insensibility.” + +β€œI never can be thankful, Mr. Bennet, for anything about the entail. How +anyone could have the conscience to entail away an estate from one’s own +daughters I cannot understand; and all for the sake of Mr. Collins, too! +Why should _he_ have it more than anybody else?” + +β€œI leave it to yourself to determine,” said Mr. Bennet. + + + + +[Illustration] + + + + +CHAPTER XXIV. + + +[Illustration] + +Miss Bingley’s letter arrived, and put an end to doubt. The very first +sentence conveyed the assurance of their being all settled in London for +the winter, and concluded with her brother’s regret at not having had +time to pay his respects to his friends in Hertfordshire before he left +the country. + +Hope was over, entirely over; and when Jane could attend to the rest of +the letter, she found little, except the professed affection of the +writer, that could give her any comfort. Miss Darcy’s praise occupied +the chief of it. Her many attractions were again dwelt on; and Caroline +boasted joyfully of their increasing intimacy, and ventured to predict +the accomplishment of the wishes which had been unfolded in her former +letter. She wrote also with great pleasure of her brother’s being an +inmate of Mr. Darcy’s house, and mentioned with raptures some plans of +the latter with regard to new furniture. + +Elizabeth, to whom Jane very soon communicated the chief of all this, +heard it in silent indignation. Her heart was divided between concern +for her sister and resentment against all others. To Caroline’s +assertion of her brother’s being partial to Miss Darcy, she paid no +credit. That he was really fond of Jane, she doubted no more than she +had ever done; and much as she had always been disposed to like him, she +could not think without anger, hardly without contempt, on that easiness +of temper, that want of proper resolution, which now made him the slave +of his designing friends, and led him to sacrifice his own happiness to +the caprice of their inclinations. Had his own happiness, however, been +the only sacrifice, he might have been allowed to sport with it in +whatever manner he thought best; but her sister’s was involved in it, as +she thought he must be sensible himself. It was a subject, in short, on +which reflection would be long indulged, and must be unavailing. She +could think of nothing else; and yet, whether Bingley’s regard had +really died away, or were suppressed by his friends’ interference; +whether he had been aware of Jane’s attachment, or whether it had +escaped his observation; whichever were the case, though her opinion of +him must be materially affected by the difference, her sister’s +situation remained the same, her peace equally wounded. + +A day or two passed before Jane had courage to speak of her feelings to +Elizabeth; but at last, on Mrs. Bennet’s leaving them together, after a +longer irritation than usual about Netherfield and its master, she could +not help saying,-- + +β€œO that my dear mother had more command over herself! she can have no +idea of the pain she gives me by her continual reflections on him. But I +will not repine. It cannot last long. He will be forgot, and we shall +all be as we were before.” + +Elizabeth looked at her sister with incredulous solicitude, but said +nothing. + +β€œYou doubt me,” cried Jane, slightly colouring; β€œindeed, you have no +reason. He may live in my memory as the most amiable man of my +acquaintance but that is all. I have nothing either to hope or fear, and +nothing to reproach him with. Thank God I have not _that_ pain. A little +time, therefore--I shall certainly try to get the better----” + +With a stronger voice she soon added, β€œI have this comfort immediately, +that it has not been more than an error of fancy on my side, and that it +has done no harm to anyone but myself.” + +β€œMy dear Jane,” exclaimed Elizabeth, β€œyou are too good. Your sweetness +and disinterestedness are really angelic; I do not know what to say to +you. I feel as if I had never done you justice, or loved you as you +deserve.” + +Miss Bennet eagerly disclaimed all extraordinary merit, and threw back +the praise on her sister’s warm affection. + +β€œNay,” said Elizabeth, β€œthis is not fair. _You_ wish to think all the +world respectable, and are hurt if I speak ill of anybody. _I_ only want +to think _you_ perfect, and you set yourself against it. Do not be +afraid of my running into any excess, of my encroaching on your +privilege of universal good-will. You need not. There are few people +whom I really love, and still fewer of whom I think well. The more I see +of the world the more am I dissatisfied with it; and every day confirms +my belief of the inconsistency of all human characters, and of the +little dependence that can be placed on the appearance of either merit +or sense. I have met with two instances lately: one I will not mention, +the other is Charlotte’s marriage. It is unaccountable! in every view it +is unaccountable!” + +β€œMy dear Lizzy, do not give way to such feelings as these. They will +ruin your happiness. You do not make allowance enough for difference of +situation and temper. Consider Mr. Collins’s respectability, and +Charlotte’s prudent, steady character. Remember that she is one of a +large family; that as to fortune it is a most eligible match; and be +ready to believe, for everybody’s sake, that she may feel something like +regard and esteem for our cousin.” + +β€œTo oblige you, I would try to believe almost anything, but no one else +could be benefited by such a belief as this; for were I persuaded that +Charlotte had any regard for him, I should only think worse of her +understanding than I now do of her heart. My dear Jane, Mr. Collins is a +conceited, pompous, narrow-minded, silly man: you know he is, as well as +I do; and you must feel, as well as I do, that the woman who marries him +cannot have a proper way of thinking. You shall not defend her, though +it is Charlotte Lucas. You shall not, for the sake of one individual, +change the meaning of principle and integrity, nor endeavour to persuade +yourself or me, that selfishness is prudence, and insensibility of +danger security for happiness.” + +β€œI must think your language too strong in speaking of both,” replied +Jane; β€œand I hope you will be convinced of it, by seeing them happy +together. But enough of this. You alluded to something else. You +mentioned _two_ instances. I cannot misunderstand you, but I entreat +you, dear Lizzy, not to pain me by thinking _that person_ to blame, and +saying your opinion of him is sunk. We must not be so ready to fancy +ourselves intentionally injured. We must not expect a lively young man +to be always so guarded and circumspect. It is very often nothing but +our own vanity that deceives us. Women fancy admiration means more than +it does.” + +β€œAnd men take care that they should.” + +β€œIf it is designedly done, they cannot be justified; but I have no idea +of there being so much design in the world as some persons imagine.” + +β€œI am far from attributing any part of Mr. Bingley’s conduct to design,” +said Elizabeth; β€œbut, without scheming to do wrong, or to make others +unhappy, there may be error and there may be misery. Thoughtlessness, +want of attention to other people’s feelings, and want of resolution, +will do the business.” + +β€œAnd do you impute it to either of those?” + +β€œYes; to the last. But if I go on I shall displease you by saying what I +think of persons you esteem. Stop me, whilst you can.” + +β€œYou persist, then, in supposing his sisters influence him?” + +β€œYes, in conjunction with his friend.” + +β€œI cannot believe it. Why should they try to influence him? They can +only wish his happiness; and if he is attached to me no other woman can +secure it.” + +β€œYour first position is false. They may wish many things besides his +happiness: they may wish his increase of wealth and consequence; they +may wish him to marry a girl who has all the importance of money, great +connections, and pride.” + +β€œBeyond a doubt they do wish him to choose Miss Darcy,” replied Jane; +β€œbut this may be from better feelings than you are supposing. They have +known her much longer than they have known me; no wonder if they love +her better. But, whatever may be their own wishes, it is very unlikely +they should have opposed their brother’s. What sister would think +herself at liberty to do it, unless there were something very +objectionable? If they believed him attached to me they would not try to +part us; if he were so, they could not succeed. By supposing such an +affection, you make everybody acting unnaturally and wrong, and me most +unhappy. Do not distress me by the idea. I am not ashamed of having been +mistaken--or, at least, it is slight, it is nothing in comparison of +what I should feel in thinking ill of him or his sisters. Let me take it +in the best light, in the light in which it may be understood.” + +Elizabeth could not oppose such a wish; and from this time Mr. Bingley’s +name was scarcely ever mentioned between them. + +Mrs. Bennet still continued to wonder and repine at his returning no +more; and though a day seldom passed in which Elizabeth did not account +for it clearly, there seemed little chance of her ever considering it +with less perplexity. Her daughter endeavoured to convince her of what +she did not believe herself, that his attentions to Jane had been merely +the effect of a common and transient liking, which ceased when he saw +her no more; but though the probability of the statement was admitted at +the time, she had the same story to repeat every day. Mrs. Bennet’s best +comfort was, that Mr. Bingley must be down again in the summer. + +Mr. Bennet treated the matter differently. β€œSo, Lizzy,” said he, one +day, β€œyour sister is crossed in love, I find. I congratulate her. Next +to being married, a girl likes to be crossed in love a little now and +then. It is something to think of, and gives her a sort of distinction +among her companions. When is your turn to come? You will hardly bear to +be long outdone by Jane. Now is your time. Here are officers enough at +Meryton to disappoint all the young ladies in the country. Let Wickham +be your man. He is a pleasant fellow, and would jilt you creditably.” + +β€œThank you, sir, but a less agreeable man would satisfy me. We must not +all expect Jane’s good fortune.” + +β€œTrue,” said Mr. Bennet; β€œbut it is a comfort to think that, whatever of +that kind may befall you, you have an affectionate mother who will +always make the most of it.” + +Mr. Wickham’s society was of material service in dispelling the gloom +which the late perverse occurrences had thrown on many of the Longbourn +family. They saw him often, and to his other recommendations was now +added that of general unreserve. The whole of what Elizabeth had already +heard, his claims on Mr. Darcy, and all that he had suffered from him, +was now openly acknowledged and publicly canvassed; and everybody was +pleased to think how much they had always disliked Mr. Darcy before they +had known anything of the matter. + +Miss Bennet was the only creature who could suppose there might be any +extenuating circumstances in the case unknown to the society of +Hertfordshire: her mild and steady candour always pleaded for +allowances, and urged the possibility of mistakes; but by everybody else +Mr. Darcy was condemned as the worst of men. + + + + +[Illustration] + + + + +CHAPTER XXV. + + +[Illustration] + +After a week spent in professions of love and schemes of felicity, Mr. +Collins was called from his amiable Charlotte by the arrival of +Saturday. The pain of separation, however, might be alleviated on his +side by preparations for the reception of his bride, as he had reason to +hope, that shortly after his next return into Hertfordshire, the day +would be fixed that was to make him the happiest of men. He took leave +of his relations at Longbourn with as much solemnity as before; wished +his fair cousins health and happiness again, and promised their father +another letter of thanks. + +On the following Monday, Mrs. Bennet had the pleasure of receiving her +brother and his wife, who came, as usual, to spend the Christmas at +Longbourn. Mr. Gardiner was a sensible, gentlemanlike man, greatly +superior to his sister, as well by nature as education. The Netherfield +ladies would have had difficulty in believing that a man who lived by +trade, and within view of his own warehouses, could have been so +well-bred and agreeable. Mrs. Gardiner, who was several years younger +than Mrs. Bennet and Mrs. Philips, was an amiable, intelligent, elegant +woman, and a great favourite with her Longbourn nieces. Between the two +eldest and herself especially, there subsisted a very particular regard. +They had frequently been staying with her in town. + +The first part of Mrs. Gardiner’s business, on her arrival, was to +distribute her presents and describe the newest fashions. When this was +done, she had a less active part to play. It became her turn to listen. +Mrs. Bennet had many grievances to relate, and much to complain of. They +had all been very ill-used since she last saw her sister. Two of her +girls had been on the point of marriage, and after all there was nothing +in it. + +β€œI do not blame Jane,” she continued, β€œfor Jane would have got Mr. +Bingley if she could. But, Lizzy! Oh, sister! it is very hard to think +that she might have been Mr. Collins’s wife by this time, had not it +been for her own perverseness. He made her an offer in this very room, +and she refused him. The consequence of it is, that Lady Lucas will have +a daughter married before I have, and that Longbourn estate is just as +much entailed as ever. The Lucases are very artful people, indeed, +sister. They are all for what they can get. I am sorry to say it of +them, but so it is. It makes me very nervous and poorly, to be thwarted +so in my own family, and to have neighbours who think of themselves +before anybody else. However, your coming just at this time is the +greatest of comforts, and I am very glad to hear what you tell us of +long sleeves.” + +Mrs. Gardiner, to whom the chief of this news had been given before, in +the course of Jane and Elizabeth’s correspondence with her, made her +sister a slight answer, and, in compassion to her nieces, turned the +conversation. + +When alone with Elizabeth afterwards, she spoke more on the subject. +β€œIt seems likely to have been a desirable match for Jane,” said she. β€œI +am sorry it went off. But these things happen so often! A young man, +such as you describe Mr. Bingley, so easily falls in love with a pretty +girl for a few weeks, and, when accident separates them, so easily +forgets her, that these sort of inconstancies are very frequent.” + +[Illustration: + + β€œOffended two or three young ladies” + +[_Copyright 1894 by George Allen._]] + +β€œAn excellent consolation in its way,” said Elizabeth; β€œbut it will not +do for _us_. We do not suffer by accident. It does not often happen +that the interference of friends will persuade a young man of +independent fortune to think no more of a girl whom he was violently in +love with only a few days before.” + +β€œBut that expression of β€˜violently in love’ is so hackneyed, so +doubtful, so indefinite, that it gives me very little idea. It is as +often applied to feelings which arise only from a half hour’s +acquaintance, as to a real, strong attachment. Pray, how _violent was_ +Mr. Bingley’s love?” + +β€œI never saw a more promising inclination; he was growing quite +inattentive to other people, and wholly engrossed by her. Every time +they met, it was more decided and remarkable. At his own ball he +offended two or three young ladies by not asking them to dance; and I +spoke to him twice myself without receiving an answer. Could there be +finer symptoms? Is not general incivility the very essence of love?” + +β€œOh, yes! of that kind of love which I suppose him to have felt. Poor +Jane! I am sorry for her, because, with her disposition, she may not get +over it immediately. It had better have happened to _you_, Lizzy; you +would have laughed yourself out of it sooner. But do you think she would +be prevailed on to go back with us? Change of scene might be of +service--and perhaps a little relief from home may be as useful as +anything.” + +Elizabeth was exceedingly pleased with this proposal, and felt persuaded +of her sister’s ready acquiescence. + +β€œI hope,” added Mrs. Gardiner, β€œthat no consideration with regard to +this young man will influence her. We live in so different a part of +town, all our connections are so different, and, as you well know, we go +out so little, that it is very improbable they should meet at all, +unless he really comes to see her.” + +β€œAnd _that_ is quite impossible; for he is now in the custody of his +friend, and Mr. Darcy would no more suffer him to call on Jane in such a +part of London! My dear aunt, how could you think of it? Mr. Darcy may, +perhaps, have _heard_ of such a place as Gracechurch Street, but he +would hardly think a month’s ablution enough to cleanse him from its +impurities, were he once to enter it; and, depend upon it, Mr. Bingley +never stirs without him.” + +β€œSo much the better. I hope they will not meet at all. But does not Jane +correspond with his sister? _She_ will not be able to help calling.” + +β€œShe will drop the acquaintance entirely.” + +But, in spite of the certainty in which Elizabeth affected to place this +point, as well as the still more interesting one of Bingley’s being +withheld from seeing Jane, she felt a solicitude on the subject which +convinced her, on examination, that she did not consider it entirely +hopeless. It was possible, and sometimes she thought it probable, that +his affection might be re-animated, and the influence of his friends +successfully combated by the more natural influence of Jane’s +attractions. + +Miss Bennet accepted her aunt’s invitation with pleasure; and the +Bingleys were no otherwise in her thoughts at the same time than as she +hoped, by Caroline’s not living in the same house with her brother, she +might occasionally spend a morning with her, without any danger of +seeing him. + +The Gardiners stayed a week at Longbourn; and what with the Philipses, +the Lucases, and the officers, there was not a day without its +engagement. Mrs. Bennet had so carefully provided for the entertainment +of her brother and sister, that they did not once sit down to a family +dinner. When the engagement was for home, some of the officers always +made part of it, of which officers Mr. Wickham was sure to be one; and +on these occasions Mrs. Gardiner, rendered suspicious by Elizabeth’s +warm commendation of him, narrowly observed them both. Without supposing +them, from what she saw, to be very seriously in love, their preference +of each other was plain enough to make her a little uneasy; and she +resolved to speak to Elizabeth on the subject before she left +Hertfordshire, and represent to her the imprudence of encouraging such +an attachment. + +To Mrs. Gardiner, Wickham had one means of affording pleasure, +unconnected with his general powers. About ten or a dozen years ago, +before her marriage, she had spent a considerable time in that very part +of Derbyshire to which he belonged. They had, therefore, many +acquaintance in common; and, though Wickham had been little there since +the death of Darcy’s father, five years before, it was yet in his power +to give her fresher intelligence of her former friends than she had been +in the way of procuring. + +Mrs. Gardiner had seen Pemberley, and known the late Mr. Darcy by +character perfectly well. Here, consequently, was an inexhaustible +subject of discourse. In comparing her recollection of Pemberley with +the minute description which Wickham could give, and in bestowing her +tribute of praise on the character of its late possessor, she was +delighting both him and herself. On being made acquainted with the +present Mr. Darcy’s treatment of him, she tried to remember something of +that gentleman’s reputed disposition, when quite a lad, which might +agree with it; and was confident, at last, that she recollected having +heard Mr. Fitzwilliam Darcy formerly spoken of as a very proud, +ill-natured boy. + + + + +[Illustration: + + β€œWill you come and see me?” +] + + + + +CHAPTER XXVI. + + +[Illustration] + +Mrs. Gardiner’s caution to Elizabeth was punctually and kindly given on +the first favourable opportunity of speaking to her alone: after +honestly telling her what she thought, she thus went on:-- + +β€œYou are too sensible a girl, Lizzy, to fall in love merely because you +are warned against it; and, therefore, I am not afraid of speaking +openly. Seriously, I would have you be on your guard. Do not involve +yourself, or endeavour to involve him, in an affection which the want of +fortune would make so very imprudent. I have nothing to say against +_him_: he is a most interesting young man; and if he had the fortune he +ought to have, I should think you could not do better. But as it is--you +must not let your fancy run away with you. You have sense, and we all +expect you to use it. Your father would depend on _your_ resolution and +good conduct, I am sure. You must not disappoint your father.” + +β€œMy dear aunt, this is being serious indeed.” + +β€œYes, and I hope to engage you to be serious likewise.” + +β€œWell, then, you need not be under any alarm. I will take care of +myself, and of Mr. Wickham too. He shall not be in love with me, if I +can prevent it.” + +β€œElizabeth, you are not serious now.” + +β€œI beg your pardon. I will try again. At present I am not in love with +Mr. Wickham; no, I certainly am not. But he is, beyond all comparison, +the most agreeable man I ever saw--and if he becomes really attached to +me--I believe it will be better that he should not. I see the imprudence +of it. Oh, _that_ abominable Mr. Darcy! My father’s opinion of me does +me the greatest honour; and I should be miserable to forfeit it. My +father, however, is partial to Mr. Wickham. In short, my dear aunt, I +should be very sorry to be the means of making any of you unhappy; but +since we see, every day, that where there is affection young people are +seldom withheld, by immediate want of fortune, from entering into +engagements with each other, how can I promise to be wiser than so many +of my fellow-creatures, if I am tempted, or how am I even to know that +it would be wiser to resist? All that I can promise you, therefore, is +not to be in a hurry. I will not be in a hurry to believe myself his +first object. When I am in company with him, I will not be wishing. In +short, I will do my best.” + +β€œPerhaps it will be as well if you discourage his coming here so very +often. At least you should not _remind_ your mother of inviting him.” + +β€œAs I did the other day,” said Elizabeth, with a conscious smile; β€œvery +true, it will be wise in me to refrain from _that_. But do not imagine +that he is always here so often. It is on your account that he has been +so frequently invited this week. You know my mother’s ideas as to the +necessity of constant company for her friends. But really, and upon my +honour, I will try to do what I think to be wisest; and now I hope you +are satisfied.” + +Her aunt assured her that she was; and Elizabeth, having thanked her for +the kindness of her hints, they parted,--a wonderful instance of advice +being given on such a point without being resented. + +Mr. Collins returned into Hertfordshire soon after it had been quitted +by the Gardiners and Jane; but, as he took up his abode with the +Lucases, his arrival was no great inconvenience to Mrs. Bennet. His +marriage was now fast approaching; and she was at length so far resigned +as to think it inevitable, and even repeatedly to say, in an ill-natured +tone, that she β€œ_wished_ they might be happy.” Thursday was to be the +wedding-day, and on Wednesday Miss Lucas paid her farewell visit; and +when she rose to take leave, Elizabeth, ashamed of her mother’s +ungracious and reluctant good wishes, and sincerely affected herself, +accompanied her out of the room. As they went down stairs together, +Charlotte said,-- + +β€œI shall depend on hearing from you very often, Eliza.” + +β€œ_That_ you certainly shall.” + +β€œAnd I have another favour to ask. Will you come and see me?” + +β€œWe shall often meet, I hope, in Hertfordshire.” + +β€œI am not likely to leave Kent for some time. Promise me, therefore, to +come to Hunsford.” + +Elizabeth could not refuse, though she foresaw little pleasure in the +visit. + +β€œMy father and Maria are to come to me in March,” added Charlotte, β€œand +I hope you will consent to be of the party. Indeed, Eliza, you will be +as welcome to me as either of them.” + +The wedding took place: the bride and bridegroom set off for Kent from +the church door, and everybody had as much to say or to hear on the +subject as usual. Elizabeth soon heard from her friend, and their +correspondence was as regular and frequent as it ever had been: that it +should be equally unreserved was impossible. Elizabeth could never +address her without feeling that all the comfort of intimacy was over; +and, though determined not to slacken as a correspondent, it was for the +sake of what had been rather than what was. Charlotte’s first letters +were received with a good deal of eagerness: there could not but be +curiosity to know how she would speak of her new home, how she would +like Lady Catherine, and how happy she would dare pronounce herself to +be; though, when the letters were read, Elizabeth felt that Charlotte +expressed herself on every point exactly as she might have foreseen. She +wrote cheerfully, seemed surrounded with comforts, and mentioned nothing +which she could not praise. The house, furniture, neighbourhood, and +roads, were all to her taste, and Lady Catherine’s behaviour was most +friendly and obliging. It was Mr. Collins’s picture of Hunsford and +Rosings rationally softened; and Elizabeth perceived that she must wait +for her own visit there, to know the rest. + +Jane had already written a few lines to her sister, to announce their +safe arrival in London; and when she wrote again, Elizabeth hoped it +would be in her power to say something of the Bingleys. + +Her impatience for this second letter was as well rewarded as impatience +generally is. Jane had been a week in town, without either seeing or +hearing from Caroline. She accounted for it, however, by supposing that +her last letter to her friend from Longbourn had by some accident been +lost. + +β€œMy aunt,” she continued, β€œis going to-morrow into that part of the +town, and I shall take the opportunity of calling in Grosvenor Street.” + +She wrote again when the visit was paid, and she had seen Miss Bingley. +β€œI did not think Caroline in spirits,” were her words, β€œbut she was very +glad to see me, and reproached me for giving her no notice of my coming +to London. I was right, therefore; my last letter had never reached her. +I inquired after their brother, of course. He was well, but so much +engaged with Mr. Darcy that they scarcely ever saw him. I found that +Miss Darcy was expected to dinner: I wish I could see her. My visit was +not long, as Caroline and Mrs. Hurst were going out. I dare say I shall +soon see them here.” + +Elizabeth shook her head over this letter. It convinced her that +accident only could discover to Mr. Bingley her sister’s being in town. + +Four weeks passed away, and Jane saw nothing of him. She endeavoured to +persuade herself that she did not regret it; but she could no longer be +blind to Miss Bingley’s inattention. After waiting at home every morning +for a fortnight, and inventing every evening a fresh excuse for her, the +visitor did at last appear; but the shortness of her stay, and, yet +more, the alteration of her manner, would allow Jane to deceive herself +no longer. The letter which she wrote on this occasion to her sister +will prove what she felt:-- + + β€œMy dearest Lizzy will, I am sure, be incapable of triumphing in + her better judgment, at my expense, when I confess myself to have + been entirely deceived in Miss Bingley’s regard for me. But, my + dear sister, though the event has proved you right, do not think me + obstinate if I still assert that, considering what her behaviour + was, my confidence was as natural as your suspicion. I do not at + all comprehend her reason for wishing to be intimate with me; but, + if the same circumstances were to happen again, I am sure I should + be deceived again. Caroline did not return my visit till yesterday; + and not a note, not a line, did I receive in the meantime. When she + did come, it was very evident that she had no pleasure in it; she + made a slight, formal apology for not calling before, said not a + word of wishing to see me again, and was, in every respect, so + altered a creature, that when she went away I was perfectly + resolved to continue the acquaintance no longer. I pity, though I + cannot help blaming, her. She was very wrong in singling me out as + she did; I can safely say, that every advance to intimacy began on + her side. But I pity her, because she must feel that she has been + acting wrong, and because I am very sure that anxiety for her + brother is the cause of it. I need not explain myself farther; and + though _we_ know this anxiety to be quite needless, yet if she + feels it, it will easily account for her behaviour to me; and so + deservedly dear as he is to his sister, whatever anxiety she may + feel on his behalf is natural and amiable. I cannot but wonder, + however, at her having any such fears now, because if he had at all + cared about me, we must have met long, long ago. He knows of my + being in town, I am certain, from something she said herself; and + yet it would seem, by her manner of talking, as if she wanted to + persuade herself that he is really partial to Miss Darcy. I cannot + understand it. If I were not afraid of judging harshly, I should be + almost tempted to say, that there is a strong appearance of + duplicity in all this. I will endeavour to banish every painful + thought, and think only of what will make me happy, your affection, + and the invariable kindness of my dear uncle and aunt. Let me hear + from you very soon. Miss Bingley said something of his never + returning to Netherfield again, of giving up the house, but not + with any certainty. We had better not mention it. I am extremely + glad that you have such pleasant accounts from our friends at + Hunsford. Pray go to see them, with Sir William and Maria. I am + sure you will be very comfortable there. + +β€œYours, etc.” + +This letter gave Elizabeth some pain; but her spirits returned, as she +considered that Jane would no longer be duped, by the sister at least. +All expectation from the brother was now absolutely over. She would not +even wish for any renewal of his attentions. His character sunk on every +review of it; and, as a punishment for him, as well as a possible +advantage to Jane, she seriously hoped he might really soon marry Mr. +Darcy’s sister, as, by Wickham’s account, she would make him abundantly +regret what he had thrown away. + +Mrs. Gardiner about this time reminded Elizabeth of her promise +concerning that gentleman, and required information; and Elizabeth had +such to send as might rather give contentment to her aunt than to +herself. His apparent partiality had subsided, his attentions were over, +he was the admirer of some one else. Elizabeth was watchful enough to +see it all, but she could see it and write of it without material pain. +Her heart had been but slightly touched, and her vanity was satisfied +with believing that _she_ would have been his only choice, had fortune +permitted it. The sudden acquisition of ten thousand pounds was the most +remarkable charm of the young lady to whom he was now rendering himself +agreeable; but Elizabeth, less clear-sighted perhaps in this case than +in Charlotte’s, did not quarrel with him for his wish of independence. +Nothing, on the contrary, could be more natural; and, while able to +suppose that it cost him a few struggles to relinquish her, she was +ready to allow it a wise and desirable measure for both, and could very +sincerely wish him happy. + +All this was acknowledged to Mrs. Gardiner; and, after relating the +circumstances, she thus went on:--β€œI am now convinced, my dear aunt, +that I have never been much in love; for had I really experienced that +pure and elevating passion, I should at present detest his very name, +and wish him all manner of evil. But my feelings are not only cordial +towards _him_, they are even impartial towards Miss King. I cannot find +out that I hate her at all, or that I am in the least unwilling to think +her a very good sort of girl. There can be no love in all this. My +watchfulness has been effectual; and though I should certainly be a more +interesting object to all my acquaintance, were I distractedly in love +with him, I cannot say that I regret my comparative insignificance. +Importance may sometimes be purchased too dearly. Kitty and Lydia take +his defection much more to heart than I do. They are young in the ways +of the world, and not yet open to the mortifying conviction that +handsome young men must have something to live on as well as the +plain.” + + + + +[Illustration: + + β€œOn the Stairs” +] + + + + +CHAPTER XXVII. + + +[Illustration] + +With no greater events than these in the Longbourn family, and otherwise +diversified by little beyond the walks to Meryton, sometimes dirty and +sometimes cold, did January and February pass away. March was to take +Elizabeth to Hunsford. She had not at first thought very seriously of +going thither; but Charlotte, she soon found, was depending on the +plan, and she gradually learned to consider it herself with greater +pleasure as well as greater certainty. Absence had increased her desire +of seeing Charlotte again, and weakened her disgust of Mr. Collins. +There was novelty in the scheme; and as, with such a mother and such +uncompanionable sisters, home could not be faultless, a little change +was not unwelcome for its own sake. The journey would, moreover, give +her a peep at Jane; and, in short, as the time drew near, she would have +been very sorry for any delay. Everything, however, went on smoothly, +and was finally settled according to Charlotte’s first sketch. She was +to accompany Sir William and his second daughter. The improvement of +spending a night in London was added in time, and the plan became as +perfect as plan could be. + +The only pain was in leaving her father, who would certainly miss her, +and who, when it came to the point, so little liked her going, that he +told her to write to him, and almost promised to answer her letter. + +The farewell between herself and Mr. Wickham was perfectly friendly; on +his side even more. His present pursuit could not make him forget that +Elizabeth had been the first to excite and to deserve his attention, the +first to listen and to pity, the first to be admired; and in his manner +of bidding her adieu, wishing her every enjoyment, reminding her of what +she was to expect in Lady Catherine de Bourgh, and trusting their +opinion of her--their opinion of everybody--would always coincide, there +was a solicitude, an interest, which she felt must ever attach her to +him with a most sincere regard; and she parted from him convinced, that, +whether married or single, he must always be her model of the amiable +and pleasing. + +Her fellow-travellers the next day were not of a kind to make her think +him less agreeable. Sir William Lucas, and his daughter Maria, a +good-humoured girl, but as empty-headed as himself, had nothing to say +that could be worth hearing, and were listened to with about as much +delight as the rattle of the chaise. Elizabeth loved absurdities, but +she had known Sir William’s too long. He could tell her nothing new of +the wonders of his presentation and knighthood; and his civilities were +worn out, like his information. + +It was a journey of only twenty-four miles, and they began it so early +as to be in Gracechurch Street by noon. As they drove to Mr. Gardiner’s +door, Jane was at a drawing-room window watching their arrival: when +they entered the passage, she was there to welcome them, and Elizabeth, +looking earnestly in her face, was pleased to see it healthful and +lovely as ever. On the stairs were a troop of little boys and girls, +whose eagerness for their cousin’s appearance would not allow them to +wait in the drawing-room, and whose shyness, as they had not seen her +for a twelvemonth, prevented their coming lower. All was joy and +kindness. The day passed most pleasantly away; the morning in bustle and +shopping, and the evening at one of the theatres. + +Elizabeth then contrived to sit by her aunt. Their first subject was her +sister; and she was more grieved than astonished to hear, in reply to +her minute inquiries, that though Jane always struggled to support her +spirits, there were periods of dejection. It was reasonable, however, to +hope that they would not continue long. Mrs. Gardiner gave her the +particulars also of Miss Bingley’s visit in Gracechurch Street, and +repeated conversations occurring at different times between Jane and +herself, which proved that the former had, from her heart, given up the +acquaintance. + +Mrs. Gardiner then rallied her niece on Wickham’s desertion, and +complimented her on bearing it so well. + +β€œBut, my dear Elizabeth,” she added, β€œwhat sort of girl is Miss King? I +should be sorry to think our friend mercenary.” + +β€œPray, my dear aunt, what is the difference in matrimonial affairs, +between the mercenary and the prudent motive? Where does discretion end, +and avarice begin? Last Christmas you were afraid of his marrying me, +because it would be imprudent; and now, because he is trying to get a +girl with only ten thousand pounds, you want to find out that he is +mercenary.” + +β€œIf you will only tell me what sort of girl Miss King is, I shall know +what to think.” + +β€œShe is a very good kind of girl, I believe. I know no harm of her.” + +β€œBut he paid her not the smallest attention till her grandfather’s death +made her mistress of this fortune?” + +β€œNo--why should he? If it were not allowable for him to gain _my_ +affections, because I had no money, what occasion could there be for +making love to a girl whom he did not care about, and who was equally +poor?” + +β€œBut there seems indelicacy in directing his attentions towards her so +soon after this event.” + +β€œA man in distressed circumstances has not time for all those elegant +decorums which other people may observe. If _she_ does not object to it, +why should _we_?” + +β€œ_Her_ not objecting does not justify _him_. It only shows her being +deficient in something herself--sense or feeling.” + +β€œWell,” cried Elizabeth, β€œhave it as you choose. _He_ shall be +mercenary, and _she_ shall be foolish.” + +β€œNo, Lizzy, that is what I do _not_ choose. I should be sorry, you know, +to think ill of a young man who has lived so long in Derbyshire.” + +β€œOh, if that is all, I have a very poor opinion of young men who live in +Derbyshire; and their intimate friends who live in Hertfordshire are not +much better. I am sick of them all. Thank heaven! I am going to-morrow +where I shall find a man who has not one agreeable quality, who has +neither manners nor sense to recommend him. Stupid men are the only ones +worth knowing, after all.” + +β€œTake care, Lizzy; that speech savours strongly of disappointment.” + +Before they were separated by the conclusion of the play, she had the +unexpected happiness of an invitation to accompany her uncle and aunt in +a tour of pleasure which they proposed taking in the summer. + +β€œWe have not quite determined how far it shall carry us,” said Mrs. +Gardiner; β€œbut perhaps, to the Lakes.” + +No scheme could have been more agreeable to Elizabeth, and her +acceptance of the invitation was most ready and grateful. β€œMy dear, dear +aunt,” she rapturously cried, β€œwhat delight! what felicity! You give me +fresh life and vigour. Adieu to disappointment and spleen. What are men +to rocks and mountains? Oh, what hours of transport we shall spend! And +when we _do_ return, it shall not be like other travellers, without +being able to give one accurate idea of anything. We _will_ know where +we have gone--we _will_ recollect what we have seen. Lakes, mountains, +and rivers, shall not be jumbled together in our imaginations; nor, when +we attempt to describe any particular scene, will we begin quarrelling +about its relative situation. Let _our_ first effusions be less +insupportable than those of the generality of travellers.” + + + + +[Illustration: + + β€œAt the door” +] + + + + +CHAPTER XXVIII. + + +[Illustration] + +Every object in the next day’s journey was new and interesting to +Elizabeth; and her spirits were in a state of enjoyment; for she had +seen her sister looking so well as to banish all fear for her health, +and the prospect of her northern tour was a constant source of delight. + +When they left the high road for the lane to Hunsford, every eye was in +search of the Parsonage, and every turning expected to bring it in view. +The paling of Rosings park was their boundary on one side. Elizabeth +smiled at the recollection of all that she had heard of its inhabitants. + +At length the Parsonage was discernible. The garden sloping to the +road, the house standing in it, the green pales and the laurel hedge, +everything declared they were arriving. Mr. Collins and Charlotte +appeared at the door, and the carriage stopped at the small gate, which +led by a short gravel walk to the house, amidst the nods and smiles of +the whole party. In a moment they were all out of the chaise, rejoicing +at the sight of each other. Mrs. Collins welcomed her friend with the +liveliest pleasure, and Elizabeth was more and more satisfied with +coming, when she found herself so affectionately received. She saw +instantly that her cousin’s manners were not altered by his marriage: +his formal civility was just what it had been; and he detained her some +minutes at the gate to hear and satisfy his inquiries after all her +family. They were then, with no other delay than his pointing out the +neatness of the entrance, taken into the house; and as soon as they were +in the parlour, he welcomed them a second time, with ostentatious +formality, to his humble abode, and punctually repeated all his wife’s +offers of refreshment. + +Elizabeth was prepared to see him in his glory; and she could not help +fancying that in displaying the good proportion of the room, its aspect, +and its furniture, he addressed himself particularly to her, as if +wishing to make her feel what she had lost in refusing him. But though +everything seemed neat and comfortable, she was not able to gratify him +by any sigh of repentance; and rather looked with wonder at her friend, +that she could have so cheerful an air with such a companion. When Mr. +Collins said anything of which his wife might reasonably be ashamed, +which certainly was not seldom, she involuntarily turned her eye on +Charlotte. Once or twice she could discern a faint blush; but in general +Charlotte wisely did not hear. After sitting long enough to admire +every article of furniture in the room, from the sideboard to the +fender, to give an account of their journey, and of all that had +happened in London, Mr. Collins invited them to take a stroll in the +garden, which was large and well laid out, and to the cultivation of +which he attended himself. To work in his garden was one of his most +respectable pleasures; and Elizabeth admired the command of countenance +with which Charlotte talked of the healthfulness of the exercise, and +owned she encouraged it as much as possible. Here, leading the way +through every walk and cross walk, and scarcely allowing them an +interval to utter the praises he asked for, every view was pointed out +with a minuteness which left beauty entirely behind. He could number the +fields in every direction, and could tell how many trees there were in +the most distant clump. But of all the views which his garden, or which +the country or the kingdom could boast, none were to be compared with +the prospect of Rosings, afforded by an opening in the trees that +bordered the park nearly opposite the front of his house. It was a +handsome modern building, well situated on rising ground. + +From his garden, Mr. Collins would have led them round his two meadows; +but the ladies, not having shoes to encounter the remains of a white +frost, turned back; and while Sir William accompanied him, Charlotte +took her sister and friend over the house, extremely well pleased, +probably, to have the opportunity of showing it without her husband’s +help. It was rather small, but well built and convenient; and everything +was fitted up and arranged with a neatness and consistency, of which +Elizabeth gave Charlotte all the credit. When Mr. Collins could be +forgotten, there was really a great air of comfort throughout, and by +Charlotte’s evident enjoyment of it, Elizabeth supposed he must be often +forgotten. + +She had already learnt that Lady Catherine was still in the country. It +was spoken of again while they were at dinner, when Mr. Collins joining +in, observed,-- + +β€œYes, Miss Elizabeth, you will have the honour of seeing Lady Catherine +de Bourgh on the ensuing Sunday at church, and I need not say you will +be delighted with her. She is all affability and condescension, and I +doubt not but you will be honoured with some portion of her notice when +service is over. I have scarcely any hesitation in saying that she will +include you and my sister Maria in every invitation with which she +honours us during your stay here. Her behaviour to my dear Charlotte is +charming. We dine at Rosings twice every week, and are never allowed to +walk home. Her Ladyship’s carriage is regularly ordered for us. I +_should_ say, one of her Ladyship’s carriages, for she has several.” + +β€œLady Catherine is a very respectable, sensible woman, indeed,” added +Charlotte, β€œand a most attentive neighbour.” + +β€œVery true, my dear, that is exactly what I say. She is the sort of +woman whom one cannot regard with too much deference.” + +The evening was spent chiefly in talking over Hertfordshire news, and +telling again what had been already written; and when it closed, +Elizabeth, in the solitude of her chamber, had to meditate upon +Charlotte’s degree of contentment, to understand her address in guiding, +and composure in bearing with, her husband, and to acknowledge that it +was all done very well. She had also to anticipate how her visit would +pass, the quiet tenour of their usual employments, the vexatious +interruptions of Mr. Collins, and the gaieties of their intercourse +with Rosings. A lively imagination soon settled it all. + +About the middle of the next day, as she was in her room getting ready +for a walk, a sudden noise below seemed to speak the whole house in +confusion; and, after listening a moment, she heard somebody running +upstairs in a violent hurry, and calling loudly after her. She opened +the door, and met Maria in the landing-place, who, breathless with +agitation, cried out,-- + +[Illustration: + + β€œIn Conversation with the ladies” + +[Copyright 1894 by George Allen.]] + +β€œOh, my dear Eliza! pray make haste and come into the dining-room, for +there is such a sight to be seen! I will not tell you what it is. Make +haste, and come down this moment.” + +Elizabeth asked questions in vain; Maria would tell her nothing more; +and down they ran into the dining-room which fronted the lane, in quest +of this wonder; it was two ladies, stopping in a low phaeton at the +garden gate. + +β€œAnd is this all?” cried Elizabeth. β€œI expected at least that the pigs +were got into the garden, and here is nothing but Lady Catherine and her +daughter!” + +β€œLa! my dear,” said Maria, quite shocked at the mistake, β€œit is not Lady +Catherine. The old lady is Mrs. Jenkinson, who lives with them. The +other is Miss De Bourgh. Only look at her. She is quite a little +creature. Who would have thought she could be so thin and small!” + +β€œShe is abominably rude to keep Charlotte out of doors in all this wind. +Why does she not come in?” + +β€œOh, Charlotte says she hardly ever does. It is the greatest of favours +when Miss De Bourgh comes in.” + +β€œI like her appearance,” said Elizabeth, struck with other ideas. β€œShe +looks sickly and cross. Yes, she will do for him very well. She will +make him a very proper wife.” + +Mr. Collins and Charlotte were both standing at the gate in conversation +with the ladies; and Sir William, to Elizabeth’s high diversion, was +stationed in the doorway, in earnest contemplation of the greatness +before him, and constantly bowing whenever Miss De Bourgh looked that +way. + +At length there was nothing more to be said; the ladies drove on, and +the others returned into the house. Mr. Collins no sooner saw the two +girls than he began to congratulate them on their good fortune, which +Charlotte explained by letting them know that the whole party was asked +to dine at Rosings the next day. + + + + +[Illustration: + + β€˜Lady Catherine, said she, you have given me a treasure.’ + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER XXIX. + + +[Illustration] + +Mr. Collins’s triumph, in consequence of this invitation, was complete. +The power of displaying the grandeur of his patroness to his wondering +visitors, and of letting them see her civility towards himself and his +wife, was exactly what he had wished for; and that an opportunity of +doing it should be given so soon was such an instance of Lady +Catherine’s condescension as he knew not how to admire enough. + +β€œI confess,” said he, β€œthat I should not have been at all surprised by +her Ladyship’s asking us on Sunday to drink tea and spend the evening +at Rosings. I rather expected, from my knowledge of her affability, that +it would happen. But who could have foreseen such an attention as this? +Who could have imagined that we should receive an invitation to dine +there (an invitation, moreover, including the whole party) so +immediately after your arrival?” + +β€œI am the less surprised at what has happened,” replied Sir William, +β€œfrom that knowledge of what the manners of the great really are, which +my situation in life has allowed me to acquire. About the court, such +instances of elegant breeding are not uncommon.” + +Scarcely anything was talked of the whole day or next morning but their +visit to Rosings. Mr. Collins was carefully instructing them in what +they were to expect, that the sight of such rooms, so many servants, and +so splendid a dinner, might not wholly overpower them. + +When the ladies were separating for the toilette, he said to +Elizabeth,-- + +β€œDo not make yourself uneasy, my dear cousin, about your apparel. Lady +Catherine is far from requiring that elegance of dress in us which +becomes herself and daughter. I would advise you merely to put on +whatever of your clothes is superior to the rest--there is no occasion +for anything more. Lady Catherine will not think the worse of you for +being simply dressed. She likes to have the distinction of rank +preserved.” + +While they were dressing, he came two or three times to their different +doors, to recommend their being quick, as Lady Catherine very much +objected to be kept waiting for her dinner. Such formidable accounts of +her Ladyship, and her manner of living, quite frightened Maria Lucas, +who had been little used to company; and she looked forward to her +introduction at Rosings with as much apprehension as her father had done +to his presentation at St. James’s. + +As the weather was fine, they had a pleasant walk of about half a mile +across the park. Every park has its beauty and its prospects; and +Elizabeth saw much to be pleased with, though she could not be in such +raptures as Mr. Collins expected the scene to inspire, and was but +slightly affected by his enumeration of the windows in front of the +house, and his relation of what the glazing altogether had originally +cost Sir Lewis de Bourgh. + +When they ascended the steps to the hall, Maria’s alarm was every moment +increasing, and even Sir William did not look perfectly calm. +Elizabeth’s courage did not fail her. She had heard nothing of Lady +Catherine that spoke her awful from any extraordinary talents or +miraculous virtue, and the mere stateliness of money and rank she +thought she could witness without trepidation. + +From the entrance hall, of which Mr. Collins pointed out, with a +rapturous air, the fine proportion and finished ornaments, they followed +the servants through an antechamber to the room where Lady Catherine, +her daughter, and Mrs. Jenkinson were sitting. Her Ladyship, with great +condescension, arose to receive them; and as Mrs. Collins had settled it +with her husband that the office of introduction should be hers, it was +performed in a proper manner, without any of those apologies and thanks +which he would have thought necessary. + +In spite of having been at St. James’s, Sir William was so completely +awed by the grandeur surrounding him, that he had but just courage +enough to make a very low bow, and take his seat without saying a word; +and his daughter, frightened almost out of her senses, sat on the edge +of her chair, not knowing which way to look. Elizabeth found herself +quite equal to the scene, and could observe the three ladies before her +composedly. Lady Catherine was a tall, large woman, with strongly-marked +features, which might once have been handsome. Her air was not +conciliating, nor was her manner of receiving them such as to make her +visitors forget their inferior rank. She was not rendered formidable by +silence: but whatever she said was spoken in so authoritative a tone as +marked her self-importance, and brought Mr. Wickham immediately to +Elizabeth’s mind; and, from the observation of the day altogether, she +believed Lady Catherine to be exactly what he had represented. + +When, after examining the mother, in whose countenance and deportment +she soon found some resemblance of Mr. Darcy, she turned her eyes on the +daughter, she could almost have joined in Maria’s astonishment at her +being so thin and so small. There was neither in figure nor face any +likeness between the ladies. Miss de Bourgh was pale and sickly: her +features, though not plain, were insignificant; and she spoke very +little, except in a low voice, to Mrs. Jenkinson, in whose appearance +there was nothing remarkable, and who was entirely engaged in listening +to what she said, and placing a screen in the proper direction before +her eyes. + +After sitting a few minutes, they were all sent to one of the windows to +admire the view, Mr. Collins attending them to point out its beauties, +and Lady Catherine kindly informing them that it was much better worth +looking at in the summer. + +The dinner was exceedingly handsome, and there were all the servants, +and all the articles of plate which Mr. Collins had promised; and, as he +had likewise foretold, he took his seat at the bottom of the table, by +her Ladyship’s desire, and looked as if he felt that life could furnish +nothing greater. He carved and ate and praised with delighted alacrity; +and every dish was commended first by him, and then by Sir William, who +was now enough recovered to echo whatever his son-in-law said, in a +manner which Elizabeth wondered Lady Catherine could bear. But Lady +Catherine seemed gratified by their excessive admiration, and gave most +gracious smiles, especially when any dish on the table proved a novelty +to them. The party did not supply much conversation. Elizabeth was ready +to speak whenever there was an opening, but she was seated between +Charlotte and Miss de Bourgh--the former of whom was engaged in +listening to Lady Catherine, and the latter said not a word to her all +the dinnertime. Mrs. Jenkinson was chiefly employed in watching how +little Miss de Bourgh ate, pressing her to try some other dish and +fearing she was indisposed. Maria thought speaking out of the question, +and the gentlemen did nothing but eat and admire. + +When the ladies returned to the drawing-room, there was little to be +done but to hear Lady Catherine talk, which she did without any +intermission till coffee came in, delivering her opinion on every +subject in so decisive a manner as proved that she was not used to have +her judgment controverted. She inquired into Charlotte’s domestic +concerns familiarly and minutely, and gave her a great deal of advice as +to the management of them all; told her how everything ought to be +regulated in so small a family as hers, and instructed her as to the +care of her cows and her poultry. Elizabeth found that nothing was +beneath this great lady’s attention which could furnish her with an +occasion for dictating to others. In the intervals of her discourse with +Mrs. Collins, she addressed a variety of questions to Maria and +Elizabeth, but especially to the latter, of whose connections she knew +the least, and who, she observed to Mrs. Collins, was a very genteel, +pretty kind of girl. She asked her at different times how many sisters +she had, whether they were older or younger than herself, whether any of +them were likely to be married, whether they were handsome, where they +had been educated, what carriage her father kept, and what had been her +mother’s maiden name? Elizabeth felt all the impertinence of her +questions, but answered them very composedly. Lady Catherine then +observed,-- + +β€œYour father’s estate is entailed on Mr. Collins, I think? For your +sake,” turning to Charlotte, β€œI am glad of it; but otherwise I see no +occasion for entailing estates from the female line. It was not thought +necessary in Sir Lewis de Bourgh’s family. Do you play and sing, Miss +Bennet?” + +β€œA little.” + +β€œOh then--some time or other we shall be happy to hear you. Our +instrument is a capital one, probably superior to ---- you shall try it +some day. Do your sisters play and sing?” + +β€œOne of them does.” + +β€œWhy did not you all learn? You ought all to have learned. The Miss +Webbs all play, and their father has not so good an income as yours. Do +you draw?” + +β€œNo, not at all.” + +β€œWhat, none of you?” + +β€œNot one.” + +β€œThat is very strange. But I suppose you had no opportunity. Your mother +should have taken you to town every spring for the benefit of masters.” + +β€œMy mother would have no objection, but my father hates London.” + +β€œHas your governess left you?” + +β€œWe never had any governess.” + +β€œNo governess! How was that possible? Five daughters brought up at home +without a governess! I never heard of such a thing. Your mother must +have been quite a slave to your education.” + +Elizabeth could hardly help smiling, as she assured her that had not +been the case. + +β€œThen who taught you? who attended to you? Without a governess, you must +have been neglected.” + +β€œCompared with some families, I believe we were; but such of us as +wished to learn never wanted the means. We were always encouraged to +read, and had all the masters that were necessary. Those who chose to be +idle certainly might.” + +β€œAy, no doubt: but that is what a governess will prevent; and if I had +known your mother, I should have advised her most strenuously to engage +one. I always say that nothing is to be done in education without steady +and regular instruction, and nobody but a governess can give it. It is +wonderful how many families I have been the means of supplying in that +way. I am always glad to get a young person well placed out. Four nieces +of Mrs. Jenkinson are most delightfully situated through my means; and +it was but the other day that I recommended another young person, who +was merely accidentally mentioned to me, and the family are quite +delighted with her. Mrs. Collins, did I tell you of Lady Metcalfe’s +calling yesterday to thank me? She finds Miss Pope a treasure. β€˜Lady +Catherine,’ said she, β€˜you have given me a treasure.’ Are any of your +younger sisters out, Miss Bennet?” + +β€œYes, ma’am, all.” + +β€œAll! What, all five out at once? Very odd! And you only the second. The +younger ones out before the elder are married! Your younger sisters must +be very young?” + +β€œYes, my youngest is not sixteen. Perhaps _she_ is full young to be much +in company. But really, ma’am, I think it would be very hard upon +younger sisters that they should not have their share of society and +amusement, because the elder may not have the means or inclination to +marry early. The last born has as good a right to the pleasures of youth +as the first. And to be kept back on _such_ a motive! I think it would +not be very likely to promote sisterly affection or delicacy of mind.” + +β€œUpon my word,” said her Ladyship, β€œyou give your opinion very decidedly +for so young a person. Pray, what is your age?” + +β€œWith three younger sisters grown up,” replied Elizabeth, smiling, β€œyour +Ladyship can hardly expect me to own it.” + +Lady Catherine seemed quite astonished at not receiving a direct answer; +and Elizabeth suspected herself to be the first creature who had ever +dared to trifle with so much dignified impertinence. + +β€œYou cannot be more than twenty, I am sure,--therefore you need not +conceal your age.” + +β€œI am not one-and-twenty.” + +When the gentlemen had joined them, and tea was over, the card tables +were placed. Lady Catherine, Sir William, and Mr. and Mrs. Collins sat +down to quadrille; and as Miss De Bourgh chose to play at cassino, the +two girls had the honour of assisting Mrs. Jenkinson to make up her +party. Their table was superlatively stupid. Scarcely a syllable was +uttered that did not relate to the game, except when Mrs. Jenkinson +expressed her fears of Miss De Bourgh’s being too hot or too cold, or +having too much or too little light. A great deal more passed at the +other table. Lady Catherine was generally speaking--stating the mistakes +of the three others, or relating some anecdote of herself. Mr. Collins +was employed in agreeing to everything her Ladyship said, thanking her +for every fish he won, and apologizing if he thought he won too many. +Sir William did not say much. He was storing his memory with anecdotes +and noble names. + +When Lady Catherine and her daughter had played as long as they chose, +the tables were broken up, the carriage was offered to Mrs. Collins, +gratefully accepted, and immediately ordered. The party then gathered +round the fire to hear Lady Catherine determine what weather they were +to have on the morrow. From these instructions they were summoned by the +arrival of the coach; and with many speeches of thankfulness on Mr. +Collins’s side, and as many bows on Sir William’s, they departed. As +soon as they had driven from the door, Elizabeth was called on by her +cousin to give her opinion of all that she had seen at Rosings, which, +for Charlotte’s sake, she made more favourable than it really was. But +her commendation, though costing her some trouble, could by no means +satisfy Mr. Collins, and he was very soon obliged to take her Ladyship’s +praise into his own hands. + + + + +[Illustration] + + + + +CHAPTER XXX. + + +[Illustration] + +Sir William stayed only a week at Hunsford; but his visit was long +enough to convince him of his daughter’s being most comfortably settled, +and of her possessing such a husband and such a neighbour as were not +often met with. While Sir William was with them, Mr. Collins devoted his +mornings to driving him out in his gig, and showing him the country: but +when he went away, the whole family returned to their usual employments, +and Elizabeth was thankful to find that they did not see more of her +cousin by the alteration; for the chief of the time between breakfast +and dinner was now passed by him either at work in the garden, or in +reading and writing, and looking out of window in his own book room, +which fronted the road. The room in which the ladies sat was backwards. +Elizabeth at first had rather wondered that Charlotte should not prefer +the dining parlour for common use; it was a better sized room, and had a +pleasanter aspect: but she soon saw that her friend had an excellent +reason for what she did, for Mr. Collins would undoubtedly have been +much less in his own apartment had they sat in one equally lively; and +she gave Charlotte credit for the arrangement. + +From the drawing-room they could distinguish nothing in the lane, and +were indebted to Mr. Collins for the knowledge of what carriages went +along, and how often especially Miss De Bourgh drove by in her phaeton, +which he never failed coming to inform them of, though it happened +almost every day. She not unfrequently stopped at the Parsonage, and had +a few minutes’ conversation with Charlotte, but was scarcely ever +prevailed on to get out. + +Very few days passed in which Mr. Collins did not walk to Rosings, and +not many in which his wife did not think it necessary to go likewise; +and till Elizabeth recollected that there might be other family livings +to be disposed of, she could not understand the sacrifice of so many +hours. Now and then they were honoured with a call from her Ladyship, +and nothing escaped her observation that was passing in the room during +these visits. She examined into their employments, looked at their work, +and advised them to do it differently; found fault with the arrangement +of the furniture, or detected the housemaid in negligence; and if she +accepted any refreshment, seemed to do it only for the sake of finding +out that Mrs. Collins’s joints of meat were too large for her family. + +Elizabeth soon perceived, that though this great lady was not in the +commission of the peace for the county, she was a most active magistrate +in her own parish, the minutest concerns of which were carried to her by +Mr. Collins; and whenever any of the cottagers were disposed to be +quarrelsome, discontented, or too poor, she sallied forth into the +village to settle their differences, silence their complaints, and scold +them into harmony and plenty. + +[Illustration: + + β€œhe never failed to inform them” +] + +The entertainment of dining at Rosings was repeated about twice a week; +and, allowing for the loss of Sir William, and there being only one +card-table in the evening, every such entertainment was the counterpart +of the first. Their other engagements were few, as the style of living +of the neighbourhood in general was beyond the Collinses’ reach. This, +however, was no evil to Elizabeth, and upon the whole she spent her time +comfortably enough: there were half hours of pleasant conversation with +Charlotte, and the weather was so fine for the time of year, that she +had often great enjoyment out of doors. Her favourite walk, and where +she frequently went while the others were calling on Lady Catherine, was +along the open grove which edged that side of the park, where there was +a nice sheltered path, which no one seemed to value but herself, and +where she felt beyond the reach of Lady Catherine’s curiosity. + +In this quiet way the first fortnight of her visit soon passed away. +Easter was approaching, and the week preceding it was to bring an +addition to the family at Rosings, which in so small a circle must be +important. Elizabeth had heard, soon after her arrival, that Mr. Darcy +was expected there in the course of a few weeks; and though there were +not many of her acquaintance whom she did not prefer, his coming would +furnish one comparatively new to look at in their Rosings parties, and +she might be amused in seeing how hopeless Miss Bingley’s designs on him +were, by his behaviour to his cousin, for whom he was evidently destined +by Lady Catherine, who talked of his coming with the greatest +satisfaction, spoke of him in terms of the highest admiration, and +seemed almost angry to find that he had already been frequently seen by +Miss Lucas and herself. + +His arrival was soon known at the Parsonage; for Mr. Collins was walking +the whole morning within view of the lodges opening into Hunsford Lane, +in order to have + +[Illustration: + +β€œThe gentlemen accompanied him.” + +[_Copyright 1894 by George Allen._]] + +the earliest assurance of it; and, after making his bow as the carriage +turned into the park, hurried home with the great intelligence. On the +following morning he hastened to Rosings to pay his respects. There were +two nephews of Lady Catherine to require them, for Mr. Darcy had brought +with him a Colonel Fitzwilliam, the younger son of his uncle, Lord ----; +and, to the great surprise of all the party, when Mr. Collins returned, +the gentlemen accompanied him. Charlotte had seen them from her +husband’s room, crossing the road, and immediately running into the +other, told the girls what an honour they might expect, adding,-- + +β€œI may thank you, Eliza, for this piece of civility. Mr. Darcy would +never have come so soon to wait upon me.” + +Elizabeth had scarcely time to disclaim all right to the compliment +before their approach was announced by the door-bell, and shortly +afterwards the three gentlemen entered the room. Colonel Fitzwilliam, +who led the way, was about thirty, not handsome, but in person and +address most truly the gentleman. Mr. Darcy looked just as he had been +used to look in Hertfordshire, paid his compliments, with his usual +reserve, to Mrs. Collins; and whatever might be his feelings towards her +friend, met her with every appearance of composure. Elizabeth merely +courtesied to him, without saying a word. + +Colonel Fitzwilliam entered into conversation directly, with the +readiness and ease of a well-bred man, and talked very pleasantly; but +his cousin, after having addressed a slight observation on the house and +garden to Mrs. Collins, sat for some time without speaking to anybody. +At length, however, his civility was so far awakened as to inquire of +Elizabeth after the health of her family. She answered him in the usual +way; and, after a moment’s pause, added,-- + +β€œMy eldest sister has been in town these three months. Have you never +happened to see her there?” + +She was perfectly sensible that he never had: but she wished to see +whether he would betray any consciousness of what had passed between the +Bingleys and Jane; and she thought he looked a little confused as he +answered that he had never been so fortunate as to meet Miss Bennet. The +subject was pursued no further, and the gentlemen soon afterwards went +away. + + + + +[Illustration: + +β€œAt Church” +] + + + + +CHAPTER XXXI. + + +[Illustration] + +Colonel Fitzwilliam’s manners were very much admired at the Parsonage, +and the ladies all felt that he must add considerably to the pleasure of +their engagements at Rosings. It was some days, however, before they +received any invitation thither, for while there were visitors in the +house they could not be necessary; and it was not till Easter-day, +almost a week after the gentlemen’s arrival, that they were honoured by +such an attention, and then they were merely asked on leaving church to +come there in the evening. For the last week they had seen very little +of either Lady Catherine or her daughter. Colonel Fitzwilliam had called +at the Parsonage more than once during the time, but Mr. Darcy they had +only seen at church. + +The invitation was accepted, of course, and at a proper hour they joined +the party in Lady Catherine’s drawing-room. Her Ladyship received them +civilly, but it was plain that their company was by no means so +acceptable as when she could get nobody else; and she was, in fact, +almost engrossed by her nephews, speaking to them, especially to Darcy, +much more than to any other person in the room. + +Colonel Fitzwilliam seemed really glad to see them: anything was a +welcome relief to him at Rosings; and Mrs. Collins’s pretty friend had, +moreover, caught his fancy very much. He now seated himself by her, and +talked so agreeably of Kent and Hertfordshire, of travelling and staying +at home, of new books and music, that Elizabeth had never been half so +well entertained in that room before; and they conversed with so much +spirit and flow as to draw the attention of Lady Catherine herself, as +well as of Mr. Darcy. _His_ eyes had been soon and repeatedly turned +towards them with a look of curiosity; and that her Ladyship, after a +while, shared the feeling, was more openly acknowledged, for she did not +scruple to call out,-- + +β€œWhat is that you are saying, Fitzwilliam? What is it you are talking +of? What are you telling Miss Bennet? Let me hear what it is.” + +β€œWe were talking of music, madam,” said he, when no longer able to avoid +a reply. + +β€œOf music! Then pray speak aloud. It is of all subjects my delight. I +must have my share in the conversation, if you are speaking of music. +There are few people in England, I suppose, who have more true +enjoyment of music than myself, or a better natural taste. If I had ever +learnt, I should have been a great proficient. And so would Anne, if her +health had allowed her to apply. I am confident that she would have +performed delightfully. How does Georgiana get on, Darcy?” + +Mr. Darcy spoke with affectionate praise of his sister’s proficiency. + +β€œI am very glad to hear such a good account of her,” said Lady +Catherine; β€œand pray tell her from me, that she cannot expect to excel, +if she does not practise a great deal.” + +β€œI assure you, madam,” he replied, β€œthat she does not need such advice. +She practises very constantly.” + +β€œSo much the better. It cannot be done too much; and when I next write +to her, I shall charge her not to neglect it on any account. I often +tell young ladies, that no excellence in music is to be acquired without +constant practice. I have told Miss Bennet several times, that she will +never play really well, unless she practises more; and though Mrs. +Collins has no instrument, she is very welcome, as I have often told +her, to come to Rosings every day, and play on the pianoforte in Mrs. +Jenkinson’s room. She would be in nobody’s way, you know, in that part +of the house.” + +Mr. Darcy looked a little ashamed of his aunt’s ill-breeding, and made +no answer. + +When coffee was over, Colonel Fitzwilliam reminded Elizabeth of having +promised to play to him; and she sat down directly to the instrument. He +drew a chair near her. Lady Catherine listened to half a song, and then +talked, as before, to her other nephew; till the latter walked away from +her, and moving with his usual deliberation towards the pianoforte, +stationed himself so as to command a full view of the fair performer’s +countenance. Elizabeth saw what he was doing, and at the first +convenient pause turned to him with an arch smile, and said,-- + +β€œYou mean to frighten me, Mr. Darcy, by coming in all this state to hear +me. But I will not be alarmed, though your sister _does_ play so well. +There is a stubbornness about me that never can bear to be frightened at +the will of others. My courage always rises with every attempt to +intimidate me.” + +β€œI shall not say that you are mistaken,” he replied, β€œbecause you could +not really believe me to entertain any design of alarming you; and I +have had the pleasure of your acquaintance long enough to know, that you +find great enjoyment in occasionally professing opinions which, in fact, +are not your own.” + +Elizabeth laughed heartily at this picture of herself, and said to +Colonel Fitzwilliam, β€œYour cousin will give you a very pretty notion of +me, and teach you not to believe a word I say. I am particularly unlucky +in meeting with a person so well able to expose my real character, in a +part of the world where I had hoped to pass myself off with some degree +of credit. Indeed, Mr. Darcy, it is very ungenerous in you to mention +all that you knew to my disadvantage in Hertfordshire--and, give me +leave to say, very impolitic too--for it is provoking me to retaliate, +and such things may come out as will shock your relations to hear.” + +β€œI am not afraid of you,” said he, smilingly. + +β€œPray let me hear what you have to accuse him of,” cried Colonel +Fitzwilliam. β€œI should like to know how he behaves among strangers.” + +β€œYou shall hear, then--but prepare for something very dreadful. The +first time of my ever seeing him in Hertfordshire, you must know, was at +a ball--and at this ball, what do you think he did? He danced only four +dances! I am sorry to pain you, but so it was. He danced only four +dances, though gentlemen were scarce; and, to my certain knowledge, more +than one young lady was sitting down in want of a partner. Mr. Darcy, +you cannot deny the fact.” + +β€œI had not at that time the honour of knowing any lady in the assembly +beyond my own party.” + +β€œTrue; and nobody can ever be introduced in a ball-room. Well, Colonel +Fitzwilliam, what do I play next? My fingers wait your orders.” + +β€œPerhaps,” said Darcy, β€œI should have judged better had I sought an +introduction, but I am ill-qualified to recommend myself to strangers.” + +β€œShall we ask your cousin the reason of this?” said Elizabeth, still +addressing Colonel Fitzwilliam. β€œShall we ask him why a man of sense and +education, and who has lived in the world, is ill-qualified to recommend +himself to strangers?” + +β€œI can answer your question,” said Fitzwilliam, β€œwithout applying to +him. It is because he will not give himself the trouble.” + +β€œI certainly have not the talent which some people possess,” said Darcy, +β€œof conversing easily with those I have never seen before. I cannot +catch their tone of conversation, or appear interested in their +concerns, as I often see done.” + +β€œMy fingers,” said Elizabeth, β€œdo not move over this instrument in the +masterly manner which I see so many women’s do. They have not the same +force or rapidity, and do not produce the same expression. But then I +have always supposed it to be my own fault--because I would not take +the trouble of practising. It is not that I do not believe _my_ fingers +as capable as any other woman’s of superior execution.” + +Darcy smiled and said, β€œYou are perfectly right. You have employed your +time much better. No one admitted to the privilege of hearing you can +think anything wanting. We neither of us perform to strangers.” + +Here they were interrupted by Lady Catherine, who called out to know +what they were talking of. Elizabeth immediately began playing again. +Lady Catherine approached, and, after listening for a few minutes, said +to Darcy,-- + +β€œMiss Bennet would not play at all amiss if she practised more, and +could have the advantage of a London master. She has a very good notion +of fingering, though her taste is not equal to Anne’s. Anne would have +been a delightful performer, had her health allowed her to learn.” + +Elizabeth looked at Darcy, to see how cordially he assented to his +cousin’s praise: but neither at that moment nor at any other could she +discern any symptom of love; and from the whole of his behaviour to Miss +De Bourgh she derived this comfort for Miss Bingley, that he might have +been just as likely to marry _her_, had she been his relation. + +Lady Catherine continued her remarks on Elizabeth’s performance, mixing +with them many instructions on execution and taste. Elizabeth received +them with all the forbearance of civility; and at the request of the +gentlemen remained at the instrument till her Ladyship’s carriage was +ready to take them all home. + + + + +[Illustration] + + + + +CHAPTER XXXII. + + +[Illustration] + +Elizabeth was sitting by herself the next morning, and writing to Jane, +while Mrs. Collins and Maria were gone on business into the village, +when she was startled by a ring at the door, the certain signal of a +visitor. As she had heard no carriage, she thought it not unlikely to be +Lady Catherine; and under that apprehension was putting away her +half-finished letter, that she might escape all impertinent questions, +when the door opened, and to her very great surprise Mr. Darcy, and Mr. +Darcy only, entered the room. + +He seemed astonished too on finding her alone, and apologized for his +intrusion, by letting her know that he had understood all the ladies to +be within. + +They then sat down, and when her inquiries after Rosings were made, +seemed in danger of sinking into total silence. It was absolutely +necessary, therefore, to think of something; and in this emergency +recollecting _when_ she had seen him last in Hertfordshire, and feeling +curious to know what he would say on the subject of their hasty +departure, she observed,-- + +β€œHow very suddenly you all quitted Netherfield last November, Mr. Darcy! +It must have been a most agreeable surprise to Mr. Bingley to see you +all after him so soon; for, if I recollect right, he went but the day +before. He and his sisters were well, I hope, when you left London?” + +β€œPerfectly so, I thank you.” + +She found that she was to receive no other answer; and, after a short +pause, added,-- + +β€œI think I have understood that Mr. Bingley has not much idea of ever +returning to Netherfield again?” + +β€œI have never heard him say so; but it is probable that he may spend +very little of his time there in future. He has many friends, and he is +at a time of life when friends and engagements are continually +increasing.” + +β€œIf he means to be but little at Netherfield, it would be better for the +neighbourhood that he should give up the place entirely, for then we +might possibly get a settled family there. But, perhaps, Mr. Bingley did +not take the house so much for the convenience of the neighbourhood as +for his own, and we must expect him to keep or quit it on the same +principle.” + +β€œI should not be surprised,” said Darcy, β€œif he were to give it up as +soon as any eligible purchase offers.” + +Elizabeth made no answer. She was afraid of talking longer of his +friend; and, having nothing else to say, was now determined to leave the +trouble of finding a subject to him. + +He took the hint and soon began with, β€œThis seems a very comfortable +house. Lady Catherine, I believe, did a great deal to it when Mr. +Collins first came to Hunsford.” + +β€œI believe she did--and I am sure she could not have bestowed her +kindness on a more grateful object.” + +β€œMr. Collins appears very fortunate in his choice of a wife.” + +β€œYes, indeed; his friends may well rejoice in his having met with one of +the very few sensible women who would have accepted him, or have made +him happy if they had. My friend has an excellent understanding--though +I am not certain that I consider her marrying Mr. Collins as the wisest +thing she ever did. She seems perfectly happy, however; and, in a +prudential light, it is certainly a very good match for her.” + +β€œIt must be very agreeable to her to be settled within so easy a +distance of her own family and friends.” + +β€œAn easy distance do you call it? It is nearly fifty miles.” + +β€œAnd what is fifty miles of good road? Little more than half a day’s +journey. Yes, I call it a very easy distance.” + +β€œI should never have considered the distance as one of the _advantages_ +of the match,” cried Elizabeth. β€œI should never have said Mrs. Collins +was settled _near_ her family.” + +β€œIt is a proof of your own attachment to Hertfordshire. Anything beyond +the very neighbourhood of Longbourn, I suppose, would appear far.” + +As he spoke there was a sort of smile, which Elizabeth fancied she +understood; he must be supposing her to be thinking of Jane and +Netherfield, and she blushed as she answered,-- + +β€œI do not mean to say that a woman may not be settled too near her +family. The far and the near must be relative, and depend on many +varying circumstances. Where there is fortune to make the expense of +travelling unimportant, distance becomes no evil. But that is not the +case _here_. Mr. and Mrs. Collins have a comfortable income, but not +such a one as will allow of frequent journeys--and I am persuaded my +friend would not call herself _near_ her family under less than _half_ +the present distance.” + +Mr. Darcy drew his chair a little towards her, and said, β€œ_You_ cannot +have a right to such very strong local attachment. _You_ cannot have +been always at Longbourn.” + +Elizabeth looked surprised. The gentleman experienced some change of +feeling; he drew back his chair, took a newspaper from the table, and, +glancing over it, said, in a colder voice,-- + +β€œAre you pleased with Kent?” + +A short dialogue on the subject of the country ensued, on either side +calm and concise--and soon put an end to by the entrance of Charlotte +and her sister, just returned from their walk. The _tΓͺte-Γ -tΓͺte_ +surprised them. Mr. Darcy related the mistake which had occasioned his +intruding on Miss Bennet, and, after sitting a few minutes longer, +without saying much to anybody, went away. + +[Illustration: β€œAccompanied by their aunt” + +[_Copyright 1894 by George Allen._]] + +β€œWhat can be the meaning of this?” said Charlotte, as soon as he was +gone. β€œMy dear Eliza, he must be in love with you, or he would never +have called on us in this familiar way.” + +But when Elizabeth told of his silence, it did not seem very likely, +even to Charlotte’s wishes, to be the case; and, after various +conjectures, they could at last only suppose his visit to proceed from +the difficulty of finding anything to do, which was the more probable +from the time of year. All field sports were over. Within doors there +was Lady Catherine, books, and a billiard table, but gentlemen cannot be +always within doors; and in the nearness of the Parsonage, or the +pleasantness of the walk to it, or of the people who lived in it, the +two cousins found a temptation from this period of walking thither +almost every day. They called at various times of the morning, sometimes +separately, sometimes together, and now and then accompanied by their +aunt. It was plain to them all that Colonel Fitzwilliam came because he +had pleasure in their society, a persuasion which of course recommended +him still more; and Elizabeth was reminded by her own satisfaction in +being with him, as well as by his evident admiration, of her former +favourite, George Wickham; and though, in comparing them, she saw there +was less captivating softness in Colonel Fitzwilliam’s manners, she +believed he might have the best informed mind. + +But why Mr. Darcy came so often to the Parsonage it was more difficult +to understand. It could not be for society, as he frequently sat there +ten minutes together without opening his lips; and when he did speak, it +seemed the effect of necessity rather than of choice--a sacrifice to +propriety, not a pleasure to himself. He seldom appeared really +animated. Mrs. Collins knew not what to make of him. Colonel +Fitzwilliam’s occasionally laughing at his stupidity proved that he was +generally different, which her own knowledge of him could not have told +her; and as she would have liked to believe this change the effect of +love, and the object of that love her friend Eliza, she set herself +seriously to work to find it out: she watched him whenever they were at +Rosings, and whenever he came to Hunsford; but without much success. He +certainly looked at her friend a great deal, but the expression of that +look was disputable. It was an earnest, steadfast gaze, but she often +doubted whether there were much admiration in it, and sometimes it +seemed nothing but absence of mind. + +She had once or twice suggested to Elizabeth the possibility of his +being partial to her, but Elizabeth always laughed at the idea; and Mrs. +Collins did not think it right to press the subject, from the danger of +raising expectations which might only end in disappointment; for in her +opinion it admitted not of a doubt, that all her friend’s dislike would +vanish, if she could suppose him to be in her power. + +In her kind schemes for Elizabeth, she sometimes planned her marrying +Colonel Fitzwilliam. He was, beyond comparison, the pleasantest man: he +certainly admired her, and his situation in life was most eligible; but, +to counterbalance these advantages, Mr. Darcy had considerable patronage +in the church, and his cousin could have none at all. + + + + +[Illustration: β€œOn looking up”] + + + + +CHAPTER XXXIII. + + +[Illustration] + +More than once did Elizabeth, in her ramble within the park, +unexpectedly meet Mr. Darcy. She felt all the perverseness of the +mischance that should bring him where no one else was brought; and, to +prevent its ever happening again, took care to inform him, at first, +that it was a favourite haunt of hers. How it could occur a second time, +therefore, was very odd! Yet it did, and even the third. It seemed like +wilful ill-nature, or a voluntary penance; for on these occasions it was +not merely a few formal inquiries and an awkward pause and then away, +but he actually thought it necessary to turn back and walk with her. He +never said a great deal, nor did she give herself the trouble of talking +or of listening much; but it struck her in the course of their third +encounter that he was asking some odd unconnected questions--about her +pleasure in being at Hunsford, her love of solitary walks, and her +opinion of Mr. and Mrs. Collins’s happiness; and that in speaking of +Rosings, and her not perfectly understanding the house, he seemed to +expect that whenever she came into Kent again she would be staying +_there_ too. His words seemed to imply it. Could he have Colonel +Fitzwilliam in his thoughts? She supposed, if he meant anything, he must +mean an allusion to what might arise in that quarter. It distressed her +a little, and she was quite glad to find herself at the gate in the +pales opposite the Parsonage. + +She was engaged one day, as she walked, in re-perusing Jane’s last +letter, and dwelling on some passages which proved that Jane had not +written in spirits, when, instead of being again surprised by Mr. Darcy, +she saw, on looking up, that Colonel Fitzwilliam was meeting her. +Putting away the letter immediately, and forcing a smile, she said,-- + +β€œI did not know before that you ever walked this way.” + +β€œI have been making the tour of the park,” he replied, β€œas I generally +do every year, and intended to close it with a call at the Parsonage. +Are you going much farther?” + +β€œNo, I should have turned in a moment.” + +And accordingly she did turn, and they walked towards the Parsonage +together. + +β€œDo you certainly leave Kent on Saturday?” said she. + +β€œYes--if Darcy does not put it off again. But I am at his disposal. He +arranges the business just as he pleases.” + +β€œAnd if not able to please himself in the arrangement, he has at least +great pleasure in the power of choice. I do not know anybody who seems +more to enjoy the power of doing what he likes than Mr. Darcy.” + +β€œHe likes to have his own way very well,” replied Colonel Fitzwilliam. +β€œBut so we all do. It is only that he has better means of having it than +many others, because he is rich, and many others are poor. I speak +feelingly. A younger son, you know, must be inured to self-denial and +dependence.” + +β€œIn my opinion, the younger son of an earl can know very little of +either. Now, seriously, what have you ever known of self-denial and +dependence? When have you been prevented by want of money from going +wherever you chose or procuring anything you had a fancy for?” + +β€œThese are home questions--and perhaps I cannot say that I have +experienced many hardships of that nature. But in matters of greater +weight, I may suffer from the want of money. Younger sons cannot marry +where they like.” + +β€œUnless where they like women of fortune, which I think they very often +do.” + +β€œOur habits of expense make us too dependent, and there are not many in +my rank of life who can afford to marry without some attention to +money.” + +β€œIs this,” thought Elizabeth, β€œmeant for me?” and she coloured at the +idea; but, recovering herself, said in a lively tone, β€œAnd pray, what is +the usual price of an earl’s younger son? Unless the elder brother is +very sickly, I suppose you would not ask above fifty thousand pounds.” + +He answered her in the same style, and the subject dropped. To interrupt +a silence which might make him fancy her affected with what had passed, +she soon afterwards said,-- + +β€œI imagine your cousin brought you down with him chiefly for the sake of +having somebody at his disposal. I wonder he does not marry, to secure a +lasting convenience of that kind. But, perhaps, his sister does as well +for the present; and, as she is under his sole care, he may do what he +likes with her.” + +β€œNo,” said Colonel Fitzwilliam, β€œthat is an advantage which he must +divide with me. I am joined with him in the guardianship of Miss Darcy.” + +β€œAre you, indeed? And pray what sort of a guardian do you make? Does +your charge give you much trouble? Young ladies of her age are sometimes +a little difficult to manage; and if she has the true Darcy spirit, she +may like to have her own way.” + +As she spoke, she observed him looking at her earnestly; and the manner +in which he immediately asked her why she supposed Miss Darcy likely to +give them any uneasiness, convinced her that she had somehow or other +got pretty near the truth. She directly replied,-- + +β€œYou need not be frightened. I never heard any harm of her; and I dare +say she is one of the most tractable creatures in the world. She is a +very great favourite with some ladies of my acquaintance, Mrs. Hurst and +Miss Bingley. I think I have heard you say that you know them.” + +β€œI know them a little. Their brother is a pleasant, gentlemanlike +man--he is a great friend of Darcy’s.” + +β€œOh yes,” said Elizabeth drily--β€œMr. Darcy is uncommonly kind to Mr. +Bingley, and takes a prodigious deal of care of him.” + +β€œCare of him! Yes, I really believe Darcy _does_ take care of him in +those points where he most wants care. From something that he told me +in our journey hither, I have reason to think Bingley very much indebted +to him. But I ought to beg his pardon, for I have no right to suppose +that Bingley was the person meant. It was all conjecture.” + +β€œWhat is it you mean?” + +β€œIt is a circumstance which Darcy of course could not wish to be +generally known, because if it were to get round to the lady’s family it +would be an unpleasant thing.” + +β€œYou may depend upon my not mentioning it.” + +β€œAnd remember that I have not much reason for supposing it to be +Bingley. What he told me was merely this: that he congratulated himself +on having lately saved a friend from the inconveniences of a most +imprudent marriage, but without mentioning names or any other +particulars; and I only suspected it to be Bingley from believing him +the kind of young man to get into a scrape of that sort, and from +knowing them to have been together the whole of last summer.” + +β€œDid Mr. Darcy give you his reasons for this interference?” + +β€œI understood that there were some very strong objections against the +lady.” + +β€œAnd what arts did he use to separate them?” + +β€œHe did not talk to me of his own arts,” said Fitzwilliam, smiling. β€œHe +only told me what I have now told you.” + +Elizabeth made no answer, and walked on, her heart swelling with +indignation. After watching her a little, Fitzwilliam asked her why she +was so thoughtful. + +β€œI am thinking of what you have been telling me,” said she. β€œYour +cousin’s conduct does not suit my feelings. Why was he to be the +judge?” + +β€œYou are rather disposed to call his interference officious?” + +β€œI do not see what right Mr. Darcy had to decide on the propriety of his +friend’s inclination; or why, upon his own judgment alone, he was to +determine and direct in what manner that friend was to be happy. But,” +she continued, recollecting herself, β€œas we know none of the +particulars, it is not fair to condemn him. It is not to be supposed +that there was much affection in the case.” + +β€œThat is not an unnatural surmise,” said Fitzwilliam; β€œbut it is +lessening the honour of my cousin’s triumph very sadly.” + +This was spoken jestingly, but it appeared to her so just a picture of +Mr. Darcy, that she would not trust herself with an answer; and, +therefore, abruptly changing the conversation, talked on indifferent +matters till they reached the Parsonage. There, shut into her own room, +as soon as their visitor left them, she could think without interruption +of all that she had heard. It was not to be supposed that any other +people could be meant than those with whom she was connected. There +could not exist in the world _two_ men over whom Mr. Darcy could have +such boundless influence. That he had been concerned in the measures +taken to separate Mr. Bingley and Jane, she had never doubted; but she +had always attributed to Miss Bingley the principal design and +arrangement of them. If his own vanity, however, did not mislead him, +_he_ was the cause--his pride and caprice were the cause--of all that +Jane had suffered, and still continued to suffer. He had ruined for a +while every hope of happiness for the most affectionate, generous heart +in the world; and no one could say how lasting an evil he might have +inflicted. + +β€œThere were some very strong objections against the lady,” were Colonel +Fitzwilliam’s words; and these strong objections probably were, her +having one uncle who was a country attorney, and another who was in +business in London. + +β€œTo Jane herself,” she exclaimed, β€œthere could be no possibility of +objection,--all loveliness and goodness as she is! Her understanding +excellent, her mind improved, and her manners captivating. Neither could +anything be urged against my father, who, though with some +peculiarities, has abilities which Mr. Darcy himself need not disdain, +and respectability which he will probably never reach.” When she thought +of her mother, indeed, her confidence gave way a little; but she would +not allow that any objections _there_ had material weight with Mr. +Darcy, whose pride, she was convinced, would receive a deeper wound from +the want of importance in his friend’s connections than from their want +of sense; and she was quite decided, at last, that he had been partly +governed by this worst kind of pride, and partly by the wish of +retaining Mr. Bingley for his sister. + +The agitation and tears which the subject occasioned brought on a +headache; and it grew so much worse towards the evening that, added to +her unwillingness to see Mr. Darcy, it determined her not to attend her +cousins to Rosings, where they were engaged to drink tea. Mrs. Collins, +seeing that she was really unwell, did not press her to go, and as much +as possible prevented her husband from pressing her; but Mr. Collins +could not conceal his apprehension of Lady Catherine’s being rather +displeased by her staying at home. + + + + +[Illustration] + + + + +CHAPTER XXXIV. + + +[Illustration] + +When they were gone, Elizabeth, as if intending to exasperate herself as +much as possible against Mr. Darcy, chose for her employment the +examination of all the letters which Jane had written to her since her +being in Kent. They contained no actual complaint, nor was there any +revival of past occurrences, or any communication of present suffering. +But in all, and in almost every line of each, there was a want of that +cheerfulness which had been used to characterize her style, and which, +proceeding from the serenity of a mind at ease with itself, and kindly +disposed towards everyone, had been scarcely ever clouded. Elizabeth +noticed every sentence conveying the idea of uneasiness, with an +attention which it had hardly received on the first perusal. Mr. Darcy’s +shameful boast of what misery he had been able to inflict gave her a +keener sense of her sister’s sufferings. It was some consolation to +think that his visit to Rosings was to end on the day after the next, +and a still greater that in less than a fortnight she should herself be +with Jane again, and enabled to contribute to the recovery of her +spirits, by all that affection could do. + +She could not think of Darcy’s leaving Kent without remembering that his +cousin was to go with him; but Colonel Fitzwilliam had made it clear +that he had no intentions at all, and, agreeable as he was, she did not +mean to be unhappy about him. + +While settling this point, she was suddenly roused by the sound of the +door-bell; and her spirits were a little fluttered by the idea of its +being Colonel Fitzwilliam himself, who had once before called late in +the evening, and might now come to inquire particularly after her. But +this idea was soon banished, and her spirits were very differently +affected, when, to her utter amazement, she saw Mr. Darcy walk into the +room. In a hurried manner he immediately began an inquiry after her +health, imputing his visit to a wish of hearing that she were better. +She answered him with cold civility. He sat down for a few moments, and +then getting up walked about the room. Elizabeth was surprised, but +said not a word. After a silence of several minutes, he came towards her +in an agitated manner, and thus began:-- + +β€œIn vain have I struggled. It will not do. My feelings will not be +repressed. You must allow me to tell you how ardently I admire and love +you.” + +Elizabeth’s astonishment was beyond expression. She stared, coloured, +doubted, and was silent. This he considered sufficient encouragement, +and the avowal of all that he felt and had long felt for her immediately +followed. He spoke well; but there were feelings besides those of the +heart to be detailed, and he was not more eloquent on the subject of +tenderness than of pride. His sense of her inferiority, of its being a +degradation, of the family obstacles which judgment had always opposed +to inclination, were dwelt on with a warmth which seemed due to the +consequence he was wounding, but was very unlikely to recommend his +suit. + +In spite of her deeply-rooted dislike, she could not be insensible to +the compliment of such a man’s affection, and though her intentions did +not vary for an instant, she was at first sorry for the pain he was to +receive; till roused to resentment by his subsequent language, she lost +all compassion in anger. She tried, however, to compose herself to +answer him with patience, when he should have done. He concluded with +representing to her the strength of that attachment which in spite of +all his endeavours he had found impossible to conquer; and with +expressing his hope that it would now be rewarded by her acceptance of +his hand. As he said this she could easily see that he had no doubt of a +favourable answer. He _spoke_ of apprehension and anxiety, but his +countenance expressed real security. Such a circumstance could only +exasperate farther; and when he ceased the colour rose into her cheeks +and she said,-- + +β€œIn such cases as this, it is, I believe, the established mode to +express a sense of obligation for the sentiments avowed, however +unequally they may be returned. It is natural that obligation should be +felt, and if I could _feel_ gratitude, I would now thank you. But I +cannot--I have never desired your good opinion, and you have certainly +bestowed it most unwillingly. I am sorry to have occasioned pain to +anyone. It has been most unconsciously done, however, and I hope will be +of short duration. The feelings which you tell me have long prevented +the acknowledgment of your regard can have little difficulty in +overcoming it after this explanation.” + +Mr. Darcy, who was leaning against the mantel-piece with his eyes fixed +on her face, seemed to catch her words with no less resentment than +surprise. His complexion became pale with anger, and the disturbance of +his mind was visible in every feature. He was struggling for the +appearance of composure, and would not open his lips till he believed +himself to have attained it. The pause was to Elizabeth’s feelings +dreadful. At length, in a voice of forced calmness, he said,-- + +β€œAnd this is all the reply which I am to have the honour of expecting! I +might, perhaps, wish to be informed why, with so little _endeavour_ at +civility, I am thus rejected. But it is of small importance.” + +β€œI might as well inquire,” replied she, β€œwhy, with so evident a design +of offending and insulting me, you chose to tell me that you liked me +against your will, against your reason, and even against your character? +Was not this some excuse for incivility, if I _was_ uncivil? But I have +other provocations. You know I have. Had not my own feelings decided +against you, had they been indifferent, or had they even been +favourable, do you think that any consideration would tempt me to accept +the man who has been the means of ruining, perhaps for ever, the +happiness of a most beloved sister?” + +As she pronounced these words, Mr. Darcy changed colour; but the emotion +was short, and he listened without attempting to interrupt her while she +continued,-- + +β€œI have every reason in the world to think ill of you. No motive can +excuse the unjust and ungenerous part you acted _there_. You dare not, +you cannot deny that you have been the principal, if not the only means +of dividing them from each other, of exposing one to the censure of the +world for caprice and instability, the other to its derision for +disappointed hopes, and involving them both in misery of the acutest +kind.” + +She paused, and saw with no slight indignation that he was listening +with an air which proved him wholly unmoved by any feeling of remorse. +He even looked at her with a smile of affected incredulity. + +β€œCan you deny that you have done it?” she repeated. + +With assumed tranquillity he then replied, β€œI have no wish of denying +that I did everything in my power to separate my friend from your +sister, or that I rejoice in my success. Towards _him_ I have been +kinder than towards myself.” + +Elizabeth disdained the appearance of noticing this civil reflection, +but its meaning did not escape, nor was it likely to conciliate her. + +β€œBut it is not merely this affair,” she continued, β€œon which my dislike +is founded. Long before it had taken place, my opinion of you was +decided. Your character was unfolded in the recital which I received +many months ago from Mr. Wickham. On this subject, what can you have to +say? In what imaginary act of friendship can you here defend yourself? +or under what misrepresentation can you here impose upon others?” + +β€œYou take an eager interest in that gentleman’s concerns,” said Darcy, +in a less tranquil tone, and with a heightened colour. + +β€œWho that knows what his misfortunes have been can help feeling an +interest in him?” + +β€œHis misfortunes!” repeated Darcy, contemptuously,--β€œyes, his +misfortunes have been great indeed.” + +β€œAnd of your infliction,” cried Elizabeth, with energy; β€œYou have +reduced him to his present state of poverty--comparative poverty. You +have withheld the advantages which you must know to have been designed +for him. You have deprived the best years of his life of that +independence which was no less his due than his desert. You have done +all this! and yet you can treat the mention of his misfortunes with +contempt and ridicule.” + +β€œAnd this,” cried Darcy, as he walked with quick steps across the room, +β€œis your opinion of me! This is the estimation in which you hold me! I +thank you for explaining it so fully. My faults, according to this +calculation, are heavy indeed! But, perhaps,” added he, stopping in his +walk, and turning towards her, β€œthese offences might have been +overlooked, had not your pride been hurt by my honest confession of the +scruples that had long prevented my forming any serious design. These +bitter accusations might have been suppressed, had I, with greater +policy, concealed my struggles, and flattered you into the belief of my +being impelled by unqualified, unalloyed inclination; by reason, by +reflection, by everything. But disguise of every sort is my abhorrence. +Nor am I ashamed of the feelings I related. They were natural and just. +Could you expect me to rejoice in the inferiority of your +connections?--to congratulate myself on the hope of relations whose +condition in life is so decidedly beneath my own?” + +Elizabeth felt herself growing more angry every moment; yet she tried to +the utmost to speak with composure when she said,-- + +β€œYou are mistaken, Mr. Darcy, if you suppose that the mode of your +declaration affected me in any other way than as it spared me the +concern which I might have felt in refusing you, had you behaved in a +more gentlemanlike manner.” + +She saw him start at this; but he said nothing, and she continued,-- + +β€œYou could not have made me the offer of your hand in any possible way +that would have tempted me to accept it.” + +Again his astonishment was obvious; and he looked at her with an +expression of mingled incredulity and mortification. She went on,-- + +β€œFrom the very beginning, from the first moment, I may almost say, of my +acquaintance with you, your manners impressing me with the fullest +belief of your arrogance, your conceit, and your selfish disdain of the +feelings of others, were such as to form that groundwork of +disapprobation, on which succeeding events have built so immovable a +dislike; and I had not known you a month before I felt that you were the +last man in the world whom I could ever be prevailed on to marry.” + +β€œYou have said quite enough, madam. I perfectly comprehend your +feelings, and have now only to be ashamed of what my own have been. +Forgive me for having taken up so much of your time, and accept my best +wishes for your health and happiness.” + +And with these words he hastily left the room, and Elizabeth heard him +the next moment open the front door and quit the house. The tumult of +her mind was now painfully great. She knew not how to support herself, +and, from actual weakness, sat down and cried for half an hour. Her +astonishment, as she reflected on what had passed, was increased by +every review of it. That she should receive an offer of marriage from +Mr. Darcy! that he should have been in love with her for so many months! +so much in love as to wish to marry her in spite of all the objections +which had made him prevent his friend’s marrying her sister, and which +must appear at least with equal force in his own case, was almost +incredible! it was gratifying to have inspired unconsciously so strong +an affection. But his pride, his abominable pride, his shameless avowal +of what he had done with respect to Jane, his unpardonable assurance in +acknowledging, though he could not justify it, and the unfeeling manner +which he had mentioned Mr. Wickham, his cruelty towards whom he had not +attempted to deny, soon overcame the pity which the consideration of his +attachment had for a moment excited. + +She continued in very agitating reflections till the sound of Lady +Catherine’s carriage made her feel how unequal she was to encounter +Charlotte’s observation, and hurried her away to her room. + + + + +[Illustration: + +β€œHearing herself called” +] + + + + +CHAPTER XXXV. + + +[Illustration] + +Elizabeth awoke the next morning to the same thoughts and meditations +which had at length closed her eyes. She could not yet recover from the +surprise of what had happened: it was impossible to think of anything +else; and, totally indisposed for employment, she resolved soon after +breakfast to indulge herself in air and exercise. She was proceeding +directly to her favourite walk, when the recollection of Mr. Darcy’s +sometimes coming there stopped her, and instead of entering the park, +she turned up the lane which led her farther from the turnpike road. The +park paling was still the boundary on one side, and she soon passed one +of the gates into the ground. + +After walking two or three times along that part of the lane, she was +tempted, by the pleasantness of the morning, to stop at the gates and +look into the park. The five weeks which she had now passed in Kent had +made a great difference in the country, and every day was adding to the +verdure of the early trees. She was on the point of continuing her +walk, when she caught a glimpse of a gentleman within the sort of grove +which edged the park: he was moving that way; and fearful of its being +Mr. Darcy, she was directly retreating. But the person who advanced was +now near enough to see her, and stepping forward with eagerness, +pronounced her name. She had turned away; but on hearing herself called, +though in a voice which proved it to be Mr. Darcy, she moved again +towards the gate. He had by that time reached it also; and, holding out +a letter, which she instinctively took, said, with a look of haughty +composure, β€œI have been walking in the grove some time, in the hope of +meeting you. Will you do me the honour of reading that letter?” and +then, with a slight bow, turned again into the plantation, and was soon +out of sight. + +With no expectation of pleasure, but with the strongest curiosity, +Elizabeth opened the letter, and to her still increasing wonder, +perceived an envelope containing two sheets of letter paper, written +quite through, in a very close hand. The envelope itself was likewise +full. Pursuing her way along the lane, she then began it. It was dated +from Rosings, at eight o’clock in the morning, and was as follows:-- + +β€œBe not alarmed, madam, on receiving this letter, by the apprehension of +its containing any repetition of those sentiments, or renewal of those +offers, which were last night so disgusting to you. I write without any +intention of paining you, or humbling myself, by dwelling on wishes, +which, for the happiness of both, cannot be too soon forgotten; and the +effort which the formation and the perusal of this letter must occasion, +should have been spared, had not my character required it to be written +and read. You must, therefore, pardon the freedom with which I demand +your attention; your feelings, I know, will bestow it unwillingly, but I +demand it of your justice. + +β€œTwo offences of a very different nature, and by no means of equal +magnitude, you last night laid to my charge. The first mentioned was, +that, regardless of the sentiments of either, I had detached Mr. Bingley +from your sister,--and the other, that I had, in defiance of various +claims, in defiance of honour and humanity, ruined the immediate +prosperity and blasted the prospects of Mr. Wickham. Wilfully and +wantonly to have thrown off the companion of my youth, the acknowledged +favourite of my father, a young man who had scarcely any other +dependence than on our patronage, and who had been brought up to expect +its exertion, would be a depravity, to which the separation of two young +persons whose affection could be the growth of only a few weeks, could +bear no comparison. But from the severity of that blame which was last +night so liberally bestowed, respecting each circumstance, I shall hope +to be in future secured, when the following account of my actions and +their motives has been read. If, in the explanation of them which is due +to myself, I am under the necessity of relating feelings which may be +offensive to yours, I can only say that I am sorry. The necessity must +be obeyed, and further apology would be absurd. I had not been long in +Hertfordshire before I saw, in common with others, that Bingley +preferred your elder sister to any other young woman in the country. But +it was not till the evening of the dance at Netherfield that I had any +apprehension of his feeling a serious attachment. I had often seen him +in love before. At that ball, while I had the honour of dancing with +you, I was first made acquainted, by Sir William Lucas’s accidental +information, that Bingley’s attentions to your sister had given rise to +a general expectation of their marriage. He spoke of it as a certain +event, of which the time alone could be undecided. From that moment I +observed my friend’s behaviour attentively; and I could then perceive +that his partiality for Miss Bennet was beyond what I had ever witnessed +in him. Your sister I also watched. Her look and manners were open, +cheerful, and engaging as ever, but without any symptom of peculiar +regard; and I remained convinced, from the evening’s scrutiny, that +though she received his attentions with pleasure, she did not invite +them by any participation of sentiment. If _you_ have not been mistaken +here, _I_ must have been in an error. Your superior knowledge of your +sister must make the latter probable. If it be so, if I have been misled +by such error to inflict pain on her, your resentment has not been +unreasonable. But I shall not scruple to assert, that the serenity of +your sister’s countenance and air was such as might have given the most +acute observer a conviction that, however amiable her temper, her heart +was not likely to be easily touched. That I was desirous of believing +her indifferent is certain; but I will venture to say that my +investigations and decisions are not usually influenced by my hopes or +fears. I did not believe her to be indifferent because I wished it; I +believed it on impartial conviction, as truly as I wished it in reason. +My objections to the marriage were not merely those which I last night +acknowledged to have required the utmost force of passion to put aside +in my own case; the want of connection could not be so great an evil to +my friend as to me. But there were other causes of repugnance; causes +which, though still existing, and existing to an equal degree in both +instances, I had myself endeavoured to forget, because they were not +immediately before me. These causes must be stated, though briefly. The +situation of your mother’s family, though objectionable, was nothing in +comparison of that total want of propriety so frequently, so almost +uniformly betrayed by herself, by your three younger sisters, and +occasionally even by your father:--pardon me,--it pains me to offend +you. But amidst your concern for the defects of your nearest relations, +and your displeasure at this representation of them, let it give you +consolation to consider that to have conducted yourselves so as to avoid +any share of the like censure is praise no less generally bestowed on +you and your eldest sister than it is honourable to the sense and +disposition of both. I will only say, farther, that from what passed +that evening my opinion of all parties was confirmed, and every +inducement heightened, which could have led me before to preserve my +friend from what I esteemed a most unhappy connection. He left +Netherfield for London on the day following, as you, I am certain, +remember, with the design of soon returning. The part which I acted is +now to be explained. His sisters’ uneasiness had been equally excited +with my own: our coincidence of feeling was soon discovered; and, alike +sensible that no time was to be lost in detaching their brother, we +shortly resolved on joining him directly in London. We accordingly +went--and there I readily engaged in the office of pointing out to my +friend the certain evils of such a choice. I described and enforced them +earnestly. But however this remonstrance might have staggered or delayed +his determination, I do not suppose that it would ultimately have +prevented the marriage, had it not been seconded by the assurance, which +I hesitated not in giving, of your sister’s indifference. He had before +believed her to return his affection with sincere, if not with equal, +regard. But Bingley has great natural modesty, with a stronger +dependence on my judgment than on his own. To convince him, therefore, +that he had deceived himself was no very difficult point. To persuade +him against returning into Hertfordshire, when that conviction had been +given, was scarcely the work of a moment. I cannot blame myself for +having done thus much. There is but one part of my conduct, in the whole +affair, on which I do not reflect with satisfaction; it is that I +condescended to adopt the measures of art so far as to conceal from him +your sister’s being in town. I knew it myself, as it was known to Miss +Bingley; but her brother is even yet ignorant of it. That they might +have met without ill consequence is, perhaps, probable; but his regard +did not appear to me enough extinguished for him to see her without some +danger. Perhaps this concealment, this disguise, was beneath me. It is +done, however, and it was done for the best. On this subject I have +nothing more to say, no other apology to offer. If I have wounded your +sister’s feelings, it was unknowingly done; and though the motives which +governed me may to you very naturally appear insufficient, I have not +yet learnt to condemn them.--With respect to that other, more weighty +accusation, of having injured Mr. Wickham, I can only refute it by +laying before you the whole of his connection with my family. Of what he +has _particularly_ accused me I am ignorant; but of the truth of what I +shall relate I can summon more than one witness of undoubted veracity. +Mr. Wickham is the son of a very respectable man, who had for many years +the management of all the Pemberley estates, and whose good conduct in +the discharge of his trust naturally inclined my father to be of service +to him; and on George Wickham, who was his godson, his kindness was +therefore liberally bestowed. My father supported him at school, and +afterwards at Cambridge; most important assistance, as his own father, +always poor from the extravagance of his wife, would have been unable to +give him a gentleman’s education. My father was not only fond of this +young man’s society, whose manners were always engaging, he had also the +highest opinion of him, and hoping the church would be his profession, +intended to provide for him in it. As for myself, it is many, many years +since I first began to think of him in a very different manner. The +vicious propensities, the want of principle, which he was careful to +guard from the knowledge of his best friend, could not escape the +observation of a young man of nearly the same age with himself, and who +had opportunities of seeing him in unguarded moments, which Mr. Darcy +could not have. Here again I shall give you pain--to what degree you +only can tell. But whatever may be the sentiments which Mr. Wickham has +created, a suspicion of their nature shall not prevent me from unfolding +his real character. It adds even another motive. My excellent father +died about five years ago; and his attachment to Mr. Wickham was to the +last so steady, that in his will he particularly recommended it to me to +promote his advancement in the best manner that his profession might +allow, and if he took orders, desired that a valuable family living +might be his as soon as it became vacant. There was also a legacy of +one thousand pounds. His own father did not long survive mine; and +within half a year from these events Mr. Wickham wrote to inform me +that, having finally resolved against taking orders, he hoped I should +not think it unreasonable for him to expect some more immediate +pecuniary advantage, in lieu of the preferment, by which he could not be +benefited. He had some intention, he added, of studying the law, and I +must be aware that the interest of one thousand pounds would be a very +insufficient support therein. I rather wished than believed him to be +sincere; but, at any rate, was perfectly ready to accede to his +proposal. I knew that Mr. Wickham ought not to be a clergyman. The +business was therefore soon settled. He resigned all claim to assistance +in the church, were it possible that he could ever be in a situation to +receive it, and accepted in return three thousand pounds. All connection +between us seemed now dissolved. I thought too ill of him to invite him +to Pemberley, or admit his society in town. In town, I believe, he +chiefly lived, but his studying the law was a mere pretence; and being +now free from all restraint, his life was a life of idleness and +dissipation. For about three years I heard little of him; but on the +decease of the incumbent of the living which had been designed for him, +he applied to me again by letter for the presentation. His +circumstances, he assured me, and I had no difficulty in believing it, +were exceedingly bad. He had found the law a most unprofitable study, +and was now absolutely resolved on being ordained, if I would present +him to the living in question--of which he trusted there could be little +doubt, as he was well assured that I had no other person to provide for, +and I could not have forgotten my revered father’s intentions. You will +hardly blame me for refusing to comply with this entreaty, or for +resisting every repetition of it. His resentment was in proportion to +the distress of his circumstances--and he was doubtless as violent in +his abuse of me to others as in his reproaches to myself. After this +period, every appearance of acquaintance was dropped. How he lived, I +know not. But last summer he was again most painfully obtruded on my +notice. I must now mention a circumstance which I would wish to forget +myself, and which no obligation less than the present should induce me +to unfold to any human being. Having said thus much, I feel no doubt of +your secrecy. My sister, who is more than ten years my junior, was left +to the guardianship of my mother’s nephew, Colonel Fitzwilliam, and +myself. About a year ago, she was taken from school, and an +establishment formed for her in London; and last summer she went with +the lady who presided over it to Ramsgate; and thither also went Mr. +Wickham, undoubtedly by design; for there proved to have been a prior +acquaintance between him and Mrs. Younge, in whose character we were +most unhappily deceived; and by her connivance and aid he so far +recommended himself to Georgiana, whose affectionate heart retained a +strong impression of his kindness to her as a child, that she was +persuaded to believe herself in love and to consent to an elopement. She +was then but fifteen, which must be her excuse; and after stating her +imprudence, I am happy to add, that I owed the knowledge of it to +herself. I joined them unexpectedly a day or two before the intended +elopement; and then Georgiana, unable to support the idea of grieving +and offending a brother whom she almost looked up to as a father, +acknowledged the whole to me. You may imagine what I felt and how I +acted. Regard for my sister’s credit and feelings prevented any public +exposure; but I wrote to Mr. Wickham, who left the place immediately, +and Mrs. Younge was of course removed from her charge. Mr. Wickham’s +chief object was unquestionably my sister’s fortune, which is thirty +thousand pounds; but I cannot help supposing that the hope of revenging +himself on me was a strong inducement. His revenge would have been +complete indeed. This, madam, is a faithful narrative of every event in +which we have been concerned together; and if you do not absolutely +reject it as false, you will, I hope, acquit me henceforth of cruelty +towards Mr. Wickham. I know not in what manner, under what form of +falsehood, he has imposed on you; but his success is not perhaps to be +wondered at, ignorant as you previously were of everything concerning +either. Detection could not be in your power, and suspicion certainly +not in your inclination. You may possibly wonder why all this was not +told you last night. But I was not then master enough of myself to know +what could or ought to be revealed. For the truth of everything here +related, I can appeal more particularly to the testimony of Colonel +Fitzwilliam, who, from our near relationship and constant intimacy, and +still more as one of the executors of my father’s will, has been +unavoidably acquainted with every particular of these transactions. If +your abhorrence of _me_ should make _my_ assertions valueless, you +cannot be prevented by the same cause from confiding in my cousin; and +that there may be the possibility of consulting him, I shall endeavour +to find some opportunity of putting this letter in your hands in the +course of the morning. I will only add, God bless you. + +β€œFITZWILLIAM DARCY.” + + + + +[Illustration] + + + + +CHAPTER XXXVI. + + +[Illustration] + +Elizabeth, when Mr. Darcy gave her the letter, did not expect it to +contain a renewal of his offers, she had formed no expectation at all of +its contents. But such as they were, it may be well supposed how eagerly +she went through them, and what a contrariety of emotion they excited. +Her feelings as she read were scarcely to be defined. With amazement did +she first understand that he believed any apology to be in his power; +and steadfastly was she persuaded, that he could have no explanation to +give, which a just sense of shame would not conceal. With a strong +prejudice against everything he might say, she began his account of +what had happened at Netherfield. She read with an eagerness which +hardly left her power of comprehension; and from impatience of knowing +what the next sentence might bring, was incapable of attending to the +sense of the one before her eyes. His belief of her sister’s +insensibility she instantly resolved to be false; and his account of the +real, the worst objections to the match, made her too angry to have any +wish of doing him justice. He expressed no regret for what he had done +which satisfied her; his style was not penitent, but haughty. It was all +pride and insolence. + +But when this subject was succeeded by his account of Mr. Wickham--when +she read, with somewhat clearer attention, a relation of events which, +if true, must overthrow every cherished opinion of his worth, and which +bore so alarming an affinity to his own history of himself--her feelings +were yet more acutely painful and more difficult of definition. +Astonishment, apprehension, and even horror, oppressed her. She wished +to discredit it entirely, repeatedly exclaiming, β€œThis must be false! +This cannot be! This must be the grossest falsehood!”--and when she had +gone through the whole letter, though scarcely knowing anything of the +last page or two, put it hastily away, protesting that she would not +regard it, that she would never look in it again. + +In this perturbed state of mind, with thoughts that could rest on +nothing, she walked on; but it would not do: in half a minute the letter +was unfolded again; and collecting herself as well as she could, she +again began the mortifying perusal of all that related to Wickham, and +commanded herself so far as to examine the meaning of every sentence. +The account of his connection with the Pemberley family was exactly +what he had related himself; and the kindness of the late Mr. Darcy, +though she had not before known its extent, agreed equally well with his +own words. So far each recital confirmed the other; but when she came to +the will, the difference was great. What Wickham had said of the living +was fresh in her memory; and as she recalled his very words, it was +impossible not to feel that there was gross duplicity on one side or the +other, and, for a few moments, she flattered herself that her wishes did +not err. But when she read and re-read, with the closest attention, the +particulars immediately following of Wickham’s resigning all pretensions +to the living, of his receiving in lieu so considerable a sum as three +thousand pounds, again was she forced to hesitate. She put down the +letter, weighed every circumstance with what she meant to be +impartiality--deliberated on the probability of each statement--but with +little success. On both sides it was only assertion. Again she read on. +But every line proved more clearly that the affair, which she had +believed it impossible that any contrivance could so represent as to +render Mr. Darcy’s conduct in it less than infamous, was capable of a +turn which must make him entirely blameless throughout the whole. + +The extravagance and general profligacy which he scrupled not to lay to +Mr. Wickham’s charge exceedingly shocked her; the more so, as she could +bring no proof of its injustice. She had never heard of him before his +entrance into the ----shire militia, in which he had engaged at the +persuasion of the young man, who, on meeting him accidentally in town, +had there renewed a slight acquaintance. Of his former way of life, +nothing had been known in Hertfordshire but what he told + +[Illustration: + + β€œMeeting accidentally in Town” + +[_Copyright 1894 by George Allen._]] + +himself. As to his real character, had information been in her power, +she had never felt a wish of inquiring. His countenance, voice, and +manner, had established him at once in the possession of every virtue. +She tried to recollect some instance of goodness, some distinguished +trait of integrity or benevolence, that might rescue him from the +attacks of Mr. Darcy; or at least, by the predominance of virtue, atone +for those casual errors, under which she would endeavour to class what +Mr. Darcy had described as the idleness and vice of many years’ +continuance. But no such recollection befriended her. She could see him +instantly before her, in every charm of air and address, but she could +remember no more substantial good than the general approbation of the +neighbourhood, and the regard which his social powers had gained him in +the mess. After pausing on this point a considerable while, she once +more continued to read. But, alas! the story which followed, of his +designs on Miss Darcy, received some confirmation from what had passed +between Colonel Fitzwilliam and herself only the morning before; and at +last she was referred for the truth of every particular to Colonel +Fitzwilliam himself--from whom she had previously received the +information of his near concern in all his cousin’s affairs and whose +character she had no reason to question. At one time she had almost +resolved on applying to him, but the idea was checked by the awkwardness +of the application, and at length wholly banished by the conviction that +Mr. Darcy would never have hazarded such a proposal, if he had not been +well assured of his cousin’s corroboration. + +She perfectly remembered everything that had passed in conversation +between Wickham and herself in their first evening at Mr. Philips’s. +Many of his expressions were still fresh in her memory. She was _now_ +struck with the impropriety of such communications to a stranger, and +wondered it had escaped her before. She saw the indelicacy of putting +himself forward as he had done, and the inconsistency of his professions +with his conduct. She remembered that he had boasted of having no fear +of seeing Mr. Darcy--that Mr. Darcy might leave the country, but that +_he_ should stand his ground; yet he had avoided the Netherfield ball +the very next week. She remembered, also, that till the Netherfield +family had quitted the country, he had told his story to no one but +herself; but that after their removal, it had been everywhere discussed; +that he had then no reserves, no scruples in sinking Mr. Darcy’s +character, though he had assured her that respect for the father would +always prevent his exposing the son. + +How differently did everything now appear in which he was concerned! His +attentions to Miss King were now the consequence of views solely and +hatefully mercenary; and the mediocrity of her fortune proved no longer +the moderation of his wishes, but his eagerness to grasp at anything. +His behaviour to herself could now have had no tolerable motive: he had +either been deceived with regard to her fortune, or had been gratifying +his vanity by encouraging the preference which she believed she had most +incautiously shown. Every lingering struggle in his favour grew fainter +and fainter; and in further justification of Mr. Darcy, she could not +but allow that Mr. Bingley, when questioned by Jane, had long ago +asserted his blamelessness in the affair;--that, proud and repulsive as +were his manners, she had never, in the whole course of their +acquaintance--an acquaintance which had latterly brought them much +together, and given her a sort of intimacy with his ways--seen anything +that betrayed him to be unprincipled or unjust--anything that spoke him +of irreligious or immoral habits;--that among his own connections he was +esteemed and valued;--that even Wickham had allowed him merit as a +brother, and that she had often heard him speak so affectionately of his +sister as to prove him capable of some amiable feeling;--that had his +actions been what Wickham represented them, so gross a violation of +everything right could hardly have been concealed from the world; and +that friendship between a person capable of it and such an amiable man +as Mr. Bingley was incomprehensible. + +She grew absolutely ashamed of herself. Of neither Darcy nor Wickham +could she think, without feeling that she had been blind, partial, +prejudiced, absurd. + +β€œHow despicably have I acted!” she cried. β€œI, who have prided myself on +my discernment! I, who have valued myself on my abilities! who have +often disdained the generous candour of my sister, and gratified my +vanity in useless or blameless distrust. How humiliating is this +discovery! Yet, how just a humiliation! Had I been in love, I could not +have been more wretchedly blind. But vanity, not love, has been my +folly. Pleased with the preference of one, and offended by the neglect +of the other, on the very beginning of our acquaintance, I have courted +prepossession and ignorance, and driven reason away where either were +concerned. Till this moment, I never knew myself.” + +From herself to Jane, from Jane to Bingley, her thoughts were in a line +which soon brought to her recollection that Mr. Darcy’s explanation +_there_ had appeared very insufficient; and she read it again. Widely +different was the effect of a second perusal. How could she deny that +credit to his assertions, in one instance, which she had been obliged to +give in the other? He declared himself to have been totally unsuspicious +of her sister’s attachment; and she could not help remembering what +Charlotte’s opinion had always been. Neither could she deny the justice +of his description of Jane. She felt that Jane’s feelings, though +fervent, were little displayed, and that there was a constant +complacency in her air and manner, not often united with great +sensibility. + +When she came to that part of the letter in which her family were +mentioned, in tones of such mortifying, yet merited, reproach, her sense +of shame was severe. The justice of the charge struck her too forcibly +for denial; and the circumstances to which he particularly alluded, as +having passed at the Netherfield ball, and as confirming all his first +disapprobation, could not have made a stronger impression on his mind +than on hers. + +The compliment to herself and her sister was not unfelt. It soothed, but +it could not console her for the contempt which had been thus +self-attracted by the rest of her family; and as she considered that +Jane’s disappointment had, in fact, been the work of her nearest +relations, and reflected how materially the credit of both must be hurt +by such impropriety of conduct, she felt depressed beyond anything she +had ever known before. + +After wandering along the lane for two hours, giving way to every +variety of thought, reconsidering events, determining probabilities, and +reconciling herself, as well as she could, to a change so sudden and so +important, fatigue, and a recollection of her long absence, made her at +length return home; and she entered the house with the wish of appearing +cheerful as usual, and the resolution of repressing such reflections as +must make her unfit for conversation. + +She was immediately told, that the two gentlemen from Rosings had each +called during her absence; Mr. Darcy, only for a few minutes, to take +leave, but that Colonel Fitzwilliam had been sitting with them at least +an hour, hoping for her return, and almost resolving to walk after her +till she could be found. Elizabeth could but just _affect_ concern in +missing him; she really rejoiced at it. Colonel Fitzwilliam was no +longer an object. She could think only of her letter. + + + + +[Illustration: + +β€œHis parting obeisance” +] + + + + +CHAPTER XXXVII. + + +[Illustration] + +The two gentlemen left Rosings the next morning; and Mr. Collins having +been in waiting near the lodges, to make them his parting obeisance, was +able to bring home the pleasing intelligence of their appearing in very +good health, and in as tolerable spirits as could be expected, after the +melancholy scene so lately gone through at Rosings. To Rosings he then +hastened to console Lady Catherine and her daughter; and on his return +brought back, with great satisfaction, a message from her Ladyship, +importing that she felt herself so dull as to make her very desirous of +having them all to dine with her. + +Elizabeth could not see Lady Catherine without recollecting that, had +she chosen it, she might by this time have been presented to her as her +future niece; nor could she think, without a smile, of what her +Ladyship’s indignation would have been. β€œWhat would she have said? how +would she have behaved?” were the questions with which she amused +herself. + +Their first subject was the diminution of the Rosings’ party. β€œI assure +you, I feel it exceedingly,” said Lady Catherine; β€œI believe nobody +feels the loss of friends so much as I do. But I am particularly +attached to these young men; and know them to be so much attached to me! +They were excessively sorry to go! But so they always are. The dear +Colonel rallied his spirits tolerably till just at last; but Darcy +seemed to feel it most acutely--more, I think, than last year. His +attachment to Rosings certainly increases.” + +Mr. Collins had a compliment and an allusion to throw in here, which +were kindly smiled on by the mother and daughter. + +Lady Catherine observed, after dinner, that Miss Bennet seemed out of +spirits; and immediately accounting for it herself, by supposing that +she did not like to go home again so soon, she added,-- + +β€œBut if that is the case, you must write to your mother to beg that you +may stay a little longer. Mrs. Collins will be very glad of your +company, I am sure.” + +β€œI am much obliged to your Ladyship for your kind invitation,” replied +Elizabeth; β€œbut it is not in my power to accept it. I must be in town +next Saturday.” + +β€œWhy, at that rate, you will have been here only six weeks. I expected +you to stay two months. I told Mrs. Collins so before you came. There +can be no occasion for your going so soon. Mrs. Bennet could certainly +spare you for another fortnight.” + +β€œBut my father cannot. He wrote last week to hurry my return.” + +[Illustration: + +β€œDawson” + +[_Copyright 1894 by George Allen._]] + +β€œOh, your father, of course, may spare you, if your mother can. +Daughters are never of so much consequence to a father. And if you will +stay another _month_ complete, it will be in my power to take one of you +as far as London, for I am going there early in June, for a week; and +as Dawson does not object to the barouche-box, there will be very good +room for one of you--and, indeed, if the weather should happen to be +cool, I should not object to taking you both, as you are neither of you +large.” + +β€œYou are all kindness, madam; but I believe we must abide by our +original plan.” + +Lady Catherine seemed resigned. β€œMrs. Collins, you must send a servant +with them. You know I always speak my mind, and I cannot bear the idea +of two young women travelling post by themselves. It is highly improper. +You must contrive to send somebody. I have the greatest dislike in the +world to that sort of thing. Young women should always be properly +guarded and attended, according to their situation in life. When my +niece Georgiana went to Ramsgate last summer, I made a point of her +having two men-servants go with her. Miss Darcy, the daughter of Mr. +Darcy of Pemberley, and Lady Anne, could not have appeared with +propriety in a different manner. I am excessively attentive to all those +things. You must send John with the young ladies, Mrs. Collins. I am +glad it occurred to me to mention it; for it would really be +discreditable to _you_ to let them go alone.” + +β€œMy uncle is to send a servant for us.” + +β€œOh! Your uncle! He keeps a man-servant, does he? I am very glad you +have somebody who thinks of those things. Where shall you change horses? +Oh, Bromley, of course. If you mention my name at the Bell, you will be +attended to.” + +Lady Catherine had many other questions to ask respecting their journey; +and as she did not answer them all herself attention was +necessary--which Elizabeth believed to be lucky for her; or, with a +mind so occupied, she might have forgotten where she was. Reflection +must be reserved for solitary hours: whenever she was alone, she gave +way to it as the greatest relief; and not a day went by without a +solitary walk, in which she might indulge in all the delight of +unpleasant recollections. + +Mr. Darcy’s letter she was in a fair way of soon knowing by heart. She +studied every sentence; and her feelings towards its writer were at +times widely different. When she remembered the style of his address, +she was still full of indignation: but when she considered how unjustly +she had condemned and upbraided him, her anger was turned against +herself; and his disappointed feelings became the object of compassion. +His attachment excited gratitude, his general character respect: but she +could not approve him; nor could she for a moment repent her refusal, or +feel the slightest inclination ever to see him again. In her own past +behaviour, there was a constant source of vexation and regret: and in +the unhappy defects of her family, a subject of yet heavier chagrin. +They were hopeless of remedy. Her father, contented with laughing at +them, would never exert himself to restrain the wild giddiness of his +youngest daughters; and her mother, with manners so far from right +herself, was entirely insensible of the evil. Elizabeth had frequently +united with Jane in an endeavour to check the imprudence of Catherine +and Lydia; but while they were supported by their mother’s indulgence, +what chance could there be of improvement? Catherine, weak-spirited, +irritable, and completely under Lydia’s guidance, had been always +affronted by their advice; and Lydia, self-willed and careless, would +scarcely give them a hearing. They were ignorant, idle, and vain. While +there was an officer in Meryton, they would flirt with him; and while +Meryton was within a walk of Longbourn, they would be going there for +ever. + +Anxiety on Jane’s behalf was another prevailing concern; and Mr. Darcy’s +explanation, by restoring Bingley to all her former good opinion, +heightened the sense of what Jane had lost. His affection was proved to +have been sincere, and his conduct cleared of all blame, unless any +could attach to the implicitness of his confidence in his friend. How +grievous then was the thought that, of a situation so desirable in every +respect, so replete with advantage, so promising for happiness, Jane had +been deprived, by the folly and indecorum of her own family! + +When to these recollections was added the development of Wickham’s +character, it may be easily believed that the happy spirits which had +seldom been depressed before were now so much affected as to make it +almost impossible for her to appear tolerably cheerful. + +Their engagements at Rosings were as frequent during the last week of +her stay as they had been at first. The very last evening was spent +there; and her Ladyship again inquired minutely into the particulars of +their journey, gave them directions as to the best method of packing, +and was so urgent on the necessity of placing gowns in the only right +way, that Maria thought herself obliged, on her return, to undo all the +work of the morning, and pack her trunk afresh. + +When they parted, Lady Catherine, with great condescension, wished them +a good journey, and invited them to come to Hunsford again next year; +and Miss de Bourgh exerted herself so far as to courtesy and hold out +her hand to both. + + + + +[Illustration: + +β€œThe elevation of his feelings.” +] + + + + +CHAPTER XXXVIII. + + +[Illustration] + +On Saturday morning Elizabeth and Mr. Collins met for breakfast a few +minutes before the others appeared; and he took the opportunity of +paying the parting civilities which he deemed indispensably necessary. + +β€œI know not, Miss Elizabeth,” said he, β€œwhether Mrs. Collins has yet +expressed her sense of your kindness in coming to us; but I am very +certain you will not leave the house without receiving her thanks for +it. The favour of your company has been much felt, I assure you. We know +how little there is to tempt anyone to our humble abode. Our plain +manner of living, our small rooms, and few domestics, and the little we +see of the world, must make Hunsford extremely dull to a young lady like +yourself; but I hope you will believe us grateful for the condescension, +and that we have done everything in our power to prevent you spending +your time unpleasantly.” + +Elizabeth was eager with her thanks and assurances of happiness. She had +spent six weeks with great enjoyment; and the pleasure of being with +Charlotte, and the kind attention she had received, must make _her_ feel +the obliged. Mr. Collins was gratified; and with a more smiling +solemnity replied,-- + +β€œIt gives me the greatest pleasure to hear that you have passed your +time not disagreeably. We have certainly done our best; and most +fortunately having it in our power to introduce you to very superior +society, and from our connection with Rosings, the frequent means of +varying the humble home scene, I think we may flatter ourselves that +your Hunsford visit cannot have been entirely irksome. Our situation +with regard to Lady Catherine’s family is, indeed, the sort of +extraordinary advantage and blessing which few can boast. You see on +what a footing we are. You see how continually we are engaged there. In +truth, I must acknowledge, that, with all the disadvantages of this +humble parsonage, I should not think anyone abiding in it an object of +compassion, while they are sharers of our intimacy at Rosings.” + +Words were insufficient for the elevation of his feelings; and he was +obliged to walk about the room, while Elizabeth tried to unite civility +and truth in a few short sentences. + +β€œYou may, in fact, carry a very favourable report of us into +Hertfordshire, my dear cousin. I flatter myself, at least, that you will +be able to do so. Lady Catherine’s great attentions to Mrs. Collins you +have been a daily witness of; and altogether I trust it does not appear +that your friend has drawn an unfortunate--but on this point it will be +as well to be silent. Only let me assure you, my dear Miss Elizabeth, +that I can from my heart most cordially wish you equal felicity in +marriage. My dear Charlotte and I have but one mind and one way of +thinking. There is in everything a most remarkable resemblance of +character and ideas between us. We seem to have been designed for each +other.” + +Elizabeth could safely say that it was a great happiness where that was +the case, and with equal sincerity could add, that she firmly believed +and rejoiced in his domestic comforts. She was not sorry, however, to +have the recital of them interrupted by the entrance of the lady from +whom they sprang. Poor Charlotte! it was melancholy to leave her to such +society! But she had chosen it with her eyes open; and though evidently +regretting that her visitors were to go, she did not seem to ask for +compassion. Her home and her housekeeping, her parish and her poultry, +and all their dependent concerns, had not yet lost their charms. + +At length the chaise arrived, the trunks were fastened on, the parcels +placed within, and it was pronounced to be ready. After an affectionate +parting between the friends, Elizabeth was attended to the carriage by +Mr. Collins; and as they walked down the garden, he was commissioning +her with his best respects to all her family, not forgetting his thanks +for the kindness he had received at Longbourn in the winter, and his +compliments to Mr. and Mrs. Gardiner, though unknown. He then handed +her in, Maria followed, and the door was on the point of being closed, +when he suddenly reminded them, with some consternation, that they had +hitherto forgotten to leave any message for the ladies of Rosings. + +[Illustration: + +β€œThey had forgotten to leave any message” +] + +β€œBut,” he added, β€œyou will of course wish to have your humble respects +delivered to them, with your grateful thanks for their kindness to you +while you have been here.” + +Elizabeth made no objection: the door was then allowed to be shut, and +the carriage drove off. + +β€œGood gracious!” cried Maria, after a few minutes’ silence, β€œit seems +but a day or two since we first came! and yet how many things have +happened!” + +β€œA great many indeed,” said her companion, with a sigh. + +β€œWe have dined nine times at Rosings, besides drinking tea there twice! +How much I shall have to tell!” + +Elizabeth privately added, β€œAnd how much I shall have to conceal!” + +Their journey was performed without much conversation, or any alarm; and +within four hours of their leaving Hunsford they reached Mr. Gardiner’s +house, where they were to remain a few days. + +Jane looked well, and Elizabeth had little opportunity of studying her +spirits, amidst the various engagements which the kindness of her aunt +had reserved for them. But Jane was to go home with her, and at +Longbourn there would be leisure enough for observation. + +It was not without an effort, meanwhile, that she could wait even for +Longbourn, before she told her sister of Mr. Darcy’s proposals. To know +that she had the power of revealing what would so exceedingly astonish +Jane, and must, at the same time, so highly gratify whatever of her own +vanity she had not yet been able to reason away, was such a temptation +to openness as nothing could have conquered, but the state of indecision +in which she remained as to the extent of what she should communicate, +and her fear, if she once entered on the subject, of being hurried into +repeating something of Bingley, which might only grieve her sister +further. + + + + +[Illustration: + + β€œHow nicely we are crammed in” +] + + + + +CHAPTER XXXIX. + + +[Illustration] + +It was the second week in May, in which the three young ladies set out +together from Gracechurch Street for the town of ----, in Hertfordshire; +and, as they drew near the appointed inn where Mr. Bennet’s carriage was +to meet them, they quickly perceived, in token of the coachman’s +punctuality, both Kitty and Lydia looking out of a dining-room upstairs. +These two girls had been above an hour in the place, happily employed +in visiting an opposite milliner, watching the sentinel on guard, and +dressing a salad and cucumber. + +After welcoming their sisters, they triumphantly displayed a table set +out with such cold meat as an inn larder usually affords, exclaiming, +β€œIs not this nice? is not this an agreeable surprise?” + +β€œAnd we mean to treat you all,” added Lydia; β€œbut you must lend us the +money, for we have just spent ours at the shop out there.” Then showing +her purchases,--β€œLook here, I have bought this bonnet. I do not think it +is very pretty; but I thought I might as well buy it as not. I shall +pull it to pieces as soon as I get home, and see if I can make it up any +better.” + +And when her sisters abused it as ugly, she added, with perfect +unconcern, β€œOh, but there were two or three much uglier in the shop; and +when I have bought some prettier-coloured satin to trim it with fresh, I +think it will be very tolerable. Besides, it will not much signify what +one wears this summer, after the ----shire have left Meryton, and they +are going in a fortnight.” + +β€œAre they, indeed?” cried Elizabeth, with the greatest satisfaction. + +β€œThey are going to be encamped near Brighton; and I do so want papa to +take us all there for the summer! It would be such a delicious scheme, +and I dare say would hardly cost anything at all. Mamma would like to +go, too, of all things! Only think what a miserable summer else we shall +have!” + +β€œYes,” thought Elizabeth; β€œ_that_ would be a delightful scheme, indeed, +and completely do for us at once. Good Heaven! Brighton and a whole +campful of soldiers, to us, who have been overset already by one poor +regiment of militia, and the monthly balls of Meryton!” + +β€œNow I have got some news for you,” said Lydia, as they sat down to +table. β€œWhat do you think? It is excellent news, capital news, and about +a certain person that we all like.” + +Jane and Elizabeth looked at each other, and the waiter was told that he +need not stay. Lydia laughed, and said,-- + +β€œAy, that is just like your formality and discretion. You thought the +waiter must not hear, as if he cared! I dare say he often hears worse +things said than I am going to say. But he is an ugly fellow! I am glad +he is gone. I never saw such a long chin in my life. Well, but now for +my news: it is about dear Wickham; too good for the waiter, is not it? +There is no danger of Wickham’s marrying Mary King--there’s for you! She +is gone down to her uncle at Liverpool; gone to stay. Wickham is safe.” + +β€œAnd Mary King is safe!” added Elizabeth; β€œsafe from a connection +imprudent as to fortune.” + +β€œShe is a great fool for going away, if she liked him.” + +β€œBut I hope there is no strong attachment on either side,” said Jane. + +β€œI am sure there is not on _his_. I will answer for it, he never cared +three straws about her. Who _could_ about such a nasty little freckled +thing?” + +Elizabeth was shocked to think that, however incapable of such +coarseness of _expression_ herself, the coarseness of the _sentiment_ +was little other than her own breast had formerly harboured and fancied +liberal! + +As soon as all had ate, and the elder ones paid, the carriage was +ordered; and, after some contrivance, the whole party, with all their +boxes, workbags, and parcels, and the unwelcome addition of Kitty’s and +Lydia’s purchases, were seated in it. + +β€œHow nicely we are crammed in!” cried Lydia. β€œI am glad I brought my +bonnet, if it is only for the fun of having another band-box! Well, now +let us be quite comfortable and snug, and talk and laugh all the way +home. And in the first place, let us hear what has happened to you all +since you went away. Have you seen any pleasant men? Have you had any +flirting? I was in great hopes that one of you would have got a husband +before you came back. Jane will be quite an old maid soon, I declare. +She is almost three-and-twenty! Lord! how ashamed I should be of not +being married before three-and-twenty! My aunt Philips wants you so to +get husbands you can’t think. She says Lizzy had better have taken Mr. +Collins; but _I_ do not think there would have been any fun in it. Lord! +how I should like to be married before any of you! and then I would +_chaperon_ you about to all the balls. Dear me! we had such a good piece +of fun the other day at Colonel Forster’s! Kitty and me were to spend +the day there, and Mrs. Forster promised to have a little dance in the +evening; (by-the-bye, Mrs. Forster and me are _such_ friends!) and so +she asked the two Harringtons to come: but Harriet was ill, and so Pen +was forced to come by herself; and then, what do you think we did? We +dressed up Chamberlayne in woman’s clothes, on purpose to pass for a +lady,--only think what fun! Not a soul knew of it, but Colonel and Mrs. +Forster, and Kitty and me, except my aunt, for we were forced to borrow +one of her gowns; and you cannot imagine how well he looked! When Denny, +and Wickham, and Pratt, and two or three more of the men came in, they +did not know him in the least. Lord! how I laughed! and so did Mrs. +Forster. I thought I should have died. And _that_ made the men suspect +something, and then they soon found out what was the matter.” + +With such kind of histories of their parties and good jokes did Lydia, +assisted by Kitty’s hints and additions, endeavour to amuse her +companions all the way to Longbourn. Elizabeth listened as little as she +could, but there was no escaping the frequent mention of Wickham’s name. + +Their reception at home was most kind. Mrs. Bennet rejoiced to see Jane +in undiminished beauty; and more than once during dinner did Mr. Bennet +say voluntarily to Elizabeth,---- + +β€œI am glad you are come back, Lizzy.” + +Their party in the dining-room was large, for almost all the Lucases +came to meet Maria and hear the news; and various were the subjects +which occupied them: Lady Lucas was inquiring of Maria, across the +table, after the welfare and poultry of her eldest daughter; Mrs. Bennet +was doubly engaged, on one hand collecting an account of the present +fashions from Jane, who sat some way below her, and on the other, +retailing them all to the younger Miss Lucases; and Lydia, in a voice +rather louder than any other person’s, was enumerating the various +pleasures of the morning to anybody who would hear her. + +β€œOh, Mary,” said she, β€œI wish you had gone with us, for we had such fun! +as we went along Kitty and me drew up all the blinds, and pretended +there was nobody in the coach; and I should have gone so all the way, if +Kitty had not been sick; and when we got to the George, I do think we +behaved very handsomely, for we treated the other three with the nicest +cold luncheon in the world, and if you would have gone, we would have +treated you too. And then when we came away it was such fun! I thought +we never should have got into the coach. I was ready to die of laughter. +And then we were so merry all the way home! we talked and laughed so +loud, that anybody might have heard us ten miles off!” + +To this, Mary very gravely replied, β€œFar be it from me, my dear sister, +to depreciate such pleasures. They would doubtless be congenial with the +generality of female minds. But I confess they would have no charms for +_me_. I should infinitely prefer a book.” + +But of this answer Lydia heard not a word. She seldom listened to +anybody for more than half a minute, and never attended to Mary at all. + +In the afternoon Lydia was urgent with the rest of the girls to walk to +Meryton, and see how everybody went on; but Elizabeth steadily opposed +the scheme. It should not be said, that the Miss Bennets could not be at +home half a day before they were in pursuit of the officers. There was +another reason, too, for her opposition. She dreaded seeing Wickham +again, and was resolved to avoid it as long as possible. The comfort to +_her_, of the regiment’s approaching removal, was indeed beyond +expression. In a fortnight they were to go, and once gone, she hoped +there could be nothing more to plague her on his account. + +She had not been many hours at home, before she found that the Brighton +scheme, of which Lydia had given them a hint at the inn, was under +frequent discussion between her parents. Elizabeth saw directly that her +father had not the smallest intention of yielding; but his answers were +at the same time so vague and equivocal, that her mother, though often +disheartened, had never yet despaired of succeeding at last. + + + + +[Illustration] + + + + +CHAPTER XL. + + +[Illustration] + +Elizabeth’s impatience to acquaint Jane with what had happened could no +longer be overcome; and at length resolving to suppress every particular +in which her sister was concerned, and preparing her to be surprised, +she related to her the next morning the chief of the scene between Mr. +Darcy and herself. + +Miss Bennet’s astonishment was soon lessened by the strong sisterly +partiality which made any admiration of Elizabeth appear perfectly +natural; and all surprise was shortly lost in other feelings. She was +sorry that Mr. Darcy should have delivered his sentiments in a manner so +little suited to recommend them; but still more was she grieved for the +unhappiness which her sister’s refusal must have given him. + +β€œHis being so sure of succeeding was wrong,” said she, β€œand certainly +ought not to have appeared; but consider how much it must increase his +disappointment.” + +β€œIndeed,” replied Elizabeth, β€œI am heartily sorry for him; but he has +other feelings which will probably soon drive away his regard for me. +You do not blame me, however, for refusing him?” + +β€œBlame you! Oh, no.” + +β€œBut you blame me for having spoken so warmly of Wickham?” + +β€œNo--I do not know that you were wrong in saying what you did.” + +β€œBut you _will_ know it, when I have told you what happened the very +next day.” + +She then spoke of the letter, repeating the whole of its contents as far +as they concerned George Wickham. What a stroke was this for poor Jane, +who would willingly have gone through the world without believing that +so much wickedness existed in the whole race of mankind as was here +collected in one individual! Nor was Darcy’s vindication, though +grateful to her feelings, capable of consoling her for such discovery. +Most earnestly did she labour to prove the probability of error, and +seek to clear one, without involving the other. + +β€œThis will not do,” said Elizabeth; β€œyou never will be able to make both +of them good for anything. Take your choice, but you must be satisfied +with only one. There is but such a quantity of merit between them; just +enough to make one good sort of man; and of late it has been shifting +about pretty much. For my part, I am inclined to believe it all Mr. +Darcy’s, but you shall do as you choose.” + +It was some time, however, before a smile could be extorted from Jane. + +β€œI do not know when I have been more shocked,” said she. β€œWickham so +very bad! It is almost past belief. And poor Mr. Darcy! dear Lizzy, +only consider what he must have suffered. Such a disappointment! and +with the knowledge of your ill opinion too! and having to relate such a +thing of his sister! It is really too distressing, I am sure you must +feel it so.” + +β€œOh no, my regret and compassion are all done away by seeing you so full +of both. I know you will do him such ample justice, that I am growing +every moment more unconcerned and indifferent. Your profusion makes me +saving; and if you lament over him much longer, my heart will be as +light as a feather.” + +β€œPoor Wickham! there is such an expression of goodness in his +countenance! such an openness and gentleness in his manner.” + +β€œThere certainly was some great mismanagement in the education of those +two young men. One has got all the goodness, and the other all the +appearance of it.” + +β€œI never thought Mr. Darcy so deficient in the _appearance_ of it as you +used to do.” + +β€œAnd yet I meant to be uncommonly clever in taking so decided a dislike +to him, without any reason. It is such a spur to one’s genius, such an +opening for wit, to have a dislike of that kind. One may be continually +abusive without saying anything just; but one cannot be always laughing +at a man without now and then stumbling on something witty.” + +β€œLizzy, when you first read that letter, I am sure you could not treat +the matter as you do now.” + +β€œIndeed, I could not. I was uncomfortable enough, I was very +uncomfortable--I may say unhappy. And with no one to speak to of what I +felt, no Jane to comfort me, and say that I had not been so very weak, +and vain, and nonsensical, as I knew I had! Oh, how I wanted you!” + +β€œHow unfortunate that you should have used such very strong expressions +in speaking of Wickham to Mr. Darcy, for now they _do_ appear wholly +undeserved.” + +β€œCertainly. But the misfortune of speaking with bitterness is a most +natural consequence of the prejudices I had been encouraging. There is +one point on which I want your advice. I want to be told whether I +ought, or ought not, to make our acquaintance in general understand +Wickham’s character.” + +Miss Bennet paused a little, and then replied, β€œSurely there can be no +occasion for exposing him so dreadfully. What is your own opinion?” + +β€œThat it ought not to be attempted. Mr. Darcy has not authorized me to +make his communication public. On the contrary, every particular +relative to his sister was meant to be kept as much as possible to +myself; and if I endeavour to undeceive people as to the rest of his +conduct, who will believe me? The general prejudice against Mr. Darcy is +so violent, that it would be the death of half the good people in +Meryton, to attempt to place him in an amiable light. I am not equal to +it. Wickham will soon be gone; and, therefore, it will not signify to +anybody here what he really is. Some time hence it will be all found +out, and then we may laugh at their stupidity in not knowing it before. +At present I will say nothing about it.” + +β€œYou are quite right. To have his errors made public might ruin him for +ever. He is now, perhaps, sorry for what he has done, and anxious to +re-establish a character. We must not make him desperate.” + +The tumult of Elizabeth’s mind was allayed by this conversation. She +had got rid of two of the secrets which had weighed on her for a +fortnight, and was certain of a willing listener in Jane, whenever she +might wish to talk again of either. But there was still something +lurking behind, of which prudence forbade the disclosure. She dared not +relate the other half of Mr. Darcy’s letter, nor explain to her sister +how sincerely she had been valued by his friend. Here was knowledge in +which no one could partake; and she was sensible that nothing less than +a perfect understanding between the parties could justify her in +throwing off this last encumbrance of mystery. β€œAnd then,” said she, β€œif +that very improbable event should ever take place, I shall merely be +able to tell what Bingley may tell in a much more agreeable manner +himself. The liberty of communication cannot be mine till it has lost +all its value!” + +She was now, on being settled at home, at leisure to observe the real +state of her sister’s spirits. Jane was not happy. She still cherished a +very tender affection for Bingley. Having never even fancied herself in +love before, her regard had all the warmth of first attachment, and from +her age and disposition, greater steadiness than first attachments often +boast; and so fervently did she value his remembrance, and prefer him to +every other man, that all her good sense, and all her attention to the +feelings of her friends, were requisite to check the indulgence of those +regrets which must have been injurious to her own health and their +tranquillity. + +β€œWell, Lizzy,” said Mrs. Bennet, one day, β€œwhat is your opinion _now_ of +this sad business of Jane’s? For my part, I am determined never to speak +of it again to anybody. I told my sister Philips so the other day. But I +cannot find out that Jane saw anything of him in London. Well, he is a +very undeserving young man--and I do not suppose there is the least +chance in the world of her ever getting him now. There is no talk of his +coming to Netherfield again in the summer; and I have inquired of +everybody, too, who is likely to know.” + +[Illustration: + + β€œI am determined never to speak of it again” +] + +β€œI do not believe that he will ever live at Netherfield any more.” + +β€œOh, well! it is just as he chooses. Nobody wants him to come; though I +shall always say that he used my daughter extremely ill; and, if I was +her, I would not have put up with it. Well, my comfort is, I am sure +Jane will die of a broken heart, and then he will be sorry for what he +has done.” + +But as Elizabeth could not receive comfort from any such expectation she +made no answer. + +β€œWell, Lizzy,” continued her mother, soon afterwards, β€œand so the +Collinses live very comfortable, do they? Well, well, I only hope it +will last. And what sort of table do they keep? Charlotte is an +excellent manager, I dare say. If she is half as sharp as her mother, +she is saving enough. There is nothing extravagant in _their_ +housekeeping, I dare say.” + +β€œNo, nothing at all.” + +β€œA great deal of good management, depend upon it. Yes, yes. _They_ will +take care not to outrun their income. _They_ will never be distressed +for money. Well, much good may it do them! And so, I suppose, they often +talk of having Longbourn when your father is dead. They look upon it +quite as their own, I dare say, whenever that happens.” + +β€œIt was a subject which they could not mention before me.” + +β€œNo; it would have been strange if they had. But I make no doubt they +often talk of it between themselves. Well, if they can be easy with an +estate that is not lawfully their own, so much the better. _I_ should be +ashamed of having one that was only entailed on me.” + + + + +[Illustration: + +β€œWhen Colonel Miller’s regiment went away” + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER XLI. + + +[Illustration] + +The first week of their return was soon gone. The second began. It was +the last of the regiment’s stay in Meryton, and all the young ladies in +the neighbourhood were drooping apace. The dejection was almost +universal. The elder Miss Bennets alone were still able to eat, drink, +and sleep, and pursue the usual course of their employments. Very +frequently were they reproached for this insensibility by Kitty and +Lydia, whose own misery was extreme, and who could not comprehend such +hard-heartedness in any of the family. + +β€œGood Heaven! What is to become of us? What are we to do?” would they +often exclaim in the bitterness of woe. β€œHow can you be smiling so, +Lizzy?” + +Their affectionate mother shared all their grief; she remembered what +she had herself endured on a similar occasion five-and-twenty years ago. + +β€œI am sure,” said she, β€œI cried for two days together when Colonel +Miller’s regiment went away. I thought I should have broke my heart.” + +β€œI am sure I shall break _mine_,” said Lydia. + +β€œIf one could but go to Brighton!” observed Mrs. Bennet. + +β€œOh yes!--if one could but go to Brighton! But papa is so disagreeable.” + +β€œA little sea-bathing would set me up for ever.” + +β€œAnd my aunt Philips is sure it would do _me_ a great deal of good,” +added Kitty. + +Such were the kind of lamentations resounding perpetually through +Longbourn House. Elizabeth tried to be diverted by them; but all sense +of pleasure was lost in shame. She felt anew the justice of Mr. Darcy’s +objections; and never had she before been so much disposed to pardon his +interference in the views of his friend. + +But the gloom of Lydia’s prospect was shortly cleared away; for she +received an invitation from Mrs. Forster, the wife of the colonel of the +regiment, to accompany her to Brighton. This invaluable friend was a +very young woman, and very lately married. A resemblance in good-humour +and good spirits had recommended her and Lydia to each other, and out of +their _three_ months’ acquaintance they had been intimate _two_. + +The rapture of Lydia on this occasion, her adoration of Mrs. Forster, +the delight of Mrs. Bennet, and the mortification of Kitty, are scarcely +to be described. Wholly inattentive to her sister’s feelings, Lydia flew +about the house in restless ecstasy, calling for everyone’s +congratulations, and laughing and talking with more violence than ever; +whilst the luckless Kitty continued in the parlour repining at her fate +in terms as unreasonable as her accent was peevish. + +β€œI cannot see why Mrs. Forster should not ask _me_ as well as Lydia,” +said she, β€œthough I am _not_ her particular friend. I have just as much +right to be asked as she has, and more too, for I am two years older.” + +In vain did Elizabeth attempt to make her reasonable, and Jane to make +her resigned. As for Elizabeth herself, this invitation was so far from +exciting in her the same feelings as in her mother and Lydia, that she +considered it as the death-warrant of all possibility of common sense +for the latter; and detestable as such a step must make her, were it +known, she could not help secretly advising her father not to let her +go. She represented to him all the improprieties of Lydia’s general +behaviour, the little advantage she could derive from the friendship of +such a woman as Mrs. Forster, and the probability of her being yet more +imprudent with such a companion at Brighton, where the temptations must +be greater than at home. He heard her attentively, and then said,-- + +β€œLydia will never be easy till she has exposed herself in some public +place or other, and we can never expect her to do it with so little +expense or inconvenience to her family as under the present +circumstances.” + +β€œIf you were aware,” said Elizabeth, β€œof the very great disadvantage to +us all, which must arise from the public notice of Lydia’s unguarded and +imprudent manner, nay, which has already arisen from it, I am sure you +would judge differently in the affair.” + +β€œAlready arisen!” repeated Mr. Bennet. β€œWhat! has she frightened away +some of your lovers? Poor little Lizzy! But do not be cast down. Such +squeamish youths as cannot bear to be connected with a little absurdity +are not worth a regret. Come, let me see the list of the pitiful fellows +who have been kept aloof by Lydia’s folly.” + +β€œIndeed, you are mistaken. I have no such injuries to resent. It is not +of peculiar, but of general evils, which I am now complaining. Our +importance, our respectability in the world, must be affected by the +wild volatility, the assurance and disdain of all restraint which mark +Lydia’s character. Excuse me,--for I must speak plainly. If you, my dear +father, will not take the trouble of checking her exuberant spirits, and +of teaching her that her present pursuits are not to be the business of +her life, she will soon be beyond the reach of amendment. Her character +will be fixed; and she will, at sixteen, be the most determined flirt +that ever made herself and her family ridiculous;--a flirt, too, in the +worst and meanest degree of flirtation; without any attraction beyond +youth and a tolerable person; and, from the ignorance and emptiness of +her mind, wholly unable to ward off any portion of that universal +contempt which her rage for admiration will excite. In this danger Kitty +is also comprehended. She will follow wherever Lydia leads. Vain, +ignorant, idle, and absolutely uncontrolled! Oh, my dear father, can you +suppose it possible that they will not be censured and despised wherever +they are known, and that their sisters will not be often involved in the +disgrace?” + +Mr. Bennet saw that her whole heart was in the subject; and, +affectionately taking her hand, said, in reply,-- + +β€œDo not make yourself uneasy, my love. Wherever you and Jane are known, +you must be respected and valued; and you will not appear to less +advantage for having a couple of--or I may say, three--very silly +sisters. We shall have no peace at Longbourn if Lydia does not go to +Brighton. Let her go, then. Colonel Forster is a sensible man, and will +keep her out of any real mischief; and she is luckily too poor to be an +object of prey to anybody. At Brighton she will be of less importance +even as a common flirt than she has been here. The officers will find +women better worth their notice. Let us hope, therefore, that her being +there may teach her her own insignificance. At any rate, she cannot grow +many degrees worse, without authorizing us to lock her up for the rest +of her life.” + +With this answer Elizabeth was forced to be content; but her own opinion +continued the same, and she left him disappointed and sorry. It was not +in her nature, however, to increase her vexations by dwelling on them. +She was confident of having performed her duty; and to fret over +unavoidable evils, or augment them by anxiety, was no part of her +disposition. + +Had Lydia and her mother known the substance of her conference with her +father, their indignation would hardly have found expression in their +united volubility. In Lydia’s imagination, a visit to Brighton comprised +every possibility of earthly happiness. She saw, with the creative eye +of fancy, the streets of that gay bathing-place covered with officers. +She saw herself the object of attention to tens and to scores of them at +present unknown. She saw all the glories of the camp: its tents +stretched forth in beauteous uniformity of lines, crowded with the young +and the gay, and dazzling with scarlet; and, to complete the view, she +saw herself seated beneath a tent, tenderly flirting with at least six +officers at once. + +[Illustration: + +β€œTenderly flirting” + +[_Copyright 1894 by George Allen._]] + +Had she known that her sister sought to tear her from such prospects and +such realities as these, what would have been her sensations? They could +have been understood only by her mother, who might have felt nearly the +same. Lydia’s going to Brighton was all that consoled her for the +melancholy conviction of her husband’s never intending to go there +himself. + +But they were entirely ignorant of what had passed; and their raptures +continued, with little intermission, to the very day of Lydia’s leaving +home. + +Elizabeth was now to see Mr. Wickham for the last time. Having been +frequently in company with him since her return, agitation was pretty +well over; the agitations of former partiality entirely so. She had even +learnt to detect, in the very gentleness which had first delighted her, +an affectation and a sameness to disgust and weary. In his present +behaviour to herself, moreover, she had a fresh source of displeasure; +for the inclination he soon testified of renewing those attentions which +had marked the early part of their acquaintance could only serve, after +what had since passed, to provoke her. She lost all concern for him in +finding herself thus selected as the object of such idle and frivolous +gallantry; and while she steadily repressed it, could not but feel the +reproof contained in his believing, that however long, and for whatever +cause, his attentions had been withdrawn, her vanity would be gratified, +and her preference secured, at any time, by their renewal. + +On the very last day of the regiment’s remaining in Meryton, he dined, +with others of the officers, at Longbourn; and so little was Elizabeth +disposed to part from him in good-humour, that, on his making some +inquiry as to the manner in which her time had passed at Hunsford, she +mentioned Colonel Fitzwilliam’s and Mr. Darcy’s having both spent three +weeks at Rosings, and asked him if he were acquainted with the former. + +He looked surprised, displeased, alarmed; but, with a moment’s +recollection, and a returning smile, replied, that he had formerly seen +him often; and, after observing that he was a very gentlemanlike man, +asked her how she had liked him. Her answer was warmly in his favour. +With an air of indifference, he soon afterwards added, β€œHow long did you +say that he was at Rosings?” + +β€œNearly three weeks.” + +β€œAnd you saw him frequently?” + +β€œYes, almost every day.” + +β€œHis manners are very different from his cousin’s.” + +β€œYes, very different; but I think Mr. Darcy improves on acquaintance.” + +β€œIndeed!” cried Wickham, with a look which did not escape her. β€œAnd pray +may I ask--” but checking himself, he added, in a gayer tone, β€œIs it in +address that he improves? Has he deigned to add aught of civility to his +ordinary style? for I dare not hope,” he continued, in a lower and more +serious tone, β€œthat he is improved in essentials.” + +β€œOh, no!” said Elizabeth. β€œIn essentials, I believe, he is very much +what he ever was.” + +While she spoke, Wickham looked as if scarcely knowing whether to +rejoice over her words or to distrust their meaning. There was a +something in her countenance which made him listen with an apprehensive +and anxious attention, while she added,-- + +β€œWhen I said that he improved on acquaintance, I did not mean that +either his mind or manners were in a state of improvement; but that, +from knowing him better, his disposition was better understood.” + +Wickham’s alarm now appeared in a heightened complexion and agitated +look; for a few minutes he was silent; till, shaking off his +embarrassment, he turned to her again, and said in the gentlest of +accents,-- + +β€œYou, who so well know my feelings towards Mr. Darcy, will readily +comprehend how sincerely I must rejoice that he is wise enough to assume +even the _appearance_ of what is right. His pride, in that direction, +may be of service, if not to himself, to many others, for it must deter +him from such foul misconduct as I have suffered by. I only fear that +the sort of cautiousness to which you, I imagine, have been alluding, is +merely adopted on his visits to his aunt, of whose good opinion and +judgment he stands much in awe. His fear of her has always operated, I +know, when they were together; and a good deal is to be imputed to his +wish of forwarding the match with Miss de Bourgh, which I am certain he +has very much at heart.” + +Elizabeth could not repress a smile at this, but she answered only by a +slight inclination of the head. She saw that he wanted to engage her on +the old subject of his grievances, and she was in no humour to indulge +him. The rest of the evening passed with the _appearance_, on his side, +of usual cheerfulness, but with no further attempt to distinguish +Elizabeth; and they parted at last with mutual civility, and possibly a +mutual desire of never meeting again. + +When the party broke up, Lydia returned with Mrs. Forster to Meryton, +from whence they were to set out early the next morning. The separation +between her and her family was rather noisy than pathetic. Kitty was the +only one who shed tears; but she did weep from vexation and envy. Mrs. +Bennet was diffuse in her good wishes for the felicity of her daughter, +and impressive in her injunctions that she would not miss the +opportunity of enjoying herself as much as possible,--advice which there +was every reason to believe would be attended to; and, in the clamorous +happiness of Lydia herself in bidding farewell, the more gentle adieus +of her sisters were uttered without being heard. + + + + +[Illustration: + +The arrival of the +Gardiners +] + + + + +CHAPTER XLII. + + +[Illustration] + +Had Elizabeth’s opinion been all drawn from her own family, she could +not have formed a very pleasing picture of conjugal felicity or domestic +comfort. Her father, captivated by youth and beauty, and that appearance +of good-humour which youth and beauty generally give, had married a +woman whose weak understanding and illiberal mind had very early in +their marriage put an end to all real affection for her. Respect, +esteem, and confidence had vanished for ever; and all his views of +domestic happiness were overthrown. But Mr. Bennet was not of a +disposition to seek comfort for the disappointment which his own +imprudence had brought on in any of those pleasures which too often +console the unfortunate for their folly or their vice. He was fond of +the country and of books; and from these tastes had arisen his principal +enjoyments. To his wife he was very little otherwise indebted than as +her ignorance and folly had contributed to his amusement. This is not +the sort of happiness which a man would in general wish to owe to his +wife; but where other powers of entertainment are wanting, the true +philosopher will derive benefit from such as are given. + +Elizabeth, however, had never been blind to the impropriety of her +father’s behaviour as a husband. She had always seen it with pain; but +respecting his abilities, and grateful for his affectionate treatment of +herself, she endeavoured to forget what she could not overlook, and to +banish from her thoughts that continual breach of conjugal obligation +and decorum which, in exposing his wife to the contempt of her own +children, was so highly reprehensible. But she had never felt so +strongly as now the disadvantages which must attend the children of so +unsuitable a marriage, nor ever been so fully aware of the evils arising +from so ill-judged a direction of talents--talents which, rightly used, +might at least have preserved the respectability of his daughters, even +if incapable of enlarging the mind of his wife. + +When Elizabeth had rejoiced over Wickham’s departure, she found little +other cause for satisfaction in the loss of the regiment. Their parties +abroad were less varied than before; and at home she had a mother and +sister, whose constant repinings at the dulness of everything around +them threw a real gloom over their domestic circle; and, though Kitty +might in time regain her natural degree of sense, since the disturbers +of her brain were removed, her other sister, from whose disposition +greater evil might be apprehended, was likely to be hardened in all her +folly and assurance, by a situation of such double danger as a +watering-place and a camp. Upon the whole, therefore, she found, what +has been sometimes found before, that an event to which she had looked +forward with impatient desire, did not, in taking place, bring all the +satisfaction she had promised herself. It was consequently necessary to +name some other period for the commencement of actual felicity; to have +some other point on which her wishes and hopes might be fixed, and by +again enjoying the pleasure of anticipation, console herself for the +present, and prepare for another disappointment. Her tour to the Lakes +was now the object of her happiest thoughts: it was her best consolation +for all the uncomfortable hours which the discontentedness of her mother +and Kitty made inevitable; and could she have included Jane in the +scheme, every part of it would have been perfect. + +β€œBut it is fortunate,” thought she, β€œthat I have something to wish for. +Were the whole arrangement complete, my disappointment would be certain. +But here, by carrying with me one ceaseless source of regret in my +sister’s absence, I may reasonably hope to have all my expectations of +pleasure realized. A scheme of which every part promises delight can +never be successful; and general disappointment is only warded off by +the defence of some little peculiar vexation.” + +When Lydia went away she promised to write very often and very minutely +to her mother and Kitty; but her letters were always long expected, and +always very short. Those to her mother contained little else than that +they were just returned from the library, where such and such officers +had attended them, and where she had seen such beautiful ornaments as +made her quite wild; that she had a new gown, or a new parasol, which +she would have described more fully, but was obliged to leave off in a +violent hurry, as Mrs. Forster called her, and they were going to the +camp; and from her correspondence with her sister there was still less +to be learnt, for her letters to Kitty, though rather longer, were much +too full of lines under the words to be made public. + +After the first fortnight or three weeks of her absence, health, +good-humour, and cheerfulness began to reappear at Longbourn. Everything +wore a happier aspect. The families who had been in town for the winter +came back again, and summer finery and summer engagements arose. Mrs. +Bennet was restored to her usual querulous serenity; and by the middle +of June Kitty was so much recovered as to be able to enter Meryton +without tears,--an event of such happy promise as to make Elizabeth +hope, that by the following Christmas she might be so tolerably +reasonable as not to mention an officer above once a day, unless, by +some cruel and malicious arrangement at the War Office, another regiment +should be quartered in Meryton. + +The time fixed for the beginning of their northern tour was now fast +approaching; and a fortnight only was wanting of it, when a letter +arrived from Mrs. Gardiner, which at once delayed its commencement and +curtailed its extent. Mr. Gardiner would be prevented by business from +setting out till a fortnight later in July, and must be in London again +within a month; and as that left too short a period for them to go so +far, and see so much as they had proposed, or at least to see it with +the leisure and comfort they had built on, they were obliged to give up +the Lakes, and substitute a more contracted tour; and, according to the +present plan, were to go no farther northward than Derbyshire. In that +county there was enough to be seen to occupy the chief of their three +weeks; and to Mrs. Gardiner it had a peculiarly strong attraction. The +town where she had formerly passed some years of her life, and where +they were now to spend a few days, was probably as great an object of +her curiosity as all the celebrated beauties of Matlock, Chatsworth, +Dovedale, or the Peak. + +Elizabeth was excessively disappointed: she had set her heart on seeing +the Lakes; and still thought there might have been time enough. But it +was her business to be satisfied--and certainly her temper to be happy; +and all was soon right again. + +With the mention of Derbyshire, there were many ideas connected. It was +impossible for her to see the word without thinking of Pemberley and its +owner. β€œBut surely,” said she, β€œI may enter his county with impunity, +and rob it of a few petrified spars, without his perceiving me.” + +The period of expectation was now doubled. Four weeks were to pass away +before her uncle and aunt’s arrival. But they did pass away, and Mr. and +Mrs. Gardiner, with their four children, did at length appear at +Longbourn. The children, two girls of six and eight years old, and two +younger boys, were to be left under the particular care of their cousin +Jane, who was the general favourite, and whose steady sense and +sweetness of temper exactly adapted her for attending to them in every +way--teaching them, playing with them, and loving them. + +The Gardiners stayed only one night at Longbourn, and set off the next +morning with Elizabeth in pursuit of novelty and amusement. One +enjoyment was certain--that of suitableness as companions; a +suitableness which comprehended health and temper to bear +inconveniences--cheerfulness to enhance every pleasure--and affection +and intelligence, which might supply it among themselves if there were +disappointments abroad. + +It is not the object of this work to give a description of Derbyshire, +nor of any of the remarkable places through which their route thither +lay--Oxford, Blenheim, Warwick, Kenilworth, Birmingham, etc., are +sufficiently known. A small part of Derbyshire is all the present +concern. To the little town of Lambton, the scene of Mrs. Gardiner’s +former residence, and where she had lately learned that some +acquaintance still remained, they bent their steps, after having seen +all the principal wonders of the country; and within five miles of +Lambton, Elizabeth found, from her aunt, that Pemberley was situated. It +was not in their direct road; nor more than a mile or two out of it. In +talking over their route the evening before, Mrs. Gardiner expressed an +inclination to see the place again. Mr. Gardiner declared his +willingness, and Elizabeth was applied to for her approbation. + +β€œMy love, should not you like to see a place of which you have heard so +much?” said her aunt. β€œA place, too, with which so many of your +acquaintance are connected. Wickham passed all his youth there, you +know.” + +Elizabeth was distressed. She felt that she had no business at +Pemberley, and was obliged to assume a disinclination for seeing it. She +must own that she was tired of great houses: after going over so many, +she really had no pleasure in fine carpets or satin curtains. + +Mrs. Gardiner abused her stupidity. β€œIf it were merely a fine house +richly furnished,” said she, β€œI should not care about it myself; but the +grounds are delightful. They have some of the finest woods in the +country.” + +Elizabeth said no more; but her mind could not acquiesce. The +possibility of meeting Mr. Darcy, while viewing the place, instantly +occurred. It would be dreadful! She blushed at the very idea; and +thought it would be better to speak openly to her aunt, than to run such +a risk. But against this there were objections; and she finally resolved +that it could be the last resource, if her private inquiries as to the +absence of the family were unfavourably answered. + +Accordingly, when she retired at night, she asked the chambermaid +whether Pemberley were not a very fine place, what was the name of its +proprietor, and, with no little alarm, whether the family were down for +the summer? A most welcome negative followed the last question; and her +alarms being now removed, she was at leisure to feel a great deal of +curiosity to see the house herself; and when the subject was revived the +next morning, and she was again applied to, could readily answer, and +with a proper air of indifference, that she had not really any dislike +to the scheme. + +To Pemberley, therefore, they were to go. + + + + +[Illustration: + + β€œConjecturing as to the date” +] + + + + +CHAPTER XLIII. + + +[Illustration] + +Elizabeth, as they drove along, watched for the first appearance of +Pemberley Woods with some perturbation; and when at length they turned +in at the lodge, her spirits were in a high flutter. + +The park was very large, and contained great variety of ground. They +entered it in one of its lowest points, and drove for some time through +a beautiful wood stretching over a wide extent. + +Elizabeth’s mind was too full for conversation, but she saw and admired +every remarkable spot and point of view. They gradually ascended for +half a mile, and then found themselves at the top of a considerable +eminence, where the wood ceased, and the eye was instantly caught by +Pemberley House, situated on the opposite side of the valley, into which +the road with some abruptness wound. It was a large, handsome stone +building, standing well on rising ground, and backed by a ridge of high +woody hills; and in front a stream of some natural importance was +swelled into greater, but without any artificial appearance. Its banks +were neither formal nor falsely adorned. Elizabeth was delighted. She +had never seen a place for which nature had done more, or where natural +beauty had been so little counteracted by an awkward taste. They were +all of them warm in their admiration; and at that moment she felt that +to be mistress of Pemberley might be something! + +They descended the hill, crossed the bridge, and drove to the door; and, +while examining the nearer aspect of the house, all her apprehension of +meeting its owner returned. She dreaded lest the chambermaid had been +mistaken. On applying to see the place, they were admitted into the +hall; and Elizabeth, as they waited for the housekeeper, had leisure to +wonder at her being where she was. + +The housekeeper came; a respectable looking elderly woman, much less +fine, and more civil, than she had any notion of finding her. They +followed her into the dining-parlour. It was a large, well-proportioned +room, handsomely fitted up. Elizabeth, after slightly surveying it, went +to a window to enjoy its prospect. The hill, crowned with wood, from +which they had descended, receiving increased abruptness from the +distance, was a beautiful object. Every disposition of the ground was +good; and she looked on the whole scene, the river, the trees scattered +on its banks, and the winding of the valley, as far as she could trace +it, with delight. As they passed into other rooms, these objects were +taking different positions; but from every window there were beauties +to be seen. The rooms were lofty and handsome, and their furniture +suitable to the fortune of their proprietor; but Elizabeth saw, with +admiration of his taste, that it was neither gaudy nor uselessly +fine,--with less of splendour, and more real elegance, than the +furniture of Rosings. + +β€œAnd of this place,” thought she, β€œI might have been mistress! With +these rooms I might have now been familiarly acquainted! Instead of +viewing them as a stranger, I might have rejoiced in them as my own, and +welcomed to them as visitors my uncle and aunt. But, no,” recollecting +herself, β€œthat could never be; my uncle and aunt would have been lost to +me; I should not have been allowed to invite them.” + +This was a lucky recollection--it saved her from something like regret. + +She longed to inquire of the housekeeper whether her master were really +absent, but had not courage for it. At length, however, the question was +asked by her uncle; and she turned away with alarm, while Mrs. Reynolds +replied, that he was; adding, β€œBut we expect him to-morrow, with a large +party of friends.” How rejoiced was Elizabeth that their own journey had +not by any circumstance been delayed a day! + +Her aunt now called her to look at a picture. She approached, and saw +the likeness of Mr. Wickham, suspended, amongst several other +miniatures, over the mantel-piece. Her aunt asked her, smilingly, how +she liked it. The housekeeper came forward, and told them it was the +picture of a young gentleman, the son of her late master’s steward, who +had been brought up by him at his own expense. β€œHe is now gone into the +army,” she added; β€œbut I am afraid he has turned out very wild.” + +Mrs. Gardiner looked at her niece with a smile, but Elizabeth could not +return it. + +β€œAnd that,” said Mrs. Reynolds, pointing to another of the miniatures, +β€œis my master--and very like him. It was drawn at the same time as the +other--about eight years ago.” + +β€œI have heard much of your master’s fine person,” said Mrs. Gardiner, +looking at the picture; β€œit is a handsome face. But, Lizzy, you can tell +us whether it is like or not.” + +Mrs. Reynolds’ respect for Elizabeth seemed to increase on this +intimation of her knowing her master. + +β€œDoes that young lady know Mr. Darcy?” + +Elizabeth coloured, and said, β€œA little.” + +β€œAnd do not you think him a very handsome gentleman, ma’am?” + +β€œYes, very handsome.” + +β€œI am sure _I_ know none so handsome; but in the gallery upstairs you +will see a finer, larger picture of him than this. This room was my late +master’s favourite room, and these miniatures are just as they used to +be then. He was very fond of them.” + +This accounted to Elizabeth for Mr. Wickham’s being among them. + +Mrs. Reynolds then directed their attention to one of Miss Darcy, drawn +when she was only eight years old. + +β€œAnd is Miss Darcy as handsome as her brother?” said Mr. Gardiner. + +β€œOh, yes--the handsomest young lady that ever was seen; and so +accomplished! She plays and sings all day long. In the next room is a +new instrument just come down for her--a present from my master: she +comes here to-morrow with him.” + +Mr. Gardiner, whose manners were easy and pleasant, encouraged her +communicativeness by his questions and remarks: Mrs. Reynolds, either +from pride or attachment, had evidently great pleasure in talking of her +master and his sister. + +β€œIs your master much at Pemberley in the course of the year?” + +β€œNot so much as I could wish, sir: but I dare say he may spend half his +time here; and Miss Darcy is always down for the summer months.” + +β€œExcept,” thought Elizabeth, β€œwhen she goes to Ramsgate.” + +β€œIf your master would marry, you might see more of him.” + +β€œYes, sir; but I do not know when _that_ will be. I do not know who is +good enough for him.” + +Mr. and Mrs. Gardiner smiled. Elizabeth could not help saying, β€œIt is +very much to his credit, I am sure, that you should think so.” + +β€œI say no more than the truth, and what everybody will say that knows +him,” replied the other. Elizabeth thought this was going pretty far; +and she listened with increasing astonishment as the housekeeper added, +β€œI have never had a cross word from him in my life, and I have known him +ever since he was four years old.” + +This was praise of all others most extraordinary, most opposite to her +ideas. That he was not a good-tempered man had been her firmest opinion. +Her keenest attention was awakened: she longed to hear more; and was +grateful to her uncle for saying,-- + +β€œThere are very few people of whom so much can be said. You are lucky in +having such a master.” + +β€œYes, sir, I know I am. If I were to go through the world, I could not +meet with a better. But I have always observed, that they who are +good-natured when children, are good-natured when they grow up; and he +was always the sweetest tempered, most generous-hearted boy in the +world.” + +Elizabeth almost stared at her. β€œCan this be Mr. Darcy?” thought she. + +β€œHis father was an excellent man,” said Mrs. Gardiner. + +β€œYes, ma’am, that he was indeed; and his son will be just like him--just +as affable to the poor.” + +Elizabeth listened, wondered, doubted, and was impatient for more. Mrs. +Reynolds could interest her on no other point. She related the subjects +of the pictures, the dimensions of the rooms, and the price of the +furniture in vain. Mr. Gardiner, highly amused by the kind of family +prejudice, to which he attributed her excessive commendation of her +master, soon led again to the subject; and she dwelt with energy on his +many merits, as they proceeded together up the great staircase. + +β€œHe is the best landlord, and the best master,” said she, β€œthat ever +lived. Not like the wild young men now-a-days, who think of nothing but +themselves. There is not one of his tenants or servants but what will +give him a good name. Some people call him proud; but I am sure I never +saw anything of it. To my fancy, it is only because he does not rattle +away like other young men.” + +β€œIn what an amiable light does this place him!” thought Elizabeth. + +β€œThis fine account of him,” whispered her aunt as they walked, β€œis not +quite consistent with his behaviour to our poor friend.” + +β€œPerhaps we might be deceived.” + +β€œThat is not very likely; our authority was too good.” + +On reaching the spacious lobby above, they were shown into a very pretty +sitting-room, lately fitted up with greater elegance and lightness than +the apartments below; and were informed that it was but just done to +give pleasure to Miss Darcy, who had taken a liking to the room, when +last at Pemberley. + +β€œHe is certainly a good brother,” said Elizabeth, as she walked towards +one of the windows. + +Mrs. Reynolds anticipated Miss Darcy’s delight, when she should enter +the room. β€œAnd this is always the way with him,” she added. β€œWhatever +can give his sister any pleasure, is sure to be done in a moment. There +is nothing he would not do for her.” + +The picture gallery, and two or three of the principal bed-rooms, were +all that remained to be shown. In the former were many good paintings: +but Elizabeth knew nothing of the art; and from such as had been already +visible below, she had willingly turned to look at some drawings of Miss +Darcy’s, in crayons, whose subjects were usually more interesting, and +also more intelligible. + +In the gallery there were many family portraits, but they could have +little to fix the attention of a stranger. Elizabeth walked on in quest +of the only face whose features would be known to her. At last it +arrested her--and she beheld a striking resemblance of Mr. Darcy, with +such a smile over the face, as she remembered to have sometimes seen, +when he looked at her. She stood several minutes before the picture, in +earnest contemplation, and returned to it again before they quitted the +gallery. Mrs. Reynolds informed them, that it had been taken in his +father’s lifetime. + +There was certainly at this moment, in Elizabeth’s mind, a more gentle +sensation towards the original than she had ever felt in the height of +their acquaintance. The commendation bestowed on him by Mrs. Reynolds +was of no trifling nature. What praise is more valuable than the praise +of an intelligent servant? As a brother, a landlord, a master, she +considered how many people’s happiness were in his guardianship! How +much of pleasure or pain it was in his power to bestow! How much of good +or evil must be done by him! Every idea that had been brought forward by +the housekeeper was favourable to his character; and as she stood before +the canvas, on which he was represented, and fixed his eyes upon +herself, she thought of his regard with a deeper sentiment of gratitude +than it had ever raised before: she remembered its warmth, and softened +its impropriety of expression. + +When all of the house that was open to general inspection had been seen, +they returned down stairs; and, taking leave of the housekeeper, were +consigned over to the gardener, who met them at the hall door. + +As they walked across the lawn towards the river, Elizabeth turned back +to look again; her uncle and aunt stopped also; and while the former was +conjecturing as to the date of the building, the owner of it himself +suddenly came forward from the road which led behind it to the stables. + +They were within twenty yards of each other; and so abrupt was his +appearance, that it was impossible to avoid his sight. Their eyes +instantly met, and the cheeks of each were overspread with the deepest +blush. He absolutely started, and for a moment seemed immovable from +surprise; but shortly recovering himself, advanced towards the party, +and spoke to Elizabeth, if not in terms of perfect composure, at least +of perfect civility. + +She had instinctively turned away; but stopping on his approach, +received his compliments with an embarrassment impossible to be +overcome. Had his first appearance, or his resemblance to the picture +they had just been examining, been insufficient to assure the other two +that they now saw Mr. Darcy, the gardener’s expression of surprise, on +beholding his master, must immediately have told it. They stood a little +aloof while he was talking to their niece, who, astonished and confused, +scarcely dared lift her eyes to his face, and knew not what answer she +returned to his civil inquiries after her family. Amazed at the +alteration of his manner since they last parted, every sentence that he +uttered was increasing her embarrassment; and every idea of the +impropriety of her being found there recurring to her mind, the few +minutes in which they continued together were some of the most +uncomfortable of her life. Nor did he seem much more at ease; when he +spoke, his accent had none of its usual sedateness; and he repeated his +inquiries as to the time of her having left Longbourn, and of her stay +in Derbyshire, so often, and in so hurried a way, as plainly spoke the +distraction of his thoughts. + +At length, every idea seemed to fail him; and after standing a few +moments without saying a word, he suddenly recollected himself, and took +leave. + +The others then joined her, and expressed their admiration of his +figure; but Elizabeth heard not a word, and, wholly engrossed by her own +feelings, followed them in silence. She was overpowered by shame and +vexation. Her coming there was the most unfortunate, the most ill-judged +thing in the world! How strange must it appear to him! In what a +disgraceful light might it not strike so vain a man! It might seem as if +she had purposely thrown herself in his way again! Oh! why did she come? +or, why did he thus come a day before he was expected? Had they been +only ten minutes sooner, they should have been beyond the reach of his +discrimination; for it was plain that he was that moment arrived, that +moment alighted from his horse or his carriage. She blushed again and +again over the perverseness of the meeting. And his behaviour, so +strikingly altered,--what could it mean? That he should even speak to +her was amazing!--but to speak with such civility, to inquire after her +family! Never in her life had she seen his manners so little dignified, +never had he spoken with such gentleness as on this unexpected meeting. +What a contrast did it offer to his last address in Rosings Park, when +he put his letter into her hand! She knew not what to think, or how to +account for it. + +They had now entered a beautiful walk by the side of the water, and +every step was bringing forward a nobler fall of ground, or a finer +reach of the woods to which they were approaching: but it was some time +before Elizabeth was sensible of any of it; and, though she answered +mechanically to the repeated appeals of her uncle and aunt, and seemed +to direct her eyes to such objects as they pointed out, she +distinguished no part of the scene. Her thoughts were all fixed on that +one spot of Pemberley House, whichever it might be, where Mr. Darcy then +was. She longed to know what at that moment was passing in his mind; in +what manner he thought of her, and whether, in defiance of everything, +she was still dear to him. Perhaps he had been civil only because he +felt himself at ease; yet there had been _that_ in his voice, which was +not like ease. Whether he had felt more of pain or of pleasure in seeing +her, she could not tell, but he certainly had not seen her with +composure. + +At length, however, the remarks of her companions on her absence of mind +roused her, and she felt the necessity of appearing more like herself. + +They entered the woods, and, bidding adieu to the river for a while, +ascended some of the higher grounds; whence, in spots where the opening +of the trees gave the eye power to wander, were many charming views of +the valley, the opposite hills, with the long range of woods +overspreading many, and occasionally part of the stream. Mr. Gardiner +expressed a wish of going round the whole park, but feared it might be +beyond a walk. With a triumphant smile, they were told, that it was ten +miles round. It settled the matter; and they pursued the accustomed +circuit; which brought them again, after some time, in a descent among +hanging woods, to the edge of the water, and one of its narrowest parts. +They crossed it by a simple bridge, in character with the general air of +the scene: it was a spot less adorned than any they had yet visited; and +the valley, here contracted into a glen, allowed room only for the +stream, and a narrow walk amidst the rough coppice-wood which bordered +it. Elizabeth longed to explore its windings; but when they had crossed +the bridge, and perceived their distance from the house, Mrs. Gardiner, +who was not a great walker, could go no farther, and thought only of +returning to the carriage as quickly as possible. Her niece was, +therefore, obliged to submit, and they took their way towards the house +on the opposite side of the river, in the nearest direction; but their +progress was slow, for Mr. Gardiner, though seldom able to indulge the +taste, was very fond of fishing, and was so much engaged in watching the +occasional appearance of some trout in the water, and talking to the man +about them, that he advanced but little. Whilst wandering on in this +slow manner, they were again surprised, and Elizabeth’s astonishment was +quite equal to what it had been at first, by the sight of Mr. Darcy +approaching them, and at no great distance. The walk being here less +sheltered than on the other side, allowed them to see him before they +met. Elizabeth, however astonished, was at least more prepared for an +interview than before, and resolved to appear and to speak with +calmness, if he really intended to meet them. For a few moments, indeed, +she felt that he would probably strike into some other path. The idea +lasted while a turning in the walk concealed him from their view; the +turning past, he was immediately before them. With a glance she saw that +he had lost none of his recent civility; and, to imitate his politeness, +she began as they met to admire the beauty of the place; but she had not +got beyond the words β€œdelightful,” and β€œcharming,” when some unlucky +recollections obtruded, and she fancied that praise of Pemberley from +her might be mischievously construed. Her colour changed, and she said +no more. + +Mrs. Gardiner was standing a little behind; and on her pausing, he asked +her if she would do him the honour of introducing him to her friends. +This was a stroke of civility for which she was quite unprepared; and +she could hardly suppress a smile at his being now seeking the +acquaintance of some of those very people, against whom his pride had +revolted, in his offer to herself. β€œWhat will be his surprise,” thought +she, β€œwhen he knows who they are! He takes them now for people of +fashion.” + +The introduction, however, was immediately made; and as she named their +relationship to herself, she stole a sly look at him, to see how he bore +it; and was not without the expectation of his decamping as fast as he +could from such disgraceful companions. That he was _surprised_ by the +connection was evident: he sustained it, however, with fortitude: and, +so far from going away, turned back with them, and entered into +conversation with Mr. Gardiner. Elizabeth could not but be pleased, +could not but triumph. It was consoling that he should know she had some +relations for whom there was no need to blush. She listened most +attentively to all that passed between them, and gloried in every +expression, every sentence of her uncle, which marked his intelligence, +his taste, or his good manners. + +The conversation soon turned upon fishing; and she heard Mr. Darcy +invite him, with the greatest civility, to fish there as often as he +chose, while he continued in the neighbourhood, offering at the same +time to supply him with fishing tackle, and pointing out those parts of +the stream where there was usually most sport. Mrs. Gardiner, who was +walking arm in arm with Elizabeth, gave her a look expressive of her +wonder. Elizabeth said nothing, but it gratified her exceedingly; the +compliment must be all for herself. Her astonishment, however, was +extreme; and continually was she repeating, β€œWhy is he so altered? From +what can it proceed? It cannot be for _me_, it cannot be for _my_ sake +that his manners are thus softened. My reproofs at Hunsford could not +work such a change as this. It is impossible that he should still love +me.” + +After walking some time in this way, the two ladies in front, the two +gentlemen behind, on resuming their places, after descending to the +brink of the river for the better inspection of some curious +water-plant, there chanced to be a little alteration. It originated in +Mrs. Gardiner, who, fatigued by the exercise of the morning, found +Elizabeth’s arm inadequate to her support, and consequently preferred +her husband’s. Mr. Darcy took her place by her niece, and they walked on +together. After a short silence the lady first spoke. She wished him to +know that she had been assured of his absence before she came to the +place, and accordingly began by observing, that his arrival had been +very unexpected--β€œfor your housekeeper,” she added, β€œinformed us that +you would certainly not be here till to-morrow; and, indeed, before we +left Bakewell, we understood that you were not immediately expected in +the country.” He acknowledged the truth of it all; and said that +business with his steward had occasioned his coming forward a few hours +before the rest of the party with whom he had been travelling. β€œThey +will join me early to-morrow,” he continued, β€œand among them are some +who will claim an acquaintance with you,--Mr. Bingley and his sisters.” + +Elizabeth answered only by a slight bow. Her thoughts were instantly +driven back to the time when Mr. Bingley’s name had been last mentioned +between them; and if she might judge from his complexion, _his_ mind was +not very differently engaged. + +β€œThere is also one other person in the party,” he continued after a +pause, β€œwho more particularly wishes to be known to you. Will you allow +me, or do I ask too much, to introduce my sister to your acquaintance +during your stay at Lambton?” + +The surprise of such an application was great indeed; it was too great +for her to know in what manner she acceded to it. She immediately felt +that whatever desire Miss Darcy might have of being acquainted with her, +must be the work of her brother, and without looking farther, it was +satisfactory; it was gratifying to know that his resentment had not made +him think really ill of her. + +They now walked on in silence; each of them deep in thought. Elizabeth +was not comfortable; that was impossible; but she was flattered and +pleased. His wish of introducing his sister to her was a compliment of +the highest kind. They soon outstripped the others; and when they had +reached the carriage, Mr. and Mrs. Gardiner were half a quarter of a +mile behind. + +He then asked her to walk into the house--but she declared herself not +tired, and they stood together on the lawn. At such a time much might +have been said, and silence was very awkward. She wanted to talk, but +there seemed an embargo on every subject. At last she recollected that +she had been travelling, and they talked of Matlock and Dovedale with +great perseverance. Yet time and her aunt moved slowly--and her patience +and her ideas were nearly worn out before the _tΓͺte-Γ -tΓͺte_ was over. + +On Mr. and Mrs. Gardiner’s coming up they were all pressed to go into +the house and take some refreshment; but this was declined, and they +parted on each side with the utmost politeness. Mr. Darcy handed the +ladies into the carriage; and when it drove off, Elizabeth saw him +walking slowly towards the house. + +The observations of her uncle and aunt now began; and each of them +pronounced him to be infinitely superior to anything they had expected. + +β€œHe is perfectly well-behaved, polite, and unassuming,” said her uncle. + +β€œThere _is_ something a little stately in him, to be sure,” replied her +aunt; β€œbut it is confined to his air, and is not unbecoming. I can now +say with the housekeeper, that though some people may call him proud, +_I_ have seen nothing of it.” + +β€œI was never more surprised than by his behaviour to us. It was more +than civil; it was really attentive; and there was no necessity for such +attention. His acquaintance with Elizabeth was very trifling.” + +β€œTo be sure, Lizzy,” said her aunt, β€œhe is not so handsome as Wickham; +or rather he has not Wickham’s countenance, for his features are +perfectly good. But how came you to tell us that he was so +disagreeable?” + +Elizabeth excused herself as well as she could: said that she had liked +him better when they met in Kent than before, and that she had never +seen him so pleasant as this morning. + +β€œBut perhaps he may be a little whimsical in his civilities,” replied +her uncle. β€œYour great men often are; and therefore I shall not take him +at his word about fishing, as he might change his mind another day, and +warn me off his grounds.” + +Elizabeth felt that they had entirely mistaken his character, but said +nothing. + +β€œFrom what we have seen of him,” continued Mrs. Gardiner, β€œI really +should not have thought that he could have behaved in so cruel a way by +anybody as he has done by poor Wickham. He has not an ill-natured look. +On the contrary, there is something pleasing about his mouth when he +speaks. And there is something of dignity in his countenance, that would +not give one an unfavourable idea of his heart. But, to be sure, the +good lady who showed us the house did give him a most flaming character! +I could hardly help laughing aloud sometimes. But he is a liberal +master, I suppose, and _that_, in the eye of a servant, comprehends +every virtue.” + +Elizabeth here felt herself called on to say something in vindication of +his behaviour to Wickham; and, therefore, gave them to understand, in as +guarded a manner as she could, that by what she had heard from his +relations in Kent, his actions were capable of a very different +construction; and that his character was by no means so faulty, nor +Wickham’s so amiable, as they had been considered in Hertfordshire. In +confirmation of this, she related the particulars of all the pecuniary +transactions in which they had been connected, without actually naming +her authority, but stating it to be such as might be relied on. + +Mrs. Gardiner was surprised and concerned: but as they were now +approaching the scene of her former pleasures, every idea gave way to +the charm of recollection; and she was too much engaged in pointing out +to her husband all the interesting spots in its environs, to think of +anything else. Fatigued as she had been by the morning’s walk, they had +no sooner dined than she set off again in quest of her former +acquaintance, and the evening was spent in the satisfactions of an +intercourse renewed after many years’ discontinuance. + +The occurrences of the day were too full of interest to leave Elizabeth +much attention for any of these new friends; and she could do nothing +but think, and think with wonder, of Mr. Darcy’s civility, and, above +all, of his wishing her to be acquainted with his sister. + + + + +[Illustration] + + + + +CHAPTER XLIV. + + +[Illustration] + +Elizabeth had settled it that Mr. Darcy would bring his sister to visit +her the very day after her reaching Pemberley; and was, consequently, +resolved not to be out of sight of the inn the whole of that morning. +But her conclusion was false; for on the very morning after their own +arrival at Lambton these visitors came. They had been walking about the +place with some of their new friends, and were just returned to the inn +to dress themselves for dining with the same family, when the sound of a +carriage drew them to a window, and they saw a gentleman and lady in a +curricle driving up the street. Elizabeth, immediately recognizing the +livery, guessed what it meant, and imparted no small degree of surprise +to her relations, by acquainting them with the honour which she +expected. Her uncle and aunt were all amazement; and the embarrassment +of her manner as she spoke, joined to the circumstance itself, and many +of the circumstances of the preceding day, opened to them a new idea on +the business. Nothing had ever suggested it before, but they now felt +that there was no other way of accounting for such attentions from such +a quarter than by supposing a partiality for their niece. While these +newly-born notions were passing in their heads, the perturbation of +Elizabeth’s feelings was every moment increasing. She was quite amazed +at her own discomposure; but, amongst other causes of disquiet, she +dreaded lest the partiality of the brother should have said too much in +her favour; and, more than commonly anxious to please, she naturally +suspected that every power of pleasing would fail her. + +She retreated from the window, fearful of being seen; and as she walked +up and down the room, endeavouring to compose herself, saw such looks of +inquiring surprise in her uncle and aunt as made everything worse. + +Miss Darcy and her brother appeared, and this formidable introduction +took place. With astonishment did Elizabeth see that her new +acquaintance was at least as much embarrassed as herself. Since her +being at Lambton, she had heard that Miss Darcy was exceedingly proud; +but the observation of a very few minutes convinced her that she was +only exceedingly shy. She found it difficult to obtain even a word from +her beyond a monosyllable. + +Miss Darcy was tall, and on a larger scale than Elizabeth; and, though +little more than sixteen, her figure was formed, and her appearance +womanly and graceful. She was less handsome than her brother, but there +was sense and good-humour in her face, and her manners were perfectly +unassuming and gentle. Elizabeth, who had expected to find in her as +acute and unembarrassed an observer as ever Mr. Darcy had been, was much +relieved by discerning such different feelings. + +They had not been long together before Darcy told her that Bingley was +also coming to wait on her; and she had barely time to express her +satisfaction, and prepare for such a visitor, when Bingley’s quick step +was heard on the stairs, and in a moment he entered the room. All +Elizabeth’s anger against him had been long done away; but had she still +felt any, it could hardly have stood its ground against the unaffected +cordiality with which he expressed himself on seeing her again. He +inquired in a friendly, though general, way, after her family, and +looked and spoke with the same good-humoured ease that he had ever done. + +To Mr. and Mrs. Gardiner he was scarcely a less interesting personage +than to herself. They had long wished to see him. The whole party before +them, indeed, excited a lively attention. The suspicions which had just +arisen of Mr. Darcy and their niece, directed their observation towards +each with an earnest, though guarded, inquiry; and they soon drew from +those inquiries the full conviction that one of them at least knew what +it was to love. Of the lady’s sensations they remained a little in +doubt; but that the gentleman was overflowing with admiration was +evident enough. + +Elizabeth, on her side, had much to do. She wanted to ascertain the +feelings of each of her visitors, she wanted to compose her own, and to +make herself agreeable to all; and in the latter object, where she +feared most to fail, she was most sure of success, for those to whom +she endeavoured to give pleasure were pre-possessed in her favour. +Bingley was ready, Georgiana was eager, and Darcy determined, to be +pleased. + +[Illustration: + + β€œTo make herself agreeable to all” + +[_Copyright 1894 by George Allen._]] + +In seeing Bingley, her thoughts naturally flew to her sister; and oh! +how ardently did she long to know whether any of his were directed in a +like manner. Sometimes she could fancy that he talked less than on +former occasions, and once or twice pleased herself with the notion +that, as he looked at her, he was trying to trace a resemblance. But, +though this might be imaginary, she could not be deceived as to his +behaviour to Miss Darcy, who had been set up as a rival to Jane. No +look appeared on either side that spoke particular regard. Nothing +occurred between them that could justify the hopes of his sister. On +this point she was soon satisfied; and two or three little circumstances +occurred ere they parted, which, in her anxious interpretation, denoted +a recollection of Jane, not untinctured by tenderness, and a wish of +saying more that might lead to the mention of her, had he dared. He +observed to her, at a moment when the others were talking together, and +in a tone which had something of real regret, that it β€œwas a very long +time since he had had the pleasure of seeing her;” and, before she could +reply, he added, β€œIt is above eight months. We have not met since the +26th of November, when we were all dancing together at Netherfield.” + +Elizabeth was pleased to find his memory so exact; and he afterwards +took occasion to ask her, when unattended to by any of the rest, whether +_all_ her sisters were at Longbourn. There was not much in the question, +nor in the preceding remark; but there was a look and a manner which +gave them meaning. + +It was not often that she could turn her eyes on Mr. Darcy himself; but +whenever she did catch a glimpse she saw an expression of general +complaisance, and in all that he said, she heard an accent so far +removed from _hauteur_ or disdain of his companions, as convinced her +that the improvement of manners which she had yesterday witnessed, +however temporary its existence might prove, had at least outlived one +day. When she saw him thus seeking the acquaintance, and courting the +good opinion of people with whom any intercourse a few months ago would +have been a disgrace; when she saw him thus civil, not only to herself, +but to the very relations whom he had openly disdained, and recollected +their last lively scene in Hunsford Parsonage, the difference, the +change was so great, and struck so forcibly on her mind, that she could +hardly restrain her astonishment from being visible. Never, even in the +company of his dear friends at Netherfield, or his dignified relations +at Rosings, had she seen him so desirous to please, so free from +self-consequence or unbending reserve, as now, when no importance could +result from the success of his endeavours, and when even the +acquaintance of those to whom his attentions were addressed, would draw +down the ridicule and censure of the ladies both of Netherfield and +Rosings. + +Their visitors stayed with them above half an hour; and when they arose +to depart, Mr. Darcy called on his sister to join him in expressing +their wish of seeing Mr. and Mrs. Gardiner, and Miss Bennet, to dinner +at Pemberley, before they left the country. Miss Darcy, though with a +diffidence which marked her little in the habit of giving invitations, +readily obeyed. Mrs. Gardiner looked at her niece, desirous of knowing +how _she_, whom the invitation most concerned, felt disposed as to its +acceptance, but Elizabeth had turned away her head. Presuming, however, +that this studied avoidance spoke rather a momentary embarrassment than +any dislike of the proposal, and seeing in her husband, who was fond of +society, a perfect willingness to accept it, she ventured to engage for +her attendance, and the day after the next was fixed on. + +Bingley expressed great pleasure in the certainty of seeing Elizabeth +again, having still a great deal to say to her, and many inquiries to +make after all their Hertfordshire friends. Elizabeth, construing all +this into a wish of hearing her speak of her sister, was pleased; and +on this account, as well as some others, found herself, when their +visitors left them, capable of considering the last half hour with some +satisfaction, though while it was passing the enjoyment of it had been +little. Eager to be alone, and fearful of inquiries or hints from her +uncle and aunt, she stayed with them only long enough to hear their +favourable opinion of Bingley, and then hurried away to dress. + +But she had no reason to fear Mr. and Mrs. Gardiner’s curiosity; it was +not their wish to force her communication. It was evident that she was +much better acquainted with Mr. Darcy than they had before any idea of; +it was evident that he was very much in love with her. They saw much to +interest, but nothing to justify inquiry. + +Of Mr. Darcy it was now a matter of anxiety to think well; and, as far +as their acquaintance reached, there was no fault to find. They could +not be untouched by his politeness; and had they drawn his character +from their own feelings and his servant’s report, without any reference +to any other account, the circle in Hertfordshire to which he was known +would not have recognized it for Mr. Darcy. There was now an interest, +however, in believing the housekeeper; and they soon became sensible +that the authority of a servant, who had known him since he was four +years old, and whose own manners indicated respectability, was not to be +hastily rejected. Neither had anything occurred in the intelligence of +their Lambton friends that could materially lessen its weight. They had +nothing to accuse him of but pride; pride he probably had, and if not, +it would certainly be imputed by the inhabitants of a small market town +where the family did not visit. It was acknowledged, however, that he +was a liberal man, and did much good among the poor. + +With respect to Wickham, the travellers soon found that he was not held +there in much estimation; for though the chief of his concerns with the +son of his patron were imperfectly understood, it was yet a well-known +fact that, on his quitting Derbyshire, he had left many debts behind +him, which Mr. Darcy afterwards discharged. + +As for Elizabeth, her thoughts were at Pemberley this evening more than +the last; and the evening, though as it passed it seemed long, was not +long enough to determine her feelings towards _one_ in that mansion; and +she lay awake two whole hours, endeavouring to make them out. She +certainly did not hate him. No; hatred had vanished long ago, and she +had almost as long been ashamed of ever feeling a dislike against him, +that could be so called. The respect created by the conviction of his +valuable qualities, though at first unwillingly admitted, had for some +time ceased to be repugnant to her feelings; and it was now heightened +into somewhat of a friendlier nature by the testimony so highly in his +favour, and bringing forward his disposition in so amiable a light, +which yesterday had produced. But above all, above respect and esteem, +there was a motive within her of good-will which could not be +overlooked. It was gratitude;--gratitude, not merely for having once +loved her, but for loving her still well enough to forgive all the +petulance and acrimony of her manner in rejecting him, and all the +unjust accusations accompanying her rejection. He who, she had been +persuaded, would avoid her as his greatest enemy, seemed, on this +accidental meeting, most eager to preserve the acquaintance; and +without any indelicate display of regard, or any peculiarity of manner, +where their two selves only were concerned, was soliciting the good +opinion of her friends, and bent on making her known to his sister. Such +a change in a man of so much pride excited not only astonishment but +gratitude--for to love, ardent love, it must be attributed; and, as +such, its impression on her was of a sort to be encouraged, as by no +means unpleasing, though it could not be exactly defined. She respected, +she esteemed, she was grateful to him, she felt a real interest in his +welfare; and she only wanted to know how far she wished that welfare to +depend upon herself, and how far it would be for the happiness of both +that she should employ the power, which her fancy told her she still +possessed, of bringing on the renewal of his addresses. + +It had been settled in the evening, between the aunt and niece, that +such a striking civility as Miss Darcy’s, in coming to them on the very +day of her arrival at Pemberley--for she had reached it only to a late +breakfast--ought to be imitated, though it could not be equalled, by +some exertion of politeness on their side; and, consequently, that it +would be highly expedient to wait on her at Pemberley the following +morning. They were, therefore, to go. Elizabeth was pleased; though when +she asked herself the reason, she had very little to say in reply. + +Mr. Gardiner left them soon after breakfast. The fishing scheme had been +renewed the day before, and a positive engagement made of his meeting +some of the gentlemen at Pemberley by noon. + + + + +[Illustration: + + β€œEngaged by the river” +] + + + + +CHAPTER XLV. + + +[Illustration] + +Convinced as Elizabeth now was that Miss Bingley’s dislike of her had +originated in jealousy, she could not help feeling how very unwelcome +her appearance at Pemberley must be to her, and was curious to know +with how much civility on that lady’s side the acquaintance would now +be renewed. + +On reaching the house, they were shown through the hall into the saloon, +whose northern aspect rendered it delightful for summer. Its windows, +opening to the ground, admitted a most refreshing view of the high woody +hills behind the house, and of the beautiful oaks and Spanish chestnuts +which were scattered over the intermediate lawn. + +In this room they were received by Miss Darcy, who was sitting there +with Mrs. Hurst and Miss Bingley, and the lady with whom she lived in +London. Georgiana’s reception of them was very civil, but attended with +all that embarrassment which, though proceeding from shyness and the +fear of doing wrong, would easily give to those who felt themselves +inferior the belief of her being proud and reserved. Mrs. Gardiner and +her niece, however, did her justice, and pitied her. + +By Mrs. Hurst and Miss Bingley they were noticed only by a courtesy; and +on their being seated, a pause, awkward as such pauses must always be, +succeeded for a few moments. It was first broken by Mrs. Annesley, a +genteel, agreeable-looking woman, whose endeavour to introduce some kind +of discourse proved her to be more truly well-bred than either of the +others; and between her and Mrs. Gardiner, with occasional help from +Elizabeth, the conversation was carried on. Miss Darcy looked as if she +wished for courage enough to join in it; and sometimes did venture a +short sentence, when there was least danger of its being heard. + +Elizabeth soon saw that she was herself closely watched by Miss Bingley, +and that she could not speak a word, especially to Miss Darcy, without +calling her attention. This observation would not have prevented her +from trying to talk to the latter, had they not been seated at an +inconvenient distance; but she was not sorry to be spared the necessity +of saying much: her own thoughts were employing her. She expected every +moment that some of the gentlemen would enter the room: she wished, she +feared, that the master of the house might be amongst them; and whether +she wished or feared it most, she could scarcely determine. After +sitting in this manner a quarter of an hour, without hearing Miss +Bingley’s voice, Elizabeth was roused by receiving from her a cold +inquiry after the health of her family. She answered with equal +indifference and brevity, and the other said no more. + +The next variation which their visit afforded was produced by the +entrance of servants with cold meat, cake, and a variety of all the +finest fruits in season; but this did not take place till after many a +significant look and smile from Mrs. Annesley to Miss Darcy had been +given, to remind her of her post. There was now employment for the whole +party; for though they could not all talk, they could all eat; and the +beautiful pyramids of grapes, nectarines, and peaches, soon collected +them round the table. + +While thus engaged, Elizabeth had a fair opportunity of deciding whether +she most feared or wished for the appearance of Mr. Darcy, by the +feelings which prevailed on his entering the room; and then, though but +a moment before she had believed her wishes to predominate, she began to +regret that he came. + +He had been some time with Mr. Gardiner, who, with two or three other +gentlemen from the house, was engaged by the river; and had left him +only on learning that the ladies of the family intended a visit to +Georgiana that morning. No sooner did he appear, than Elizabeth wisely +resolved to be perfectly easy and unembarrassed;--a resolution the more +necessary to be made, but perhaps not the more easily kept, because she +saw that the suspicions of the whole party were awakened against them, +and that there was scarcely an eye which did not watch his behaviour +when he first came into the room. In no countenance was attentive +curiosity so strongly marked as in Miss Bingley’s, in spite of the +smiles which overspread her face whenever she spoke to one of its +objects; for jealousy had not yet made her desperate, and her attentions +to Mr. Darcy were by no means over. Miss Darcy, on her brother’s +entrance, exerted herself much more to talk; and Elizabeth saw that he +was anxious for his sister and herself to get acquainted, and forwarded, +as much as possible, every attempt at conversation on either side. Miss +Bingley saw all this likewise; and, in the imprudence of anger, took the +first opportunity of saying, with sneering civility,-- + +β€œPray, Miss Eliza, are not the ----shire militia removed from Meryton? +They must be a great loss to _your_ family.” + +In Darcy’s presence she dared not mention Wickham’s name: but Elizabeth +instantly comprehended that he was uppermost in her thoughts; and the +various recollections connected with him gave her a moment’s distress; +but, exerting herself vigorously to repel the ill-natured attack, she +presently answered the question in a tolerably disengaged tone. While +she spoke, an involuntary glance showed her Darcy with a heightened +complexion, earnestly looking at her, and his sister overcome with +confusion, and unable to lift up her eyes. Had Miss Bingley known what +pain she was then giving her beloved friend, she undoubtedly would have +refrained from the hint; but she had merely intended to discompose +Elizabeth, by bringing forward the idea of a man to whom she believed +her partial, to make her betray a sensibility which might injure her in +Darcy’s opinion, and, perhaps, to remind the latter of all the follies +and absurdities by which some part of her family were connected with +that corps. Not a syllable had ever reached her of Miss Darcy’s +meditated elopement. To no creature had it been revealed, where secrecy +was possible, except to Elizabeth; and from all Bingley’s connections +her brother was particularly anxious to conceal it, from that very wish +which Elizabeth had long ago attributed to him, of their becoming +hereafter her own. He had certainly formed such a plan; and without +meaning that it should affect his endeavour to separate him from Miss +Bennet, it is probable that it might add something to his lively concern +for the welfare of his friend. + +Elizabeth’s collected behaviour, however, soon quieted his emotion; and +as Miss Bingley, vexed and disappointed, dared not approach nearer to +Wickham, Georgiana also recovered in time, though not enough to be able +to speak any more. Her brother, whose eye she feared to meet, scarcely +recollected her interest in the affair; and the very circumstance which +had been designed to turn his thoughts from Elizabeth, seemed to have +fixed them on her more and more cheerfully. + +Their visit did not continue long after the question and answer above +mentioned; and while Mr. Darcy was attending them to their carriage, +Miss Bingley was venting her feelings in criticisms on Elizabeth’s +person, behaviour, and dress. But Georgiana would not join her. Her +brother’s recommendation was enough to insure her favour: his judgment +could not err; and he had spoken in such terms of Elizabeth, as to leave +Georgiana without the power of finding her otherwise than lovely and +amiable. When Darcy returned to the saloon, Miss Bingley could not help +repeating to him some part of what she had been saying to his sister. + +β€œHow very ill Eliza Bennet looks this morning, Mr. Darcy,” she cried: β€œI +never in my life saw anyone so much altered as she is since the winter. +She is grown so brown and coarse! Louisa and I were agreeing that we +should not have known her again.” + +However little Mr. Darcy might have liked such an address, he contented +himself with coolly replying, that he perceived no other alteration than +her being rather tanned,--no miraculous consequence of travelling in the +summer. + +β€œFor my own part,” she rejoined, β€œI must confess that I never could see +any beauty in her. Her face is too thin; her complexion has no +brilliancy; and her features are not at all handsome. Her nose wants +character; there is nothing marked in its lines. Her teeth are +tolerable, but not out of the common way; and as for her eyes, which +have sometimes been called so fine, I never could perceive anything +extraordinary in them. They have a sharp, shrewish look, which I do not +like at all; and in her air altogether, there is a self-sufficiency +without fashion, which is intolerable.” + +Persuaded as Miss Bingley was that Darcy admired Elizabeth, this was not +the best method of recommending herself; but angry people are not always +wise; and in seeing him at last look somewhat nettled, she had all the +success she expected. He was resolutely silent, however; and, from a +determination of making him speak, she continued,-- + +β€œI remember, when we first knew her in Hertfordshire, how amazed we all +were to find that she was a reputed beauty; and I particularly recollect +your saying one night, after they had been dining at Netherfield, β€˜_She_ +a beauty! I should as soon call her mother a wit.’ But afterwards she +seemed to improve on you, and I believe you thought her rather pretty at +one time.” + +β€œYes,” replied Darcy, who could contain himself no longer, β€œbut _that_ +was only when I first knew her; for it is many months since I have +considered her as one of the handsomest women of my acquaintance.” + +He then went away, and Miss Bingley was left to all the satisfaction of +having forced him to say what gave no one any pain but herself. + +Mrs. Gardiner and Elizabeth talked of all that had occurred during their +visit, as they returned, except what had particularly interested them +both. The looks and behaviour of everybody they had seen were discussed, +except of the person who had mostly engaged their attention. They talked +of his sister, his friends, his house, his fruit, of everything but +himself; yet Elizabeth was longing to know what Mrs. Gardiner thought of +him, and Mrs. Gardiner would have been highly gratified by her niece’s +beginning the subject. + + + + +[Illustration] + + + + +Chapter XLVI. + + +[Illustration] + +Elizabeth had been a good deal disappointed in not finding a letter from +Jane on their first arrival at Lambton; and this disappointment had been +renewed on each of the mornings that had now been spent there; but on +the third her repining was over, and her sister justified, by the +receipt of two letters from her at once, on one of which was marked that +it had been mis-sent elsewhere. Elizabeth was not surprised at it, as +Jane had written the direction remarkably ill. + +They had just been preparing to walk as the letters came in; and her +uncle and aunt, leaving her to enjoy them in quiet, set off by +themselves. The one mis-sent must be first attended to; it had been +written five days ago. The beginning contained an account of all their +little parties and engagements, with such news as the country afforded; +but the latter half, which was dated a day later, and written in evident +agitation, gave more important intelligence. It was to this effect:-- + +β€œSince writing the above, dearest Lizzy, something has occurred of a +most unexpected and serious nature; but I am afraid of alarming you--be +assured that we are all well. What I have to say relates to poor Lydia. +An express came at twelve last night, just as we were all gone to bed, +from Colonel Forster, to inform us that she was gone off to Scotland +with one of his officers; to own the truth, with Wickham! Imagine our +surprise. To Kitty, however, it does not seem so wholly unexpected. I am +very, very sorry. So imprudent a match on both sides! But I am willing +to hope the best, and that his character has been misunderstood. +Thoughtless and indiscreet I can easily believe him, but this step (and +let us rejoice over it) marks nothing bad at heart. His choice is +disinterested at least, for he must know my father can give her nothing. +Our poor mother is sadly grieved. My father bears it better. How +thankful am I, that we never let them know what has been said against +him; we must forget it ourselves. They were off Saturday night about +twelve, as is conjectured, but were not missed till yesterday morning at +eight. The express was sent off directly. My dear Lizzy, they must have +passed within ten miles of us. Colonel Forster gives us reason to expect +him here soon. Lydia left a few lines for his wife, informing her of +their intention. I must conclude, for I cannot be long from my poor +mother. I am afraid you will not be able to make it out, but I hardly +know what I have written.” + +Without allowing herself time for consideration, and scarcely knowing +what she felt, Elizabeth, on finishing this letter, instantly seized the +other, and opening it with the utmost impatience, read as follows: it +had been written a day later than the conclusion of the first. + +β€œBy this time, my dearest sister, you have received my hurried letter; I +wish this may be more intelligible, but though not confined for time, my +head is so bewildered that I cannot answer for being coherent. Dearest +Lizzy, I hardly know what I would write, but I have bad news for you, +and it cannot be delayed. Imprudent as a marriage between Mr. Wickham +and our poor Lydia would be, we are now anxious to be assured it has +taken place, for there is but too much reason to fear they are not gone +to Scotland. Colonel Forster came yesterday, having left Brighton the +day before, not many hours after the express. Though Lydia’s short +letter to Mrs. F. gave them to understand that they were going to Gretna +Green, something was dropped by Denny expressing his belief that W. +never intended to go there, or to marry Lydia at all, which was repeated +to Colonel F., who, instantly taking the alarm, set off from B., +intending to trace their route. He did trace them easily to Clapham, but +no farther; for on entering that place, they removed into a +hackney-coach, and dismissed the chaise that brought them from Epsom. +All that is known after this is, that they were seen to continue the +London road. I know not what to think. After making every possible +inquiry on that side of London, Colonel F. came on into Hertfordshire, +anxiously renewing them at all the turnpikes, and at the inns in Barnet +and Hatfield, but without any success,--no such people had been seen to +pass through. With the kindest concern he came on to Longbourn, and +broke his apprehensions to us in a manner most creditable to his heart. +I am sincerely grieved for him and Mrs. F.; but no one can throw any +blame on them. Our distress, my dear Lizzy, is very great. My father and +mother believe the worst, but I cannot think so ill of him. Many +circumstances might make it more eligible for them to be married +privately in town than to pursue their first plan; and even if _he_ +could form such a design against a young woman of Lydia’s connections, +which is not likely, can I suppose her so lost to everything? +Impossible! I grieve to find, however, that Colonel F. is not disposed +to depend upon their marriage: he shook his head when I expressed my +hopes, and said he feared W. was not a man to be trusted. My poor mother +is really ill, and keeps her room. Could she exert herself, it would be +better, but this is not to be expected; and as to my father, I never in +my life saw him so affected. Poor Kitty has anger for having concealed +their attachment; but as it was a matter of confidence, one cannot +wonder. I am truly glad, dearest Lizzy, that you have been spared +something of these distressing scenes; but now, as the first shock is +over, shall I own that I long for your return? I am not so selfish, +however, as to press for it, if inconvenient. Adieu! I take up my pen +again to do, what I have just told you I would not; but circumstances +are such, that I cannot help earnestly begging you all to come here as +soon as possible. I know my dear uncle and aunt so well, that I am not +afraid of requesting it, though I have still something more to ask of +the former. My father is going to London with Colonel Forster instantly, +to try to discover her. What he means to do, I am sure I know not; but +his excessive distress will not allow him to pursue any measure in the +best and safest way, and Colonel Forster is obliged to be at Brighton +again to-morrow evening. In such an exigence my uncle’s advice and +assistance would be everything in the world; he will immediately +comprehend what I must feel, and I rely upon his goodness.” + +β€œOh! where, where is my uncle?” cried Elizabeth, darting from her seat +as she finished the letter, in eagerness to follow him, without losing a +moment of the time so precious; but as she reached the door, it was +opened by a servant, and Mr. Darcy appeared. Her pale face and +impetuous manner made him start, and before he could recover himself +enough to speak, she, in whose mind every idea was superseded by Lydia’s +situation, hastily exclaimed, β€œI beg your pardon, but I must leave you. +I must find Mr. Gardiner this moment on business that cannot be delayed; +I have not an instant to lose.” + +β€œGood God! what is the matter?” cried he, with more feeling than +politeness; then recollecting himself, β€œI will not detain you a minute; +but let me, or let the servant, go after Mr. and Mrs. Gardiner. You are +not well enough; you cannot go yourself.” + +Elizabeth hesitated; but her knees trembled under her, and she felt how +little would be gained by her attempting to pursue them. Calling back +the servant, therefore, she commissioned him, though in so breathless an +accent as made her almost unintelligible, to fetch his master and +mistress home instantly. + +On his quitting the room, she sat down, unable to support herself, and +looking so miserably ill, that it was impossible for Darcy to leave her, +or to refrain from saying, in a tone of gentleness and commiseration, +β€œLet me call your maid. Is there nothing you could take to give you +present relief? A glass of wine; shall I get you one? You are very ill.” + +β€œNo, I thank you,” she replied, endeavouring to recover herself. β€œThere +is nothing the matter with me. I am quite well, I am only distressed by +some dreadful news which I have just received from Longbourn.” + +She burst into tears as she alluded to it, and for a few minutes could +not speak another word. Darcy, in wretched suspense, could only say +something indistinctly of his + +[Illustration: + + β€œI have not an instant to lose” +] + +concern, and observe her in compassionate silence. At length she spoke +again. β€œI have just had a letter from Jane, with such dreadful news. It +cannot be concealed from anyone. My youngest sister has left all her +friends--has eloped; has thrown herself into the power of--of Mr. +Wickham. They are gone off together from Brighton. _You_ know him too +well to doubt the rest. She has no money, no connections, nothing that +can tempt him to--she is lost for ever.” + +Darcy was fixed in astonishment. + +β€œWhen I consider,” she added, in a yet more agitated voice, β€œthat _I_ +might have prevented it! _I_ who knew what he was. Had I but explained +some part of it only--some part of what I learnt, to my own family! Had +his character been known, this could not have happened. But it is all, +all too late now.” + +β€œI am grieved, indeed,” cried Darcy: β€œgrieved--shocked. But is it +certain, absolutely certain?” + +β€œOh, yes! They left Brighton together on Sunday night, and were traced +almost to London, but not beyond: they are certainly not gone to +Scotland.” + +β€œAnd what has been done, what has been attempted, to recover her?” + +β€œMy father has gone to London, and Jane has written to beg my uncle’s +immediate assistance, and we shall be off, I hope, in half an hour. But +nothing can be done; I know very well that nothing can be done. How is +such a man to be worked on? How are they even to be discovered? I have +not the smallest hope. It is every way horrible!” + +Darcy shook his head in silent acquiescence. + +β€œWhen _my_ eyes were opened to his real character, oh! had I known what +I ought, what I dared to do! But I knew not--I was afraid of doing too +much. Wretched, wretched mistake!” + +Darcy made no answer. He seemed scarcely to hear her, and was walking up +and down the room in earnest meditation; his brow contracted, his air +gloomy. Elizabeth soon observed, and instantly understood it. Her power +was sinking; everything _must_ sink under such a proof of family +weakness, such an assurance of the deepest disgrace. She could neither +wonder nor condemn; but the belief of his self-conquest brought nothing +consolatory to her bosom, afforded no palliation of her distress. It +was, on the contrary, exactly calculated to make her understand her own +wishes; and never had she so honestly felt that she could have loved +him, as now, when all love must be vain. + +But self, though it would intrude, could not engross her. Lydia--the +humiliation, the misery she was bringing on them all--soon swallowed up +every private care; and covering her face with her handkerchief, +Elizabeth was soon lost to everything else; and, after a pause of +several minutes, was only recalled to a sense of her situation by the +voice of her companion, who, in a manner which, though it spoke +compassion, spoke likewise restraint, said,-- + +β€œI am afraid you have been long desiring my absence, nor have I anything +to plead in excuse of my stay, but real, though unavailing concern. +Would to Heaven that anything could be either said or done on my part, +that might offer consolation to such distress! But I will not torment +you with vain wishes, which may seem purposely to ask for your thanks. +This unfortunate affair will, I fear, prevent my sister’s having the +pleasure of seeing you at Pemberley to-day.” + +β€œOh, yes! Be so kind as to apologize for us to Miss Darcy. Say that +urgent business calls us home immediately. Conceal the unhappy truth as +long as it is possible. I know it cannot be long.” + +He readily assured her of his secrecy, again expressed his sorrow for +her distress, wished it a happier conclusion than there was at present +reason to hope, and, leaving his compliments for her relations, with +only one serious parting look, went away. + +As he quitted the room, Elizabeth felt how improbable it was that they +should ever see each other again on such terms of cordiality as had +marked their several meetings in Derbyshire; and as she threw a +retrospective glance over the whole of their acquaintance, so full of +contradictions and varieties, sighed at the perverseness of those +feelings which would now have promoted its continuance, and would +formerly have rejoiced in its termination. + +If gratitude and esteem are good foundations of affection, Elizabeth’s +change of sentiment will be neither improbable nor faulty. But if +otherwise, if the regard springing from such sources is unreasonable or +unnatural, in comparison of what is so often described as arising on a +first interview with its object, and even before two words have been +exchanged, nothing can be said in her defence, except that she had given +somewhat of a trial to the latter method, in her partiality for Wickham, +and that its ill success might, perhaps, authorize her to seek the other +less interesting mode of attachment. Be that as it may, she saw him go +with regret; and in this early example of what Lydia’s infamy must +produce, found additional anguish as she reflected on that wretched +business. Never since reading Jane’s second letter had she entertained a +hope of Wickham’s meaning to marry her. No one but Jane, she thought, +could flatter herself with such an expectation. Surprise was the least +of all her feelings on this development. While the contents of the first +letter remained on her mind, she was all surprise, all astonishment, +that Wickham should marry a girl whom it was impossible he could marry +for money; and how Lydia could ever have attached him had appeared +incomprehensible. But now it was all too natural. For such an attachment +as this, she might have sufficient charms; and though she did not +suppose Lydia to be deliberately engaging in an elopement, without the +intention of marriage, she had no difficulty in believing that neither +her virtue nor her understanding would preserve her from falling an easy +prey. + +She had never perceived, while the regiment was in Hertfordshire, that +Lydia had any partiality for him; but she was convinced that Lydia had +wanted only encouragement to attach herself to anybody. Sometimes one +officer, sometimes another, had been her favourite, as their attentions +raised them in her opinion. Her affections had been continually +fluctuating, but never without an object. The mischief of neglect and +mistaken indulgence towards such a girl--oh! how acutely did she now +feel it! + +She was wild to be at home--to hear, to see, to be upon the spot to +share with Jane in the cares that must now fall wholly upon her, in a +family so deranged; a father absent, a mother incapable of exertion, and +requiring constant attendance; and though almost persuaded that nothing +could be done for Lydia, her uncle’s interference seemed of the utmost +importance, and till he entered the room the misery of her impatience +was severe. Mr. and Mrs. Gardiner had hurried back in alarm, supposing, +by the servant’s account, that their niece was taken suddenly ill; but +satisfying them instantly on that head, she eagerly communicated the +cause of their summons, reading the two letters aloud, and dwelling on +the postscript of the last with trembling energy. Though Lydia had never +been a favourite with them, Mr. and Mrs. Gardiner could not but be +deeply affected. Not Lydia only, but all were concerned in it; and after +the first exclamations of surprise and horror, Mr. Gardiner readily +promised every assistance in his power. Elizabeth, though expecting no +less, thanked him with tears of gratitude; and all three being actuated +by one spirit, everything relating to their journey was speedily +settled. They were to be off as soon as possible. β€œBut what is to be +done about Pemberley?” cried Mrs. Gardiner. β€œJohn told us Mr. Darcy was +here when you sent for us;--was it so?” + +β€œYes; and I told him we should not be able to keep our engagement. +_That_ is all settled.” + +β€œWhat is all settled?” repeated the other, as she ran into her room to +prepare. β€œAnd are they upon such terms as for her to disclose the real +truth? Oh, that I knew how it was!” + +But wishes were vain; or, at best, could serve only to amuse her in the +hurry and confusion of the following hour. Had Elizabeth been at leisure +to be idle, she would have remained certain that all employment was +impossible to one so wretched as herself; but she had her share of +business as well as her aunt, and amongst the rest there were notes to +be written to all their friends at Lambton, with false excuses for their +sudden departure. An hour, however, saw the whole completed; and Mr. +Gardiner, meanwhile, having settled his account at the inn, nothing +remained to be done but to go; and Elizabeth, after all the misery of +the morning, found herself, in a shorter space of time than she could +have supposed, seated in the carriage, and on the road to Longbourn. + + + + +[Illustration: + + β€œThe first pleasing earnest of their welcome” +] + + + + +CHAPTER XLVII. + + +[Illustration] + +β€œI have been thinking it over again, Elizabeth,” said her uncle, as they +drove from the town; β€œand really, upon serious consideration, I am much +more inclined than I was to judge as your eldest sister does of the +matter. It appears to me so very unlikely that any young man should form +such a design against a girl who is by no means unprotected or +friendless, and who was actually staying in his Colonel’s family, that I +am strongly inclined to hope the best. Could he expect that her friends +would not step forward? Could he expect to be noticed again by the +regiment, after such an affront to Colonel Forster? His temptation is +not adequate to the risk.” + +β€œDo you really think so?” cried Elizabeth, brightening up for a moment. + +β€œUpon my word,” said Mrs. Gardiner, β€œI begin to be of your uncle’s +opinion. It is really too great a violation of decency, honour, and +interest, for him to be guilty of it. I cannot think so very ill of +Wickham. Can you, yourself, Lizzie, so wholly give him up, as to believe +him capable of it?” + +β€œNot perhaps of neglecting his own interest. But of every other neglect +I can believe him capable. If, indeed, it should be so! But I dare not +hope it. Why should they not go on to Scotland, if that had been the +case?” + +β€œIn the first place,” replied Mr. Gardiner, β€œthere is no absolute proof +that they are not gone to Scotland.” + +β€œOh, but their removing from the chaise into a hackney coach is such a +presumption! And, besides, no traces of them were to be found on the +Barnet road.” + +β€œWell, then,--supposing them to be in London--they may be there, though +for the purpose of concealment, for no more exceptionable purpose. It is +not likely that money should be very abundant on either side; and it +might strike them that they could be more economically, though less +expeditiously, married in London, than in Scotland.” + +β€œBut why all this secrecy? Why any fear of detection? Why must their +marriage be private? Oh, no, no--this is not likely. His most particular +friend, you see by Jane’s account, was persuaded of his never intending +to marry her. Wickham will never marry a woman without some money. He +cannot afford it. And what claims has Lydia, what attractions has she +beyond youth, health, and good humour, that could make him for her sake +forego every chance of benefiting himself by marrying well? As to what +restraint the apprehensions of disgrace in the corps might throw on a +dishonourable elopement with her, I am not able to judge; for I know +nothing of the effects that such a step might produce. But as to your +other objection, I am afraid it will hardly hold good. Lydia has no +brothers to step forward; and he might imagine, from my father’s +behaviour, from his indolence and the little attention he has ever +seemed to give to what was going forward in his family, that _he_ would +do as little and think as little about it, as any father could do, in +such a matter.” + +β€œBut can you think that Lydia is so lost to everything but love of him, +as to consent to live with him on any other terms than marriage?” + +β€œIt does seem, and it is most shocking, indeed,” replied Elizabeth, with +tears in her eyes, β€œthat a sister’s sense of decency and virtue in such +a point should admit of doubt. But, really, I know not what to say. +Perhaps I am not doing her justice. But she is very young: she has never +been taught to think on serious subjects; and for the last half year, +nay, for a twelvemonth, she has been given up to nothing but amusement +and vanity. She has been allowed to dispose of her time in the most idle +and frivolous manner, and to adopt any opinions that came in her way. +Since the ----shire were first quartered in Meryton, nothing but love, +flirtation, and officers, have been in her head. She has been doing +everything in her power, by thinking and talking on the subject, to give +greater--what shall I call it?--susceptibility to her feelings; which +are naturally lively enough. And we all know that Wickham has every +charm of person and address that can captivate a woman.” + +β€œBut you see that Jane,” said her aunt, β€œdoes not think so ill of +Wickham, as to believe him capable of the attempt.” + +β€œOf whom does Jane ever think ill? And who is there, whatever might be +their former conduct, that she would believe capable of such an attempt, +till it were proved against them? But Jane knows, as well as I do, what +Wickham really is. We both know that he has been profligate in every +sense of the word; that he has neither integrity nor honour; that he is +as false and deceitful as he is insinuating.” + +β€œAnd do you really know all this?” cried Mrs. Gardiner, whose curiosity +as to the mode of her intelligence was all alive. + +β€œI do, indeed,” replied Elizabeth, colouring. β€œI told you the other day +of his infamous behaviour to Mr. Darcy; and you, yourself, when last at +Longbourn, heard in what manner he spoke of the man who had behaved with +such forbearance and liberality towards him. And there are other +circumstances which I am not at liberty--which it is not worth while to +relate; but his lies about the whole Pemberley family are endless. From +what he said of Miss Darcy, I was thoroughly prepared to see a proud, +reserved, disagreeable girl. Yet he knew to the contrary himself. He +must know that she was as amiable and unpretending as we have found +her.” + +β€œBut does Lydia know nothing of this? can she be ignorant of what you +and Jane seem so well to understand?” + +β€œOh, yes!--that, that is the worst of all. Till I was in Kent, and saw +so much both of Mr. Darcy and his relation Colonel Fitzwilliam, I was +ignorant of the truth myself. And when I returned home the ----shire +was to leave Meryton in a week or fortnight’s time. As that was the +case, neither Jane, to whom I related the whole, nor I, thought it +necessary to make our knowledge public; for of what use could it +apparently be to anyone, that the good opinion, which all the +neighbourhood had of him, should then be overthrown? And even when it +was settled that Lydia should go with Mrs. Forster, the necessity of +opening her eyes to his character never occurred to me. That _she_ could +be in any danger from the deception never entered my head. That such a +consequence as _this_ should ensue, you may easily believe was far +enough from my thoughts.” + +β€œWhen they all removed to Brighton, therefore, you had no reason, I +suppose, to believe them fond of each other?” + +β€œNot the slightest. I can remember no symptom of affection on either +side; and had anything of the kind been perceptible, you must be aware +that ours is not a family on which it could be thrown away. When first +he entered the corps, she was ready enough to admire him; but so we all +were. Every girl in or near Meryton was out of her senses about him for +the first two months: but he never distinguished _her_ by any particular +attention; and, consequently, after a moderate period of extravagant and +wild admiration, her fancy for him gave way, and others of the regiment, +who treated her with more distinction, again became her favourites.” + +It may be easily believed, that however little of novelty could be added +to their fears, hopes, and conjectures, on this interesting subject by +its repeated discussion, no other could detain them from it long, during +the whole of the journey. From Elizabeth’s thoughts it was never absent. +Fixed there by the keenest of all anguish, self-reproach, she could +find no interval of ease or forgetfulness. + +They travelled as expeditiously as possible; and sleeping one night on +the road, reached Longbourn by dinnertime the next day. It was a comfort +to Elizabeth to consider that Jane could not have been wearied by long +expectations. + +The little Gardiners, attracted by the sight of a chaise, were standing +on the steps of the house, as they entered the paddock; and when the +carriage drove up to the door, the joyful surprise that lighted up their +faces and displayed itself over their whole bodies, in a variety of +capers and frisks, was the first pleasing earnest of their welcome. + +Elizabeth jumped out; and after giving each of them a hasty kiss, +hurried into the vestibule, where Jane, who came running downstairs from +her mother’s apartment, immediately met her. + +Elizabeth, as she affectionately embraced her, whilst tears filled the +eyes of both, lost not a moment in asking whether anything had been +heard of the fugitives. + +β€œNot yet,” replied Jane. β€œBut now that my dear uncle is come, I hope +everything will be well.” + +β€œIs my father in town?” + +β€œYes, he went on Tuesday, as I wrote you word.” + +β€œAnd have you heard from him often?” + +β€œWe have heard only once. He wrote me a few lines on Wednesday, to say +that he had arrived in safety, and to give me his directions, which I +particularly begged him to do. He merely added, that he should not write +again, till he had something of importance to mention.” + +β€œAnd my mother--how is she? How are you all?” + +β€œMy mother is tolerably well, I trust; though her spirits are greatly +shaken. She is upstairs, and will have great satisfaction in seeing you +all. She does not yet leave her dressing-room. Mary and Kitty, thank +Heaven! are quite well.” + +β€œBut you--how are you?” cried Elizabeth. β€œYou look pale. How much you +must have gone through!” + +Her sister, however, assured her of her being perfectly well; and their +conversation, which had been passing while Mr. and Mrs. Gardiner were +engaged with their children, was now put an end to by the approach of +the whole party. Jane ran to her uncle and aunt, and welcomed and +thanked them both, with alternate smiles and tears. + +When they were all in the drawing-room, the questions which Elizabeth +had already asked were of course repeated by the others, and they soon +found that Jane had no intelligence to give. The sanguine hope of good, +however, which the benevolence of her heart suggested, had not yet +deserted her; she still expected that it would all end well, and that +every morning would bring some letter, either from Lydia or her father, +to explain their proceedings, and, perhaps, announce the marriage. + +Mrs. Bennet, to whose apartment they all repaired, after a few minutes’ +conversation together, received them exactly as might be expected; with +tears and lamentations of regret, invectives against the villainous +conduct of Wickham, and complaints of her own sufferings and ill-usage; +blaming everybody but the person to whose ill-judging indulgence the +errors of her daughter must be principally owing. + +β€œIf I had been able,” said she, β€œto carry my point in going to Brighton +with all my family, _this_ would not have happened: but poor dear Lydia +had nobody to take care of her. Why did the Forsters ever let her go out +of their sight? I am sure there was some great neglect or other on their +side, for she is not the kind of girl to do such a thing, if she had +been well looked after. I always thought they were very unfit to have +the charge of her; but I was over-ruled, as I always am. Poor, dear +child! And now here’s Mr. Bennet gone away, and I know he will fight +Wickham, wherever he meets him, and then he will be killed, and what is +to become of us all? The Collinses will turn us out, before he is cold +in his grave; and if you are not kind to us, brother, I do not know what +we shall do.” + +They all exclaimed against such terrific ideas; and Mr. Gardiner, after +general assurances of his affection for her and all her family, told her +that he meant to be in London the very next day, and would assist Mr. +Bennet in every endeavour for recovering Lydia. + +β€œDo not give way to useless alarm,” added he: β€œthough it is right to be +prepared for the worst, there is no occasion to look on it as certain. +It is not quite a week since they left Brighton. In a few days more, we +may gain some news of them; and till we know that they are not married, +and have no design of marrying, do not let us give the matter over as +lost. As soon as I get to town, I shall go to my brother, and make him +come home with me to Gracechurch Street, and then we may consult +together as to what is to be done.” + +β€œOh, my dear brother,” replied Mrs. Bennet, β€œthat is exactly what I +could most wish for. And now do, when you get to town, find them out, +wherever they may be; and if they are not married already, _make_ them +marry. And as for wedding clothes, do not let them wait for that, but +tell Lydia she shall have as much money as she chooses to buy them, +after they are married. And, above all things, keep Mr. Bennet from +fighting. Tell him what a dreadful state I am in--that I am frightened +out of my wits; and have such tremblings, such flutterings all over me, +such spasms in my side, and pains in my head, and such beatings at my +heart, that I can get no rest by night nor by day. And tell my dear +Lydia not to give any directions about her clothes till she has seen me, +for she does not know which are the best warehouses. Oh, brother, how +kind you are! I know you will contrive it all.” + +But Mr. Gardiner, though he assured her again of his earnest endeavours +in the cause, could not avoid recommending moderation to her, as well in +her hopes as her fears; and after talking with her in this manner till +dinner was on table, they left her to vent all her feelings on the +housekeeper, who attended in the absence of her daughters. + +Though her brother and sister were persuaded that there was no real +occasion for such a seclusion from the family, they did not attempt to +oppose it; for they knew that she had not prudence enough to hold her +tongue before the servants, while they waited at table, and judged it +better that _one_ only of the household, and the one whom they could +most trust, should comprehend all her fears and solicitude on the +subject. + +In the dining-room they were soon joined by Mary and Kitty, who had been +too busily engaged in their separate apartments to make their appearance +before. One came from her books, and the other from her toilette. The +faces of both, however, were tolerably calm; and no change was visible +in either, except that the loss of her favourite sister, or the anger +which she had herself incurred in the business, had given something more +of fretfulness than usual to the accents of Kitty. As for Mary, she was +mistress enough of herself to whisper to Elizabeth, with a countenance +of grave reflection, soon after they were seated at table,-- + +β€œThis is a most unfortunate affair, and will probably be much talked of. +But we must stem the tide of malice, and pour into the wounded bosoms of +each other the balm of sisterly consolation.” + +Then perceiving in Elizabeth no inclination of replying, she added, +β€œUnhappy as the event must be for Lydia, we may draw from it this useful +lesson:--that loss of virtue in a female is irretrievable, that one +false step involves her in endless ruin, that her reputation is no less +brittle than it is beautiful, and that she cannot be too much guarded in +her behaviour towards the undeserving of the other sex.” + +Elizabeth lifted up her eyes in amazement, but was too much oppressed to +make any reply. Mary, however, continued to console herself with such +kind of moral extractions from the evil before them. + +In the afternoon, the two elder Miss Bennets were able to be for half an +hour by themselves; and Elizabeth instantly availed herself of the +opportunity of making any inquiries which Jane was equally eager to +satisfy. After joining in general lamentations over the dreadful sequel +of this event, which Elizabeth considered as all but certain, and Miss +Bennet could not assert to be wholly impossible, the former continued +the subject by saying, β€œBut tell me all and everything about it which I +have not already heard. Give me further particulars. What did Colonel +Forster say? Had they no apprehension of anything before the elopement +took place? They must have seen them together for ever.” + +β€œColonel Forster did own that he had often suspected some partiality, +especially on Lydia’s side, but nothing to give him any alarm. I am so +grieved for him. His behaviour was attentive and kind to the utmost. He +_was_ coming to us, in order to assure us of his concern, before he had +any idea of their not being gone to Scotland: when that apprehension +first got abroad, it hastened his journey.” + +β€œAnd was Denny convinced that Wickham would not marry? Did he know of +their intending to go off? Had Colonel Forster seen Denny himself?” + +β€œYes; but when questioned by _him_, Denny denied knowing anything of +their plan, and would not give his real opinion about it. He did not +repeat his persuasion of their not marrying, and from _that_ I am +inclined to hope he might have been misunderstood before.” + +β€œAnd till Colonel Forster came himself, not one of you entertained a +doubt, I suppose, of their being really married?” + +β€œHow was it possible that such an idea should enter our brains? I felt a +little uneasy--a little fearful of my sister’s happiness with him in +marriage, because I knew that his conduct had not been always quite +right. My father and mother knew nothing of that; they only felt how +imprudent a match it must be. Kitty then owned, with a very natural +triumph on knowing more than the rest of us, that in Lydia’s last letter +she had prepared her for such a step. She had known, it seems, of their +being in love with each other many weeks.” + +β€œBut not before they went to Brighton?” + +β€œNo, I believe not.” + +β€œAnd did Colonel Forster appear to think ill of Wickham himself? Does he +know his real character?” + +β€œI must confess that he did not speak so well of Wickham as he formerly +did. He believed him to be imprudent and extravagant; and since this sad +affair has taken place, it is said that he left Meryton greatly in debt: +but I hope this may be false.” + +β€œOh, Jane, had we been less secret, had we told what we knew of him, +this could not have happened!” + +β€œPerhaps it would have been better,” replied her sister. + +β€œBut to expose the former faults of any person, without knowing what +their present feelings were, seemed unjustifiable.” + +β€œWe acted with the best intentions.” + +β€œCould Colonel Forster repeat the particulars of Lydia’s note to his +wife?” + +β€œHe brought it with him for us to see.” + +Jane then took it from her pocket-book, and gave it to Elizabeth. These +were the contents:-- + + /* NIND β€œMy dear Harriet, */ + + β€œYou will laugh when you know where I am gone, and I cannot help + laughing myself at your surprise to-morrow morning, as soon as I am + missed. I am going to Gretna Green, and if you cannot guess with + who, I shall think you a simpleton, for there is but one man in the + world I love, and he is an angel. I should never be happy without + him, so think it no harm to be off. You need not send them word at + Longbourn of my going, if you do not like it, for it will make the + surprise the greater when I write to them, and sign my name Lydia + Wickham. What a good joke it will be! I can hardly write for + laughing. Pray make my excuses to Pratt for not keeping my + engagement, and dancing with him to-night. Tell him I hope he will + excuse me when he knows all, and tell him I will dance with him at + the next ball we meet with great pleasure. I shall send for my + clothes when I get to Longbourn; but I wish you would tell Sally to + mend a great slit in my worked muslin gown before they are packed + up. Good-bye. Give my love to Colonel Forster. I hope you will + drink to our good journey. + +β€œYour affectionate friend, + +β€œLYDIA BENNET.” + + +β€œOh, thoughtless, thoughtless Lydia!” cried Elizabeth when she had +finished it. β€œWhat a letter is this, to be written at such a moment! But +at least it shows that _she_ was serious in the object of her journey. +Whatever he might afterwards persuade her to, it was not on her side a +_scheme_ of infamy. My poor father! how he must have felt it!” + +β€œI never saw anyone so shocked. He could not speak a word for full ten +minutes. My mother was taken ill immediately, and the whole house in +such confusion!” + +β€œOh, Jane,” cried Elizabeth, β€œwas there a servant belonging to it who +did not know the whole story before the end of the day?” + +β€œI do not know: I hope there was. But to be guarded at such a time is +very difficult. My mother was in hysterics; and though I endeavoured to +give her every assistance in my power, I am afraid I did not do so much +as I might have done. But the horror of what might possibly happen +almost took from me my faculties.” + +β€œYour attendance upon her has been too much for you. You do not look +well. Oh that I had been with you! you have had every care and anxiety +upon yourself alone.” + +β€œMary and Kitty have been very kind, and would have shared in every +fatigue, I am sure, but I did not think it right for either of them. +Kitty is slight and delicate, and Mary studies so much that her hours of +repose should not be broken in on. My aunt Philips came to Longbourn on +Tuesday, after my father went away; and was so good as to stay till +Thursday with me. She was of great use and comfort to us all, and Lady +Lucas has been very kind: she walked here on Wednesday morning to +condole with us, and offered her services, or any of her daughters, if +they could be of use to us.” + +β€œShe had better have stayed at home,” cried Elizabeth: β€œperhaps she +_meant_ well, but, under such a misfortune as this, one cannot see too +little of one’s neighbours. Assistance is impossible; condolence, +insufferable. Let them triumph over us at a distance, and be satisfied.” + +She then proceeded to inquire into the measures which her father had +intended to pursue, while in town, for the recovery of his daughter. + +β€œHe meant, I believe,” replied Jane, β€œto go to Epsom, the place where +they last changed horses, see the postilions, and try if anything could +be made out from them. His principal object must be to discover the +number of the hackney coach which took them from Clapham. It had come +with a fare from London; and as he thought the circumstance of a +gentleman and lady’s removing from one carriage into another might be +remarked, he meant to make inquiries at Clapham. If he could anyhow +discover at what house the coachman had before set down his fare, he +determined to make inquiries there, and hoped it might not be impossible +to find out the stand and number of the coach. I do not know of any +other designs that he had formed; but he was in such a hurry to be gone, +and his spirits so greatly discomposed, that I had difficulty in finding +out even so much as this.” + + + + +[Illustration: + + The Post +] + + + + +CHAPTER XLVIII. + + +[Illustration] + +The whole party were in hopes of a letter from Mr. Bennet the next +morning, but the post came in without bringing a single line from him. +His family knew him to be, on all common occasions, a most negligent and +dilatory correspondent; but at such a time they had hoped for exertion. +They were forced to conclude, that he had no pleasing intelligence to +send; but even of _that_ they would have been glad to be certain. Mr. +Gardiner had waited only for the letters before he set off. + +When he was gone, they were certain at least of receiving constant +information of what was going on; and their uncle promised, at parting, +to prevail on Mr. Bennet to return to Longbourn as soon as he could, to +the great consolation of his sister, who considered it as the only +security for her husband’s not being killed in a duel. + +Mrs. Gardiner and the children were to remain in Hertfordshire a few +days longer, as the former thought her presence might be serviceable to +her nieces. She shared in their attendance on Mrs. Bennet, and was a +great comfort to them in their hours of freedom. Their other aunt also +visited them frequently, and always, as she said, with the design of +cheering and heartening them up--though, as she never came without +reporting some fresh instance of Wickham’s extravagance or irregularity, +she seldom went away without leaving them more dispirited than she found +them. + +All Meryton seemed striving to blacken the man who, but three months +before, had been almost an angel of light. He was declared to be in debt +to every tradesman in the place, and his intrigues, all honoured with +the title of seduction, had been extended into every tradesman’s family. +Everybody declared that he was the wickedest young man in the world; and +everybody began to find out that they had always distrusted the +appearance of his goodness. Elizabeth, though she did not credit above +half of what was said, believed enough to make her former assurance of +her sister’s ruin still more certain; and even Jane, who believed still +less of it, became almost hopeless, more especially as the time was now +come, when, if they had gone to Scotland, which she had never before +entirely despaired of, they must in all probability have gained some +news of them. + +Mr. Gardiner left Longbourn on Sunday; on Tuesday, his wife received a +letter from him: it told them, that on his arrival he had immediately +found out his brother, and persuaded him to come to Gracechurch Street. +That Mr. Bennet had been to Epsom and Clapham, before his arrival, but +without gaining any satisfactory information; and that he was now +determined to inquire at all the principal hotels in town, as Mr. Bennet +thought it possible they might have gone to one of them, on their first +coming to London, before they procured lodgings. Mr. Gardiner himself +did not expect any success from this measure; but as his brother was +eager in it, he meant to assist him in pursuing it. He added, that Mr. +Bennet seemed wholly disinclined at present to leave London, and +promised to write again very soon. There was also a postscript to this +effect:-- + +β€œI have written to Colonel Forster to desire him to find out, if +possible, from some of the young man’s intimates in the regiment, +whether Wickham has any relations or connections who would be likely to +know in what part of the town he has now concealed himself. If there +were anyone that one could apply to, with a probability of gaining such +a clue as that, it might be of essential consequence. At present we have +nothing to guide us. Colonel Forster will, I dare say, do everything in +his power to satisfy us on this head. But, on second thoughts, perhaps +Lizzy could tell us what relations he has now living better than any +other person.” + +Elizabeth was at no loss to understand from whence this deference for +her authority proceeded; but it was not in her power to give any +information of so satisfactory a nature as the compliment deserved. + +She had never heard of his having had any relations, except a father +and mother, both of whom had been dead many years. It was possible, +however, that some of his companions in the ----shire might be able to +give more information; and though she was not very sanguine in expecting +it, the application was a something to look forward to. + +Every day at Longbourn was now a day of anxiety; but the most anxious +part of each was when the post was expected. The arrival of letters was +the first grand object of every morning’s impatience. Through letters, +whatever of good or bad was to be told would be communicated; and every +succeeding day was expected to bring some news of importance. + +But before they heard again from Mr. Gardiner, a letter arrived for +their father, from a different quarter, from Mr. Collins; which, as Jane +had received directions to open all that came for him in his absence, +she accordingly read; and Elizabeth, who knew what curiosities his +letters always were, looked over her, and read it likewise. It was as +follows:-- + + /* β€œMy dear Sir, */ + + β€œI feel myself called upon, by our relationship, and my situation + in life, to condole with you on the grievous affliction you are now + suffering under, of which we were yesterday informed by a letter + from Hertfordshire. Be assured, my dear sir, that Mrs. Collins and + myself sincerely sympathize with you, and all your respectable + family, in your present distress, which must be of the bitterest + kind, because proceeding from a cause which no time can remove. No + arguments shall be wanting on my part, that can alleviate so severe + a misfortune; or that may comfort you, under a circumstance that + must be, of all others, most afflicting to a parent’s mind. The + death of your daughter would have been a blessing in comparison of + this. And it is the more to be lamented, because there is reason to + suppose, as my dear Charlotte informs me, that this licentiousness + of behaviour in your + + [Illustration: + +β€œTo whom I have related the affair” + + [_Copyright 1894 by George Allen._]] + + daughter has proceeded from a faulty degree of indulgence; though, + at the same time, for the consolation of yourself and Mrs. Bennet, + I am inclined to think that her own disposition must be naturally + bad, or she could not be guilty of such an enormity, at so early an + age. Howsoever that may be, you are grievously to be pitied; in + which opinion I am not only joined by Mrs. Collins, but likewise by + Lady Catherine and her daughter, to whom I have related the affair. + They agree with me in apprehending that this false step in one + daughter will be injurious to the fortunes of all the others: for + who, as Lady Catherine herself condescendingly says, will connect + themselves with such a family? And this consideration leads me, + moreover, to reflect, with augmented satisfaction, on a certain + event of last November; for had it been otherwise, I must have been + involved in all your sorrow and disgrace. Let me advise you, then, + my dear sir, to console yourself as much as possible, to throw off + your unworthy child from your affection for ever, and leave her to + reap the fruits of her own heinous offence. + +β€œI am, dear sir,” etc., etc. + +Mr. Gardiner did not write again, till he had received an answer from +Colonel Forster; and then he had nothing of a pleasant nature to send. +It was not known that Wickham had a single relation with whom he kept up +any connection, and it was certain that he had no near one living. His +former acquaintance had been numerous; but since he had been in the +militia, it did not appear that he was on terms of particular friendship +with any of them. There was no one, therefore, who could be pointed out +as likely to give any news of him. And in the wretched state of his own +finances, there was a very powerful motive for secrecy, in addition to +his fear of discovery by Lydia’s relations; for it had just transpired +that he had left gaming debts behind him to a very considerable amount. +Colonel Forster believed that more than a thousand pounds would be +necessary to clear his expenses at Brighton. He owed a good deal in the +town, but his debts of honour were still more formidable. Mr. Gardiner +did not attempt to conceal these particulars from the Longbourn family; +Jane heard them with horror. β€œA gamester!” she cried. β€œThis is wholly +unexpected; I had not an idea of it.” + +Mr. Gardiner added, in his letter, that they might expect to see their +father at home on the following day, which was Saturday. Rendered +spiritless by the ill success of all their endeavours, he had yielded to +his brother-in-law’s entreaty that he would return to his family and +leave it to him to do whatever occasion might suggest to be advisable +for continuing their pursuit. When Mrs. Bennet was told of this, she did +not express so much satisfaction as her children expected, considering +what her anxiety for his life had been before. + +β€œWhat! is he coming home, and without poor Lydia?” she cried. β€œSure he +will not leave London before he has found them. Who is to fight Wickham, +and make him marry her, if he comes away?” + +As Mrs. Gardiner began to wish to be at home, it was settled that she +and her children should go to London at the same time that Mr. Bennet +came from it. The coach, therefore, took them the first stage of their +journey, and brought its master back to Longbourn. + +Mrs. Gardiner went away in all the perplexity about Elizabeth and her +Derbyshire friend, that had attended her from that part of the world. +His name had never been voluntarily mentioned before them by her niece; +and the kind of half-expectation which Mrs. Gardiner had formed, of +their being followed by a letter from him, had ended in nothing. +Elizabeth had received none since her return, that could come from +Pemberley. + +The present unhappy state of the family rendered any other excuse for +the lowness of her spirits unnecessary; nothing, therefore, could be +fairly conjectured from _that_,--though Elizabeth, who was by this time +tolerably well acquainted with her own feelings, was perfectly aware +that, had she known nothing of Darcy, she could have borne the dread of +Lydia’s infamy somewhat better. It would have spared her, she thought, +one sleepless night out of two. + +When Mr. Bennet arrived, he had all the appearance of his usual +philosophic composure. He said as little as he had ever been in the +habit of saying; made no mention of the business that had taken him +away; and it was some time before his daughters had courage to speak of +it. + +It was not till the afternoon, when he joined them at tea, that +Elizabeth ventured to introduce the subject; and then, on her briefly +expressing her sorrow for what he must have endured, he replied, β€œSay +nothing of that. Who should suffer but myself? It has been my own doing, +and I ought to feel it.” + +β€œYou must not be too severe upon yourself,” replied Elizabeth. + +β€œYou may well warn me against such an evil. Human nature is so prone to +fall into it! No, Lizzy, let me once in my life feel how much I have +been to blame. I am not afraid of being overpowered by the impression. +It will pass away soon enough.” + +β€œDo you suppose them to be in London?” + +β€œYes; where else can they be so well concealed?” + +β€œAnd Lydia used to want to go to London,” added Kitty. + +β€œShe is happy, then,” said her father, drily; β€œand her residence there +will probably be of some duration.” + +Then, after a short silence, he continued, β€œLizzy, I bear you no +ill-will for being justified in your advice to me last May, which, +considering the event, shows some greatness of mind.” + +They were interrupted by Miss Bennet, who came to fetch her mother’s +tea. + +β€œThis is a parade,” cried he, β€œwhich does one good; it gives such an +elegance to misfortune! Another day I will do the same; I will sit in my +library, in my nightcap and powdering gown, and give as much trouble as +I can,--or perhaps I may defer it till Kitty runs away.” + +β€œI am not going to run away, papa,” said Kitty, fretfully. β€œIf _I_ +should ever go to Brighton, I would behave better than Lydia.” + +β€œ_You_ go to Brighton! I would not trust you so near it as Eastbourne, +for fifty pounds! No, Kitty, I have at least learnt to be cautious, and +you will feel the effects of it. No officer is ever to enter my house +again, nor even to pass through the village. Balls will be absolutely +prohibited, unless you stand up with one of your sisters. And you are +never to stir out of doors, till you can prove that you have spent ten +minutes of every day in a rational manner.” + +Kitty, who took all these threats in a serious light, began to cry. + +β€œWell, well,” said he, β€œdo not make yourself unhappy. If you are a good +girl for the next ten years, I will take you to a review at the end of +them.” + + + + +[Illustration] + + + + +CHAPTER XLIX. + + +[Illustration] + +Two days after Mr. Bennet’s return, as Jane and Elizabeth were walking +together in the shrubbery behind the house, they saw the housekeeper +coming towards them, and concluding that she came to call them to their +mother, went forward to meet her; but instead of the expected summons, +when they approached her, she said to Miss Bennet, β€œI beg your pardon, +madam, for interrupting you, but I was in hopes you might have got some +good news from town, so I took the liberty of coming to ask.” + +β€œWhat do you mean, Hill? We have heard nothing from town.” + +β€œDear madam,” cried Mrs. Hill, in great astonishment, β€œdon’t you know +there is an express come for master from Mr. Gardiner? He has been here +this half hour, and master has had a letter.” + +Away ran the girls, too eager to get in to have time for speech. They +ran through the vestibule into the breakfast-room; from thence to the +library;--their father was in neither; and they were on the point of +seeking him upstairs with their mother, when they were met by the +butler, who said,-- + +β€œIf you are looking for my master, ma’am, he is walking towards the +little copse.” + +Upon this information, they instantly passed through the hall once more, +and ran across the lawn after their father, who was deliberately +pursuing his way towards a small wood on one side of the paddock. + +Jane, who was not so light, nor so much in the habit of running as +Elizabeth, soon lagged behind, while her sister, panting for breath, +came up with him, and eagerly cried out,-- + +β€œOh, papa, what news? what news? have you heard from my uncle?” + +β€œYes, I have had a letter from him by express.” + +β€œWell, and what news does it bring--good or bad?” + +β€œWhat is there of good to be expected?” said he, taking the letter from +his pocket; β€œbut perhaps you would like to read it.” + +Elizabeth impatiently caught it from his hand. Jane now came up. + +β€œRead it aloud,” said their father, β€œfor I hardly know myself what it is +about.” + + /* RIGHT β€œGracechurch Street, _Monday, August 2_. */ + +β€œMy dear Brother, + + β€œAt last I am able to send you some tidings of my niece, and such + as, upon the whole, I hope will give you satisfaction. Soon after + you left me on Saturday, I was fortunate enough to find out in what + part of London they were. The particulars I reserve till we meet. + It is enough to know they are discovered: I have seen them + both----” + + [Illustration: + +β€œBut perhaps you would like to read it” + + [_Copyright 1894 by George Allen._]] + + β€œThen it is as I always hoped,” cried Jane: β€œthey are married!” + + Elizabeth read on: β€œI have seen them both. They are not married, + nor can I find there was any intention of being so; but if you are + willing to perform the engagements which I have ventured to make on + your side, I hope it will not be long before they are. All that is + required of you is, to assure to your daughter, by settlement, her + equal share of the five thousand pounds, secured among your + children after the decease of yourself and my sister; and, + moreover, to enter into an engagement of allowing her, during your + life, one hundred pounds per annum. These are conditions which, + considering everything, I had no hesitation in complying with, as + far as I thought myself privileged, for you. I shall send this by + express, that no time may be lost in bringing me your answer. You + will easily comprehend, from these particulars, that Mr. Wickham’s + circumstances are not so hopeless as they are generally believed to + be. The world has been deceived in that respect; and I am happy to + say, there will be some little money, even when all his debts are + discharged, to settle on my niece, in addition to her own fortune. + If, as I conclude will be the case, you send me full powers to act + in your name throughout the whole of this business, I will + immediately give directions to Haggerston for preparing a proper + settlement. There will not be the smallest occasion for your coming + to town again; therefore stay quietly at Longbourn, and depend on + my diligence and care. Send back your answer as soon as you can, + and be careful to write explicitly. We have judged it best that my + niece should be married from this house, of which I hope you will + approve. She comes to us to-day. I shall write again as soon as + anything more is determined on. Yours, etc. + +β€œEDW. GARDINER.” + +β€œIs it possible?” cried Elizabeth, when she had finished. β€œCan it be +possible that he will marry her?” + +β€œWickham is not so undeserving, then, as we have thought him,” said her +sister. β€œMy dear father, I congratulate you.” + +β€œAnd have you answered the letter?” said Elizabeth. + +β€œNo; but it must be done soon.” + +Most earnestly did she then entreat him to lose no more time before he +wrote. + +β€œOh! my dear father,” she cried, β€œcome back and write immediately. +Consider how important every moment is in such a case.” + +β€œLet me write for you,” said Jane, β€œif you dislike the trouble +yourself.” + +β€œI dislike it very much,” he replied; β€œbut it must be done.” + +And so saying, he turned back with them, and walked towards the house. + +β€œAnd--may I ask?” said Elizabeth; β€œbut the terms, I suppose, must be +complied with.” + +β€œComplied with! I am only ashamed of his asking so little.” + +β€œAnd they _must_ marry! Yet he is _such_ a man.” + +β€œYes, yes, they must marry. There is nothing else to be done. But there +are two things that I want very much to know:--one is, how much money +your uncle has laid down to bring it about; and the other, how I am ever +to pay him.” + +β€œMoney! my uncle!” cried Jane, β€œwhat do you mean, sir?” + +β€œI mean that no man in his proper senses would marry Lydia on so slight +a temptation as one hundred a year during my life, and fifty after I am +gone.” + +β€œThat is very true,” said Elizabeth; β€œthough it had not occurred to me +before. His debts to be discharged, and something still to remain! Oh, +it must be my uncle’s doings! Generous, good man, I am afraid he has +distressed himself. A small sum could not do all this.” + +β€œNo,” said her father. β€œWickham’s a fool if he takes her with a farthing +less than ten thousand pounds: I should be sorry to think so ill of him, +in the very beginning of our relationship.” + +β€œTen thousand pounds! Heaven forbid! How is half such a sum to be +repaid?” + +Mr. Bennet made no answer; and each of them, deep in thought, continued +silent till they reached the house. Their father then went to the +library to write, and the girls walked into the breakfast-room. + +β€œAnd they are really to be married!” cried Elizabeth, as soon as they +were by themselves. β€œHow strange this is! and for _this_ we are to be +thankful. That they should marry, small as is their chance of happiness, +and wretched as is his character, we are forced to rejoice! Oh, Lydia!” + +β€œI comfort myself with thinking,” replied Jane, β€œthat he certainly would +not marry Lydia, if he had not a real regard for her. Though our kind +uncle has done something towards clearing him, I cannot believe that ten +thousand pounds, or anything like it, has been advanced. He has children +of his own, and may have more. How could he spare half ten thousand +pounds?” + +β€œIf we are ever able to learn what Wickham’s debts have been,” said +Elizabeth, β€œand how much is settled on his side on our sister, we shall +exactly know what Mr. Gardiner has done for them, because Wickham has +not sixpence of his own. The kindness of my uncle and aunt can never be +requited. Their taking her home, and affording her their personal +protection and countenance, is such a sacrifice to her advantage as +years of gratitude cannot enough acknowledge. By this time she is +actually with them! If such goodness does not make her miserable now, +she will never deserve to be happy! What a meeting for her, when she +first sees my aunt!” + +β€œWe must endeavour to forget all that has passed on either side,” said +Jane: β€œI hope and trust they will yet be happy. His consenting to marry +her is a proof, I will believe, that he is come to a right way of +thinking. Their mutual affection will steady them; and I flatter myself +they will settle so quietly, and live in so rational a manner, as may in +time make their past imprudence forgotten.” + +β€œTheir conduct has been such,” replied Elizabeth, β€œas neither you, nor +I, nor anybody, can ever forget. It is useless to talk of it.” + +It now occurred to the girls that their mother was in all likelihood +perfectly ignorant of what had happened. They went to the library, +therefore, and asked their father whether he would not wish them to make +it known to her. He was writing, and, without raising his head, coolly +replied,-- + +β€œJust as you please.” + +β€œMay we take my uncle’s letter to read to her?” + +β€œTake whatever you like, and get away.” + +Elizabeth took the letter from his writing-table, and they went upstairs +together. Mary and Kitty were both with Mrs. Bennet: one communication +would, therefore, do for all. After a slight preparation for good news, +the letter was read aloud. Mrs. Bennet could hardly contain herself. As +soon as Jane had read Mr. Gardiner’s hope of Lydia’s being soon married, +her joy burst forth, and every following sentence added to its +exuberance. She was now in an irritation as violent from delight as she +had ever been fidgety from alarm and vexation. To know that her daughter +would be married was enough. She was disturbed by no fear for her +felicity, nor humbled by any remembrance of her misconduct. + +β€œMy dear, dear Lydia!” she cried: β€œthis is delightful indeed! She will +be married! I shall see her again! She will be married at sixteen! My +good, kind brother! I knew how it would be--I knew he would manage +everything. How I long to see her! and to see dear Wickham too! But the +clothes, the wedding clothes! I will write to my sister Gardiner about +them directly. Lizzy, my dear, run down to your father, and ask him how +much he will give her. Stay, stay, I will go myself. Ring the bell, +Kitty, for Hill. I will put on my things in a moment. My dear, dear +Lydia! How merry we shall be together when we meet!” + +Her eldest daughter endeavoured to give some relief to the violence of +these transports, by leading her thoughts to the obligations which Mr. +Gardiner’s behaviour laid them all under. + +β€œFor we must attribute this happy conclusion,” she added, β€œin a great +measure to his kindness. We are persuaded that he has pledged himself to +assist Mr. Wickham with money.” + +β€œWell,” cried her mother, β€œit is all very right; who should do it but +her own uncle? If he had not had a family of his own, I and my children +must have had all his money, you know; and it is the first time we have +ever had anything from him except a few presents. Well! I am so happy. +In a short time, I shall have a daughter married. Mrs. Wickham! How well +it sounds! And she was only sixteen last June. My dear Jane, I am in +such a flutter, that I am sure I can’t write; so I will dictate, and you +write for me. We will settle with your father about the money +afterwards; but the things should be ordered immediately.” + +She was then proceeding to all the particulars of calico, muslin, and +cambric, and would shortly have dictated some very plentiful orders, had +not Jane, though with some difficulty, persuaded her to wait till her +father was at leisure to be consulted. One day’s delay, she observed, +would be of small importance; and her mother was too happy to be quite +so obstinate as usual. Other schemes, too, came into her head. + +β€œI will go to Meryton,” said she, β€œas soon as I am dressed, and tell the +good, good news to my sister Philips. And as I come back, I can call on +Lady Lucas and Mrs. Long. Kitty, run down and order the carriage. An +airing would do me a great deal of good, I am sure. Girls, can I do +anything for you in Meryton? Oh! here comes Hill. My dear Hill, have you +heard the good news? Miss Lydia is going to be married; and you shall +all have a bowl of punch to make merry at her wedding.” + +Mrs. Hill began instantly to express her joy. Elizabeth received her +congratulations amongst the rest, and then, sick of this folly, took +refuge in her own room, that she might think with freedom. Poor Lydia’s +situation must, at best, be bad enough; but that it was no worse, she +had need to be thankful. She felt it so; and though, in looking forward, +neither rational happiness, nor worldly prosperity could be justly +expected for her sister, in looking back to what they had feared, only +two hours ago, she felt all the advantages of what they had gained. + + + + +[Illustration: + +β€œThe spiteful old ladies” + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER L. + + +[Illustration] + +Mr. Bennet had very often wished, before this period of his life, that, +instead of spending his whole income, he had laid by an annual sum, for +the better provision of his children, and of his wife, if she survived +him. He now wished it more than ever. Had he done his duty in that +respect, Lydia need not have been indebted to her uncle for whatever of +honour or credit could now be purchased for her. The satisfaction of +prevailing on one of the most worthless young men in Great Britain to +be her husband might then have rested in its proper place. + +He was seriously concerned that a cause of so little advantage to anyone +should be forwarded at the sole expense of his brother-in-law; and he +was determined, if possible, to find out the extent of his assistance, +and to discharge the obligation as soon as he could. + +When first Mr. Bennet had married, economy was held to be perfectly +useless; for, of course, they were to have a son. This son was to join +in cutting off the entail, as soon as he should be of age, and the widow +and younger children would by that means be provided for. Five daughters +successively entered the world, but yet the son was to come; and Mrs. +Bennet, for many years after Lydia’s birth, had been certain that he +would. This event had at last been despaired of, but it was then too +late to be saving. Mrs. Bennet had no turn for economy; and her +husband’s love of independence had alone prevented their exceeding their +income. + +Five thousand pounds was settled by marriage articles on Mrs. Bennet and +the children. But in what proportions it should be divided amongst the +latter depended on the will of the parents. This was one point, with +regard to Lydia at least, which was now to be settled, and Mr. Bennet +could have no hesitation in acceding to the proposal before him. In +terms of grateful acknowledgment for the kindness of his brother, though +expressed most concisely, he then delivered on paper his perfect +approbation of all that was done, and his willingness to fulfil the +engagements that had been made for him. He had never before supposed +that, could Wickham be prevailed on to marry his daughter, it would be +done with so little inconvenience to himself as by the present +arrangement. He would scarcely be ten pounds a year the loser, by the +hundred that was to be paid them; for, what with her board and pocket +allowance, and the continual presents in money which passed to her +through her mother’s hands, Lydia’s expenses had been very little within +that sum. + +That it would be done with such trifling exertion on his side, too, was +another very welcome surprise; for his chief wish at present was to have +as little trouble in the business as possible. When the first transports +of rage which had produced his activity in seeking her were over, he +naturally returned to all his former indolence. His letter was soon +despatched; for though dilatory in undertaking business, he was quick in +its execution. He begged to know further particulars of what he was +indebted to his brother; but was too angry with Lydia to send any +message to her. + +The good news quickly spread through the house; and with proportionate +speed through the neighbourhood. It was borne in the latter with decent +philosophy. To be sure, it would have been more for the advantage of +conversation, had Miss Lydia Bennet come upon the town; or, as the +happiest alternative, been secluded from the world in some distant +farm-house. But there was much to be talked of, in marrying her; and the +good-natured wishes for her well-doing, which had proceeded before from +all the spiteful old ladies in Meryton, lost but little of their spirit +in this change of circumstances, because with such a husband her misery +was considered certain. + +It was a fortnight since Mrs. Bennet had been down stairs, but on this +happy day she again took her seat at the head of her table, and in +spirits oppressively high. No sentiment of shame gave a damp to her +triumph. The marriage of a daughter, which had been the first object of +her wishes since Jane was sixteen, was now on the point of +accomplishment, and her thoughts and her words ran wholly on those +attendants of elegant nuptials, fine muslins, new carriages, and +servants. She was busily searching through the neighbourhood for a +proper situation for her daughter; and, without knowing or considering +what their income might be, rejected many as deficient in size and +importance. + +β€œHaye Park might do,” said she, β€œif the Gouldings would quit it, or the +great house at Stoke, if the drawing-room were larger; but Ashworth is +too far off. I could not bear to have her ten miles from me; and as for +Purvis Lodge, the attics are dreadful.” + +Her husband allowed her to talk on without interruption while the +servants remained. But when they had withdrawn, he said to her, β€œMrs. +Bennet, before you take any, or all of these houses, for your son and +daughter, let us come to a right understanding. Into _one_ house in this +neighbourhood they shall never have admittance. I will not encourage the +imprudence of either, by receiving them at Longbourn.” + +A long dispute followed this declaration; but Mr. Bennet was firm: it +soon led to another; and Mrs. Bennet found, with amazement and horror, +that her husband would not advance a guinea to buy clothes for his +daughter. He protested that she should receive from him no mark of +affection whatever on the occasion. Mrs. Bennet could hardly comprehend +it. That his anger could be carried to such a point of inconceivable +resentment as to refuse his daughter a privilege, without which her +marriage would scarcely seem valid, exceeded all that she could believe +possible. She was more alive to the disgrace, which her want of new +clothes must reflect on her daughter’s nuptials, than to any sense of +shame at her eloping and living with Wickham a fortnight before they +took place. + +Elizabeth was now most heartily sorry that she had, from the distress of +the moment, been led to make Mr. Darcy acquainted with their fears for +her sister; for since her marriage would so shortly give the proper +termination to the elopement, they might hope to conceal its +unfavourable beginning from all those who were not immediately on the +spot. + +She had no fear of its spreading farther, through his means. There were +few people on whose secrecy she would have more confidently depended; +but at the same time there was no one whose knowledge of a sister’s +frailty would have mortified her so much. Not, however, from any fear of +disadvantage from it individually to herself; for at any rate there +seemed a gulf impassable between them. Had Lydia’s marriage been +concluded on the most honourable terms, it was not to be supposed that +Mr. Darcy would connect himself with a family, where to every other +objection would now be added an alliance and relationship of the nearest +kind with the man whom he so justly scorned. + +From such a connection she could not wonder that he should shrink. The +wish of procuring her regard, which she had assured herself of his +feeling in Derbyshire, could not in rational expectation survive such a +blow as this. She was humbled, she was grieved; she repented, though she +hardly knew of what. She became jealous of his esteem, when she could no +longer hope to be benefited by it. She wanted to hear of him, when there +seemed the least chance of gaining intelligence. She was convinced that +she could have been happy with him, when it was no longer likely they +should meet. + +What a triumph for him, as she often thought, could he know that the +proposals which she had proudly spurned only four months ago would now +have been gladly and gratefully received! He was as generous, she +doubted not, as the most generous of his sex. But while he was mortal, +there must be a triumph. + +She began now to comprehend that he was exactly the man who, in +disposition and talents, would most suit her. His understanding and +temper, though unlike her own, would have answered all her wishes. It +was an union that must have been to the advantage of both: by her ease +and liveliness, his mind might have been softened, his manners improved; +and from his judgment, information, and knowledge of the world, she must +have received benefit of greater importance. + +But no such happy marriage could now teach the admiring multitude what +connubial felicity really was. An union of a different tendency, and +precluding the possibility of the other, was soon to be formed in their +family. + +How Wickham and Lydia were to be supported in tolerable independence she +could not imagine. But how little of permanent happiness could belong to +a couple who were only brought together because their passions were +stronger than their virtue, she could easily conjecture. + +Mr. Gardiner soon wrote again to his brother. To Mr. Bennet’s +acknowledgments he briefly replied, with assurances of his eagerness to +promote the welfare of any of his family; and concluded with entreaties +that the subject might never be mentioned to him again. The principal +purport of his letter was to inform them, that Mr. Wickham had resolved +on quitting the militia. + +β€œIt was greatly my wish that he should do so,” he added, β€œas soon as his +marriage was fixed on. And I think you will agree with me, in +considering a removal from that corps as highly advisable, both on his +account and my niece’s. It is Mr. Wickham’s intention to go into the +Regulars; and, among his former friends, there are still some who are +able and willing to assist him in the army. He has the promise of an +ensigncy in General----’s regiment, now quartered in the north. It is +an advantage to have it so far from this part of the kingdom. He +promises fairly; and I hope among different people, where they may each +have a character to preserve, they will both be more prudent. I have +written to Colonel Forster, to inform him of our present arrangements, +and to request that he will satisfy the various creditors of Mr. Wickham +in and near Brighton with assurances of speedy payment, for which I have +pledged myself. And will you give yourself the trouble of carrying +similar assurances to his creditors in Meryton, of whom I shall subjoin +a list, according to his information? He has given in all his debts; I +hope at least he has not deceived us. Haggerston has our directions, and +all will be completed in a week. They will then join his regiment, +unless they are first invited to Longbourn; and I understand from Mrs. +Gardiner that my niece is very desirous of seeing you all before she +leaves the south. She is well, and begs to be dutifully remembered to +you and her mother.--Yours, etc. + +β€œE. GARDINER.” + +Mr. Bennet and his daughters saw all the advantages of Wickham’s +removal from the ----shire, as clearly as Mr. Gardiner could do. But +Mrs. Bennet was not so well pleased with it. Lydia’s being settled in +the north, just when she had expected most pleasure and pride in her +company, for she had by no means given up her plan of their residing in +Hertfordshire, was a severe disappointment; and, besides, it was such a +pity that Lydia should be taken from a regiment where she was acquainted +with everybody, and had so many favourites. + +β€œShe is so fond of Mrs. Forster,” said she, β€œit will be quite shocking +to send her away! And there are several of the young men, too, that she +likes very much. The officers may not be so pleasant in General----’s +regiment.” + +His daughter’s request, for such it might be considered, of being +admitted into her family again, before she set off for the north, +received at first an absolute negative. But Jane and Elizabeth, who +agreed in wishing, for the sake of their sister’s feelings and +consequence, that she should be noticed on her marriage by her parents, +urged him so earnestly, yet so rationally and so mildly, to receive her +and her husband at Longbourn, as soon as they were married, that he was +prevailed on to think as they thought, and act as they wished. And their +mother had the satisfaction of knowing, that she should be able to show +her married daughter in the neighbourhood, before she was banished to +the north. When Mr. Bennet wrote again to his brother, therefore, he +sent his permission for them to come; and it was settled, that, as soon +as the ceremony was over, they should proceed to Longbourn. Elizabeth +was surprised, however, that Wickham should consent to such a scheme; +and, had she consulted only her own inclination, any meeting with him +would have been the last object of her wishes. + + + + +[Illustration: + +β€œWith an affectionate smile” + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER LI. + + +[Illustration] + +Their sister’s wedding-day arrived; and Jane and Elizabeth felt for her +probably more than she felt for herself. The carriage was sent to meet +them at----, and they were to return in it by dinnertime. Their arrival +was dreaded by the elder Miss Bennets--and Jane more especially, who +gave Lydia the feelings which would have attended herself, had _she_ +been the culprit, and was wretched in the thought of what her sister +must endure. + +They came. The family were assembled in the breakfast-room to receive +them. Smiles decked the face of Mrs. Bennet, as the carriage drove up to +the door; her husband looked impenetrably grave; her daughters, alarmed, +anxious, uneasy. + +Lydia’s voice was heard in the vestibule; the door was thrown open, and +she ran into the room. Her mother stepped forwards, embraced her, and +welcomed her with rapture; gave her hand with an affectionate smile to +Wickham, who followed his lady; and wished them both joy, with an +alacrity which showed no doubt of their happiness. + +Their reception from Mr. Bennet, to whom they then turned, was not quite +so cordial. His countenance rather gained in austerity; and he scarcely +opened his lips. The easy assurance of the young couple, indeed, was +enough to provoke him. + +Elizabeth was disgusted, and even Miss Bennet was shocked. Lydia was +Lydia still; untamed, unabashed, wild, noisy, and fearless. She turned +from sister to sister, demanding their congratulations; and when at +length they all sat down, looked eagerly round the room, took notice of +some little alteration in it, and observed, with a laugh, that it was a +great while since she had been there. + +Wickham was not at all more distressed than herself; but his manners +were always so pleasing, that, had his character and his marriage been +exactly what they ought, his smiles and his easy address, while he +claimed their relationship, would have delighted them all. Elizabeth +had not before believed him quite equal to such assurance; but she sat +down, resolving within herself to draw no limits in future to the +impudence of an impudent man. _She_ blushed, and Jane blushed; but the +cheeks of the two who caused their confusion suffered no variation of +colour. + +There was no want of discourse. The bride and her mother could neither +of them talk fast enough; and Wickham, who happened to sit near +Elizabeth, began inquiring after his acquaintance in that neighbourhood, +with a good-humoured ease, which she felt very unable to equal in her +replies. They seemed each of them to have the happiest memories in the +world. Nothing of the past was recollected with pain; and Lydia led +voluntarily to subjects which her sisters would not have alluded to for +the world. + +β€œOnly think of its being three months,” she cried, β€œsince I went away: +it seems but a fortnight, I declare; and yet there have been things +enough happened in the time. Good gracious! when I went away, I am sure +I had no more idea of being married till I came back again! though I +thought it would be very good fun if I was.” + +Her father lifted up his eyes, Jane was distressed, Elizabeth looked +expressively at Lydia; but she, who never heard nor saw anything of +which she chose to be insensible, gaily continued,-- + +β€œOh, mamma, do the people hereabouts know I am married to-day? I was +afraid they might not; and we overtook William Goulding in his curricle, +so I was determined he should know it, and so I let down the side glass +next to him, and took off my glove and let my hand just rest upon the +window frame, so that he might see the ring, and then I bowed and +smiled like anything.” + +Elizabeth could bear it no longer. She got up and ran out of the room; +and returned no more, till she heard them passing through the hall to +the dining-parlour. She then joined them soon enough to see Lydia, with +anxious parade, walk up to her mother’s right hand, and hear her say to +her eldest sister,-- + +β€œAh, Jane, I take your place now, and you must go lower, because I am a +married woman.” + +It was not to be supposed that time would give Lydia that embarrassment +from which she had been so wholly free at first. Her ease and good +spirits increased. She longed to see Mrs. Philips, the Lucases, and all +their other neighbours, and to hear herself called β€œMrs. Wickham” by +each of them; and in the meantime she went after dinner to show her ring +and boast of being married to Mrs. Hill and the two housemaids. + +β€œWell, mamma,” said she, when they were all returned to the +breakfast-room, β€œand what do you think of my husband? Is not he a +charming man? I am sure my sisters must all envy me. I only hope they +may have half my good luck. They must all go to Brighton. That is the +place to get husbands. What a pity it is, mamma, we did not all go!” + +β€œVery true; and if I had my will we should. But, my dear Lydia, I don’t +at all like your going such a way off. Must it be so?” + +β€œOh, Lord! yes; there is nothing in that. I shall like it of all things. +You and papa, and my sisters, must come down and see us. We shall be at +Newcastle all the winter, and I dare say there will be some balls, and I +will take care to get good partners for them all.” + +β€œI should like it beyond anything!” said her mother. + +β€œAnd then when you go away, you may leave one or two of my sisters +behind you; and I dare say I shall get husbands for them before the +winter is over.” + +β€œI thank you for my share of the favour,” said Elizabeth; β€œbut I do not +particularly like your way of getting husbands.” + +Their visitors were not to remain above ten days with them. Mr. Wickham +had received his commission before he left London, and he was to join +his regiment at the end of a fortnight. + +No one but Mrs. Bennet regretted that their stay would be so short; and +she made the most of the time by visiting about with her daughter, and +having very frequent parties at home. These parties were acceptable to +all; to avoid a family circle was even more desirable to such as did +think than such as did not. + +Wickham’s affection for Lydia was just what Elizabeth had expected to +find it; not equal to Lydia’s for him. She had scarcely needed her +present observation to be satisfied, from the reason of things, that +their elopement had been brought on by the strength of her love rather +than by his; and she would have wondered why, without violently caring +for her, he chose to elope with her at all, had she not felt certain +that his flight was rendered necessary by distress of circumstances; and +if that were the case, he was not the young man to resist an opportunity +of having a companion. + +Lydia was exceedingly fond of him. He was her dear Wickham on every +occasion; no one was to be put in competition with him. He did +everything best in the world; and she was sure he would kill more birds +on the first of September than anybody else in the country. + +One morning, soon after their arrival, as she was sitting with her two +elder sisters, she said to Elizabeth,-- + +β€œLizzy, I never gave _you_ an account of my wedding, I believe. You were +not by, when I told mamma, and the others, all about it. Are not you +curious to hear how it was managed?” + +β€œNo, really,” replied Elizabeth; β€œI think there cannot be too little +said on the subject.” + +β€œLa! You are so strange! But I must tell you how it went off. We were +married, you know, at St. Clement’s, because Wickham’s lodgings were in +that parish. And it was settled that we should all be there by eleven +o’clock. My uncle and aunt and I were to go together; and the others +were to meet us at the church. + +β€œWell, Monday morning came, and I was in such a fuss! I was so afraid, +you know, that something would happen to put it off, and then I should +have gone quite distracted. And there was my aunt, all the time I was +dressing, preaching and talking away just as if she was reading a +sermon. However, I did not hear above one word in ten, for I was +thinking, you may suppose, of my dear Wickham. I longed to know whether +he would be married in his blue coat. + +β€œWell, and so we breakfasted at ten as usual: I thought it would never +be over; for, by the bye, you are to understand that my uncle and aunt +were horrid unpleasant all the time I was with them. If you’ll believe +me, I did not once put my foot out of doors, though I was there a +fortnight. Not one party, or scheme, or anything! To be sure, London was +rather thin, but, however, the Little Theatre was open. + +β€œWell, and so, just as the carriage came to the door, my uncle was +called away upon business to that horrid man Mr. Stone. And then, you +know, when once they get together, there is no end of it. Well, I was so +frightened I did not know what to do, for my uncle was to give me away; +and if we were beyond the hour we could not be married all day. But, +luckily, he came back again in ten minutes’ time, and then we all set +out. However, I recollected afterwards, that if he _had_ been prevented +going, the wedding need not be put off, for Mr. Darcy might have done as +well.” + +β€œMr. Darcy!” repeated Elizabeth, in utter amazement. + +β€œOh, yes! he was to come there with Wickham, you know. But, gracious me! +I quite forgot! I ought not to have said a word about it. I promised +them so faithfully! What will Wickham say? It was to be such a secret!” + +β€œIf it was to be a secret,” said Jane, β€œsay not another word on the +subject. You may depend upon my seeking no further.” + +β€œOh, certainly,” said Elizabeth, though burning with curiosity; β€œwe will +ask you no questions.” + +β€œThank you,” said Lydia; β€œfor if you did, I should certainly tell you +all, and then Wickham would be so angry.” + +On such encouragement to ask, Elizabeth was forced to put it out of her +power, by running away. + +But to live in ignorance on such a point was impossible; or at least it +was impossible not to try for information. Mr. Darcy had been at her +sister’s wedding. It was exactly a scene, and exactly among people, +where he had apparently least to do, and least temptation to go. +Conjectures as to the meaning of it, rapid and wild, hurried into her +brain; but she was satisfied with none. Those that best pleased her, as +placing his conduct in the noblest light, seemed most improbable. She +could not bear such suspense; and hastily seizing a sheet of paper, +wrote a short letter to her aunt, to request an explanation of what +Lydia had dropped, if it were compatible with the secrecy which had been +intended. + +β€œYou may readily comprehend,” she added, β€œwhat my curiosity must be to +know how a person unconnected with any of us, and, comparatively +speaking, a stranger to our family, should have been amongst you at such +a time. Pray write instantly, and let me understand it--unless it is, +for very cogent reasons, to remain in the secrecy which Lydia seems to +think necessary; and then I must endeavour to be satisfied with +ignorance.” + +β€œNot that I _shall_, though,” she added to herself, and she finished the +letter; β€œand, my dear aunt, if you do not tell me in an honourable +manner, I shall certainly be reduced to tricks and stratagems to find it +out.” + +Jane’s delicate sense of honour would not allow her to speak to +Elizabeth privately of what Lydia had let fall; Elizabeth was glad of +it:--till it appeared whether her inquiries would receive any +satisfaction, she had rather be without a confidante. + + + + +[Illustration: + +β€œI am sure she did not listen.” +] + + + + +CHAPTER LII. + + +[Illustration] + +Elizabeth had the satisfaction of receiving an answer to her letter as +soon as she possibly could. She was no sooner in possession of it, than +hurrying into the little copse, where she was least likely to be +interrupted, she sat down on one of the benches, and prepared to be +happy; for the length of the letter convinced her that it did not +contain a denial. + + /* RIGHT β€œGracechurch Street, _Sept. 6_. */ + +β€œMy dear Niece, + + β€œI have just received your letter, and shall devote this whole + morning to answering it, as I foresee that a _little_ writing will + not comprise what I have to tell you. I must confess myself + surprised by your application; I did not expect it from _you_. + Don’t think me angry, however, for I only mean to let you know, + that I had not imagined such inquiries to be necessary on _your_ + side. If you do not choose to understand me, forgive my + impertinence. Your uncle is as much surprised as I am; and nothing + but the belief of your being a party concerned would have allowed + him to act as he has done. But if you are really innocent and + ignorant, I must be more explicit. On the very day of my coming + home from Longbourn, your uncle had a most unexpected visitor. Mr. + Darcy called, and was shut up with him several hours. It was all + over before I arrived; so my curiosity was not so dreadfully racked + as _yours_ seems to have been. He came to tell Mr. Gardiner that he + had found out where your sister and Mr. Wickham were, and that he + had seen and talked with them both--Wickham repeatedly, Lydia once. + From what I can collect, he left Derbyshire only one day after + ourselves, and came to town with the resolution of hunting for + them. The motive professed was his conviction of its being owing to + himself that Wickham’s worthlessness had not been so well known as + to make it impossible for any young woman of character to love or + confide in him. He generously imputed the whole to his mistaken + pride, and confessed that he had before thought it beneath him to + lay his private actions open to the world. His character was to + speak for itself. He called it, therefore, his duty to step + forward, and endeavour to remedy an evil which had been brought on + by himself. If he _had another_ motive, I am sure it would never + disgrace him. He had been some days in town before he was able to + discover them; but he had something to direct his search, which was + more than _we_ had; and the consciousness of this was another + reason for his resolving to follow us. There is a lady, it seems, a + Mrs. Younge, who was some time ago governess to Miss Darcy, and was + dismissed from her charge on some cause of disapprobation, though + he did not say what. She then took a large house in Edward Street, + and has since maintained herself by letting lodgings. This Mrs. + Younge was, he knew, intimately acquainted with Wickham; and he + went to her for intelligence of him, as soon as he got to town. But + it was two or three days before he could get from her what he + wanted. She would not betray her trust, I suppose, without bribery + and corruption, for she really did know where her friend was to be + found. Wickham, indeed, had gone to her on their first arrival in + London; and had she been able to receive them into her house, they + would have taken up their abode with her. At length, however, our + kind friend procured the wished-for direction. They were in ---- + Street. He saw Wickham, and afterwards insisted on seeing Lydia. + His first object with her, he acknowledged, had been to persuade + her to quit her present disgraceful situation, and return to her + friends as soon as they could be prevailed on to receive her, + offering his assistance as far as it would go. But he found Lydia + absolutely resolved on remaining where she was. She cared for none + of her friends; she wanted no help of his; she would not hear of + leaving Wickham. She was sure they should be married some time or + other, and it did not much signify when. Since such were her + feelings, it only remained, he thought, to secure and expedite a + marriage, which, in his very first conversation with Wickham, he + easily learnt had never been _his_ design. He confessed himself + obliged to leave the regiment on account of some debts of honour + which were very pressing; and scrupled not to lay all the ill + consequences of Lydia’s flight on her own folly alone. He meant to + resign his commission immediately; and as to his future situation, + he could conjecture very little about it. He must go somewhere, but + he did not know where, and he knew he should have nothing to live + on. Mr. Darcy asked why he did not marry your sister at once. + Though Mr. Bennet was not imagined to be very rich, he would have + been able to do something for him, and his situation must have been + benefited by marriage. But he found, in reply to this question, + that Wickham still cherished the hope of more effectually making + his fortune by marriage, in some other country. Under such + circumstances, however, he was not likely to be proof against the + temptation of immediate relief. They met several times, for there + was much to be discussed. Wickham, of course, wanted more than he + could get; but at length was reduced to be reasonable. Everything + being settled between _them_, Mr. Darcy’s next step was to make + your uncle acquainted with it, and he first called in Gracechurch + Street the evening before I came home. But Mr. Gardiner could not + be seen; and Mr. Darcy found, on further inquiry, that your father + was still with him, but would quit town the next morning. He did + not judge your father to be a person whom he could so properly + consult as your uncle, and therefore readily postponed seeing him + till after the departure of the former. He did not leave his name, + and till the next day it was only known that a gentleman had called + on business. On Saturday he came again. Your father was gone, your + uncle at home, and, as I said before, they had a great deal of talk + together. They met again on Sunday, and then _I_ saw him too. It + was not all settled before Monday: as soon as it was, the express + was sent off to Longbourn. But our visitor was very obstinate. I + fancy, Lizzy, that obstinacy is the real defect of his character, + after all. He has been accused of many faults at different times; + but _this_ is the true one. Nothing was to be done that he did not + do himself; though I am sure (and I do not speak it to be thanked, + therefore say nothing about it) your uncle would most readily have + settled the whole. They battled it together for a long time, which + was more than either the gentleman or lady concerned in it + deserved. But at last your uncle was forced to yield, and instead + of being allowed to be of use to his niece, was forced to put up + with only having the probable credit of it, which went sorely + against the grain; and I really believe your letter this morning + gave him great pleasure, because it required an explanation that + would rob him of his borrowed feathers, and give the praise where + it was due. But, Lizzy, this must go no further than yourself, or + Jane at most. You know pretty well, I suppose, what has been done + for the young people. His debts are to be paid, amounting, I + believe, to considerably more than a thousand pounds, another + thousand in addition to her own settled upon _her_, and his + commission purchased. The reason why all this was to be done by him + alone, was such as I have given above. It was owing to him, to his + reserve and want of proper consideration, that Wickham’s character + had been so misunderstood, and consequently that he had been + received and noticed as he was. Perhaps there was some truth in + _this_; though I doubt whether _his_ reserve, or _anybody’s_ + reserve can be answerable for the event. But in spite of all this + fine talking, my dear Lizzy, you may rest perfectly assured that + your uncle would never have yielded, if we had not given him credit + for _another interest_ in the affair. When all this was resolved + on, he returned again to his friends, who were still staying at + Pemberley; but it was agreed that he should be in London once more + when the wedding took place, and all money matters were then to + receive the last finish. I believe I have now told you everything. + It is a relation which you tell me is to give you great surprise; I + hope at least it will not afford you any displeasure. Lydia came to + us, and Wickham had constant admission to the house. _He_ was + exactly what he had been when I knew him in Hertfordshire; but I + would not tell you how little I was satisfied with _her_ behaviour + while she stayed with us, if I had not perceived, by Jane’s letter + last Wednesday, that her conduct on coming home was exactly of a + piece with it, and therefore what I now tell you can give you no + fresh pain. I talked to her repeatedly in the most serious manner, + representing to her the wickedness of what she had done, and all + the unhappiness she had brought on her family. If she heard me, it + was by good luck, for I am sure she did not listen. I was sometimes + quite provoked; but then I recollected my dear Elizabeth and Jane, + and for their sakes had patience with her. Mr. Darcy was punctual + in his return, and, as Lydia informed you, attended the wedding. He + dined with us the next day, and was to leave town again on + Wednesday or Thursday. Will you be very angry with me, my dear + Lizzy, if I take this opportunity of saying (what I was never bold + enough to say before) how much I like him? His behaviour to us has, + in every respect, been as pleasing as when we were in Derbyshire. + His understanding and opinions all please me; he wants nothing but + a little more liveliness, and _that_, if he marry _prudently_, his + wife may teach him. I thought him very sly; he hardly ever + mentioned your name. But slyness seems the fashion. Pray forgive + me, if I have been very presuming, or at least do not punish me so + far as to exclude me from P. I shall never be quite happy till I + have been all round the park. A low phaeton with a nice little pair + of ponies would be the very thing. But I must write no more. The + children have been wanting me this half hour. + +β€œYours, very sincerely, + +β€œM. GARDINER.” + + +The contents of this letter threw Elizabeth into a flutter of spirits, +in which it was difficult to determine whether pleasure or pain bore the +greatest share. The vague and unsettled suspicions which uncertainty had +produced, of what Mr. Darcy might have been doing to forward her +sister’s match--which she had feared to encourage, as an exertion of +goodness too great to be probable, and at the same time dreaded to be +just, from the pain of obligation--were proved beyond their greatest +extent to be true! He had followed them purposely to town, he had taken +on himself all the trouble and mortification attendant on such a +research; in which supplication had been necessary to a woman whom he +must abominate and despise, and where he was reduced to meet, frequently +meet, reason with, persuade, and finally bribe the man whom he always +most wished to avoid, and whose very name it was punishment to him to +pronounce. He had done all this for a girl whom he could neither regard +nor esteem. Her heart did whisper that he had done it for her. But it +was a hope shortly checked by other considerations; and she soon felt +that even her vanity was insufficient, when required to depend on his +affection for her, for a woman who had already refused him, as able to +overcome a sentiment so natural as abhorrence against relationship with +Wickham. Brother-in-law of Wickham! Every kind of pride must revolt from +the connection. He had, to be sure, done much. She was ashamed to think +how much. But he had given a reason for his interference, which asked no +extraordinary stretch of belief. It was reasonable that he should feel +he had been wrong; he had liberality, and he had the means of exercising +it; and though she would not place herself as his principal inducement, +she could perhaps believe, that remaining partiality for her might +assist his endeavours in a cause where her peace of mind must be +materially concerned. It was painful, exceedingly painful, to know that +they were under obligations to a person who could never receive a +return. They owed the restoration of Lydia, her character, everything to +him. Oh, how heartily did she grieve over every ungracious sensation she +had ever encouraged, every saucy speech she had ever directed towards +him! For herself she was humbled; but she was proud of him,--proud that +in a cause of compassion and honour he had been able to get the better +of himself. She read over her aunt’s commendation of him again and +again. It was hardly enough; but it pleased her. She was even sensible +of some pleasure, though mixed with regret, on finding how steadfastly +both she and her uncle had been persuaded that affection and confidence +subsisted between Mr. Darcy and herself. + +She was roused from her seat and her reflections, by someone’s approach; +and, before she could strike into another path, she was overtaken by +Wickham. + +β€œI am afraid I interrupt your solitary ramble, my dear sister?” said he, +as he joined her. + +β€œYou certainly do,” she replied with a smile; β€œbut it does not follow +that the interruption must be unwelcome.” + +β€œI should be sorry, indeed, if it were. _We_ were always good friends, +and now we are better.” + +β€œTrue. Are the others coming out?” + +β€œI do not know. Mrs. Bennet and Lydia are going in the carriage to +Meryton. And so, my dear sister, I find, from our uncle and aunt, that +you have actually seen Pemberley.” + +She replied in the affirmative. + +β€œI almost envy you the pleasure, and yet I believe it would be too much +for me, or else I could take it in my way to Newcastle. And you saw the +old housekeeper, I suppose? Poor Reynolds, she was always very fond of +me. But of course she did not mention my name to you.” + +β€œYes, she did.” + +β€œAnd what did she say?” + +β€œThat you were gone into the army, and she was afraid had--not turned +out well. At such a distance as _that_, you know, things are strangely +misrepresented.” + +β€œCertainly,” he replied, biting his lips. Elizabeth hoped she had +silenced him; but he soon afterwards said,-- + +β€œI was surprised to see Darcy in town last month. We passed each other +several times. I wonder what he can be doing there.” + +β€œPerhaps preparing for his marriage with Miss de Bourgh,” said +Elizabeth. β€œIt must be something particular to take him there at this +time of year.” + +β€œUndoubtedly. Did you see him while you were at Lambton? I thought I +understood from the Gardiners that you had.” + +β€œYes; he introduced us to his sister.” + +β€œAnd do you like her?” + +β€œVery much.” + +β€œI have heard, indeed, that she is uncommonly improved within this year +or two. When I last saw her, she was not very promising. I am very glad +you liked her. I hope she will turn out well.” + +β€œI dare say she will; she has got over the most trying age.” + +β€œDid you go by the village of Kympton?” + +β€œI do not recollect that we did.” + +β€œI mention it because it is the living which I ought to have had. A most +delightful place! Excellent parsonage-house! It would have suited me in +every respect.” + +β€œHow should you have liked making sermons?” + +β€œExceedingly well. I should have considered it as part of my duty, and +the exertion would soon have been nothing. One ought not to repine; but, +to be sure, it would have been such a thing for me! The quiet, the +retirement of such a life, would have answered all my ideas of +happiness! But it was not to be. Did you ever hear Darcy mention the +circumstance when you were in Kent?” + +β€œI _have_ heard from authority, which I thought _as good_, that it was +left you conditionally only, and at the will of the present patron.” + +β€œYou have! Yes, there was something in _that_; I told you so from the +first, you may remember.” + +β€œI _did_ hear, too, that there was a time when sermon-making was not so +palatable to you as it seems to be at present; that you actually +declared your resolution of never taking orders, and that the business +had been compromised accordingly.” + +β€œYou did! and it was not wholly without foundation. You may remember +what I told you on that point, when first we talked of it.” + +They were now almost at the door of the house, for she had walked fast +to get rid of him; and unwilling, for her sister’s sake, to provoke him, +she only said in reply, with a good-humoured smile,-- + +β€œCome, Mr. Wickham, we are brother and sister, you know. Do not let us +quarrel about the past. In future, I hope we shall be always of one +mind.” + +She held out her hand: he kissed it with affectionate gallantry, though +he hardly knew how to look, and they entered the house. + + + + +[Illustration: + +β€œMr. Darcy with him.” +] + + + + +CHAPTER LIII. + + +[Illustration] + +Mr. Wickham was so perfectly satisfied with this conversation, that he +never again distressed himself, or provoked his dear sister Elizabeth, +by introducing the subject of it; and she was pleased to find that she +had said enough to keep him quiet. + +The day of his and Lydia’s departure soon came; and Mrs. Bennet was +forced to submit to a separation, which, as her husband by no means +entered into her scheme of their all going to Newcastle, was likely to +continue at least a twelvemonth. + +β€œOh, my dear Lydia,” she cried, β€œwhen shall we meet again?” + +β€œOh, Lord! I don’t know. Not these two or three years, perhaps.” + +β€œWrite to me very often, my dear.” + +β€œAs often as I can. But you know married women have never much time for +writing. My sisters may write to _me_. They will have nothing else to +do.” + +Mr. Wickham’s adieus were much more affectionate than his wife’s. He +smiled, looked handsome, and said many pretty things. + +β€œHe is as fine a fellow,” said Mr. Bennet, as soon as they were out of +the house, β€œas ever I saw. He simpers, and smirks, and makes love to us +all. I am prodigiously proud of him. I defy even Sir William Lucas +himself to produce a more valuable son-in-law.” + +The loss of her daughter made Mrs. Bennet very dull for several days. + +β€œI often think,” said she, β€œthat there is nothing so bad as parting with +one’s friends. One seems so forlorn without them.” + +β€œThis is the consequence, you see, madam, of marrying a daughter,” said +Elizabeth. β€œIt must make you better satisfied that your other four are +single.” + +β€œIt is no such thing. Lydia does not leave me because she is married; +but only because her husband’s regiment happens to be so far off. If +that had been nearer, she would not have gone so soon.” + +But the spiritless condition which this event threw her into was shortly +relieved, and her mind opened again to the agitation of hope, by an +article of news which then began to be in circulation. The housekeeper +at Netherfield had received orders to prepare for the arrival of her +master, who was coming down in a day or two, to shoot there for several +weeks. Mrs. Bennet was quite in the fidgets. She looked at Jane, and +smiled, and shook her head, by turns. + +β€œWell, well, and so Mr. Bingley is coming down, sister,” (for Mrs. +Philips first brought her the news). β€œWell, so much the better. Not that +I care about it, though. He is nothing to us, you know, and I am sure I +never want to see him again. But, however, he is very welcome to come to +Netherfield, if he likes it. And who knows what _may_ happen? But that +is nothing to us. You know, sister, we agreed long ago never to mention +a word about it. And so, it is quite certain he is coming?” + +β€œYou may depend on it,” replied the other, β€œfor Mrs. Nichols was in +Meryton last night: I saw her passing by, and went out myself on purpose +to know the truth of it; and she told me that it was certainly true. He +comes down on Thursday, at the latest, very likely on Wednesday. She was +going to the butcher’s, she told me, on purpose to order in some meat on +Wednesday, and she has got three couple of ducks just fit to be killed.” + +Miss Bennet had not been able to hear of his coming without changing +colour. It was many months since she had mentioned his name to +Elizabeth; but now, as soon as they were alone together, she said,-- + +β€œI saw you look at me to-day, Lizzy, when my aunt told us of the present +report; and I know I appeared distressed; but don’t imagine it was from +any silly cause. I was only confused for the moment, because I felt that +I _should_ be looked at. I do assure you that the news does not affect +me either with pleasure or pain. I am glad of one thing, that he comes +alone; because we shall see the less of him. Not that I am afraid of +_myself_, but I dread other people’s remarks.” + +Elizabeth did not know what to make of it. Had she not seen him in +Derbyshire, she might have supposed him capable of coming there with no +other view than what was acknowledged; but she still thought him partial +to Jane, and she wavered as to the greater probability of his coming +there _with_ his friend’s permission, or being bold enough to come +without it. + +β€œYet it is hard,” she sometimes thought, β€œthat this poor man cannot come +to a house, which he has legally hired, without raising all this +speculation! I _will_ leave him to himself.” + +In spite of what her sister declared, and really believed to be her +feelings, in the expectation of his arrival, Elizabeth could easily +perceive that her spirits were affected by it. They were more disturbed, +more unequal, than she had often seen them. + +The subject which had been so warmly canvassed between their parents, +about a twelvemonth ago, was now brought forward again. + +β€œAs soon as ever Mr. Bingley comes, my dear,” said Mrs. Bennet, β€œyou +will wait on him, of course.” + +β€œNo, no. You forced me into visiting him last year, and promised, if I +went to see him, he should marry one of my daughters. But it ended in +nothing, and I will not be sent on a fool’s errand again.” + +His wife represented to him how absolutely necessary such an attention +would be from all the neighbouring gentlemen, on his returning to +Netherfield. + +β€œβ€™Tis an _etiquette_ I despise,” said he. β€œIf he wants our society, let +him seek it. He knows where we live. I will not spend _my_ hours in +running after my neighbours every time they go away and come back +again.” + +β€œWell, all I know is, that it will be abominably rude if you do not wait +on him. But, however, that shan’t prevent my asking him to dine here, I +am determined. We must have Mrs. Long and the Gouldings soon. That will +make thirteen with ourselves, so there will be just room at table for +him.” + +Consoled by this resolution, she was the better able to bear her +husband’s incivility; though it was very mortifying to know that her +neighbours might all see Mr. Bingley, in consequence of it, before +_they_ did. As the day of his arrival drew near,-- + +β€œI begin to be sorry that he comes at all,” said Jane to her sister. β€œIt +would be nothing; I could see him with perfect indifference; but I can +hardly bear to hear it thus perpetually talked of. My mother means well; +but she does not know, no one can know, how much I suffer from what she +says. Happy shall I be when his stay at Netherfield is over!” + +β€œI wish I could say anything to comfort you,” replied Elizabeth; β€œbut it +is wholly out of my power. You must feel it; and the usual satisfaction +of preaching patience to a sufferer is denied me, because you have +always so much.” + +Mr. Bingley arrived. Mrs. Bennet, through the assistance of servants, +contrived to have the earliest tidings of it, that the period of anxiety +and fretfulness on her side be as long as it could. She counted the days +that must intervene before their invitation could be sent--hopeless of +seeing him before. But on the third morning after his arrival in +Hertfordshire, she saw him from her dressing-room window enter the +paddock, and ride towards the house. + +Her daughters were eagerly called to partake of her joy. Jane resolutely +kept her place at the table; but Elizabeth, to satisfy her mother, went +to the window--she looked--she saw Mr. Darcy with him, and sat down +again by her sister. + +β€œThere is a gentleman with him, mamma,” said Kitty; β€œwho can it be?” + +β€œSome acquaintance or other, my dear, I suppose; I am sure I do not +know.” + +β€œLa!” replied Kitty, β€œit looks just like that man that used to be with +him before. Mr. what’s his name--that tall, proud man.” + +β€œGood gracious! Mr. Darcy!--and so it does, I vow. Well, any friend of +Mr. Bingley’s will always be welcome here, to be sure; but else I must +say that I hate the very sight of him.” + +Jane looked at Elizabeth with surprise and concern. She knew but little +of their meeting in Derbyshire, and therefore felt for the awkwardness +which must attend her sister, in seeing him almost for the first time +after receiving his explanatory letter. Both sisters were uncomfortable +enough. Each felt for the other, and of course for themselves; and their +mother talked on of her dislike of Mr. Darcy, and her resolution to be +civil to him only as Mr. Bingley’s friend, without being heard by either +of them. But Elizabeth had sources of uneasiness which could not yet be +suspected by Jane, to whom she had never yet had courage to show Mrs. +Gardiner’s letter, or to relate her own change of sentiment towards +him. To Jane, he could be only a man whose proposals she had refused, +and whose merits she had undervalued; but to her own more extensive +information, he was the person to whom the whole family were indebted +for the first of benefits, and whom she regarded herself with an +interest, if not quite so tender, at least as reasonable and just, as +what Jane felt for Bingley. Her astonishment at his coming--at his +coming to Netherfield, to Longbourn, and voluntarily seeking her again, +was almost equal to what she had known on first witnessing his altered +behaviour in Derbyshire. + +The colour which had been driven from her face returned for half a +minute with an additional glow, and a smile of delight added lustre to +her eyes, as she thought for that space of time that his affection and +wishes must still be unshaken; but she would not be secure. + +β€œLet me first see how he behaves,” said she; β€œit will then be early +enough for expectation.” + +She sat intently at work, striving to be composed, and without daring to +lift up her eyes, till anxious curiosity carried them to the face of her +sister as the servant was approaching the door. Jane looked a little +paler than usual, but more sedate than Elizabeth had expected. On the +gentlemen’s appearing, her colour increased; yet she received them with +tolerable ease, and with a propriety of behaviour equally free from any +symptom of resentment, or any unnecessary complaisance. + +Elizabeth said as little to either as civility would allow, and sat down +again to her work, with an eagerness which it did not often command. She +had ventured only one glance at Darcy. He looked serious as usual; and, +she thought, more as he had been used to look in Hertfordshire, than as +she had seen him at Pemberley. But, perhaps, he could not in her +mother’s presence be what he was before her uncle and aunt. It was a +painful, but not an improbable, conjecture. + +Bingley she had likewise seen for an instant, and in that short period +saw him looking both pleased and embarrassed. He was received by Mrs. +Bennet with a degree of civility which made her two daughters ashamed, +especially when contrasted with the cold and ceremonious politeness of +her courtesy and address of his friend. + +Elizabeth particularly, who knew that her mother owed to the latter the +preservation of her favourite daughter from irremediable infamy, was +hurt and distressed to a most painful degree by a distinction so ill +applied. + +Darcy, after inquiring of her how Mr. and Mrs. Gardiner did--a question +which she could not answer without confusion--said scarcely anything. He +was not seated by her: perhaps that was the reason of his silence; but +it had not been so in Derbyshire. There he had talked to her friends +when he could not to herself. But now several minutes elapsed, without +bringing the sound of his voice; and when occasionally, unable to resist +the impulse of curiosity, she raised her eyes to his face, she as often +found him looking at Jane as at herself, and frequently on no object but +the ground. More thoughtfulness and less anxiety to please, than when +they last met, were plainly expressed. She was disappointed, and angry +with herself for being so. + +β€œCould I expect it to be otherwise?” said she. β€œYet why did he come?” + +She was in no humour for conversation with anyone but himself; and to +him she had hardly courage to speak. + +She inquired after his sister, but could do no more. + +β€œIt is a long time, Mr. Bingley, since you went away,” said Mrs. Bennet. + +He readily agreed to it. + +β€œI began to be afraid you would never come back again. People _did_ say, +you meant to quit the place entirely at Michaelmas; but, however, I hope +it is not true. A great many changes have happened in the neighbourhood +since you went away. Miss Lucas is married and settled: and one of my +own daughters. I suppose you have heard of it; indeed, you must have +seen it in the papers. It was in the β€˜Times’ and the β€˜Courier,’ I know; +though it was not put in as it ought to be. It was only said, β€˜Lately, +George Wickham, Esq., to Miss Lydia Bennet,’ without there being a +syllable said of her father, or the place where she lived, or anything. +It was my brother Gardiner’s drawing up, too, and I wonder how he came +to make such an awkward business of it. Did you see it?” + +Bingley replied that he did, and made his congratulations. Elizabeth +dared not lift up her eyes. How Mr. Darcy looked, therefore, she could +not tell. + +β€œIt is a delightful thing, to be sure, to have a daughter well married,” +continued her mother; β€œbut at the same time, Mr. Bingley, it is very +hard to have her taken away from me. They are gone down to Newcastle, a +place quite northward it seems, and there they are to stay, I do not +know how long. His regiment is there; for I suppose you have heard of +his leaving the ----shire, and of his being gone into the Regulars. +Thank heaven! he has _some_ friends, though, perhaps, not so many as he +deserves.” + +Elizabeth, who knew this to be levelled at Mr. Darcy, was in such misery +of shame that she could hardly keep her seat. It drew from her, however, +the exertion of speaking, which nothing else had so effectually done +before; and she asked Bingley whether he meant to make any stay in the +country at present. A few weeks, he believed. + +β€œWhen you have killed all your own birds, Mr. Bingley,” said her mother, +β€œI beg you will come here and shoot as many as you please on Mr. +Bennet’s manor. I am sure he will be vastly happy to oblige you, and +will save all the best of the coveys for you.” + +Elizabeth’s misery increased at such unnecessary, such officious +attention! Were the same fair prospect to arise at present, as had +flattered them a year ago, everything, she was persuaded, would be +hastening to the same vexatious conclusion. At that instant she felt, +that years of happiness could not make Jane or herself amends for +moments of such painful confusion. + +β€œThe first wish of my heart,” said she to herself, β€œis never more to be +in company with either of them. Their society can afford no pleasure +that will atone for such wretchedness as this! Let me never see either +one or the other again!” + +Yet the misery, for which years of happiness were to offer no +compensation, received soon afterwards material relief, from observing +how much the beauty of her sister rekindled the admiration of her former +lover. When first he came in, he had spoken to her but little, but every +five minutes seemed to be giving her more of his attention. He found her +as handsome as she had been last year; as good-natured, and as +unaffected, though not quite so chatty. Jane was anxious that no +difference should be perceived in her at all, and was really persuaded +that she talked as much as ever; but her mind was so busily engaged, +that she did not always know when she was silent. + +When the gentlemen rose to go away, Mrs. Bennet was mindful of her +intended civility, and they were invited and engaged to dine at +Longbourn in a few days’ time. + +β€œYou are quite a visit in my debt, Mr. Bingley,” she added; β€œfor when +you went to town last winter, you promised to take a family dinner with +us as soon as you returned. I have not forgot, you see; and I assure you +I was very much disappointed that you did not come back and keep your +engagement.” + +Bingley looked a little silly at this reflection, and said something of +his concern at having been prevented by business. They then went away. + +Mrs. Bennet had been strongly inclined to ask them to stay and dine +there that day; but, though she always kept a very good table, she did +not think anything less than two courses could be good enough for a man +on whom she had such anxious designs, or satisfy the appetite and pride +of one who had ten thousand a year. + + + + +[Illustration: + + β€œJane happened to look round” +] + + + + +CHAPTER LIV. + + +[Illustration] + +As soon as they were gone, Elizabeth walked out to recover her spirits; +or, in other words, to dwell without interruption on those subjects +which must deaden them more. Mr. Darcy’s behaviour astonished and vexed +her. + +β€œWhy, if he came only to be silent, grave, and indifferent,” said she, +β€œdid he come at all?” + +She could settle it in no way that gave her pleasure. + +β€œHe could be still amiable, still pleasing to my uncle and aunt, when he +was in town; and why not to me? If he fears me, why come hither? If he +no longer cares for me, why silent? Teasing, teasing man! I will think +no more about him.” + +Her resolution was for a short time involuntarily kept by the approach +of her sister, who joined her with a cheerful look which showed her +better satisfied with their visitors than Elizabeth. + +β€œNow,” said she, β€œthat this first meeting is over, I feel perfectly +easy. I know my own strength, and I shall never be embarrassed again by +his coming. I am glad he dines here on Tuesday. It will then be publicly +seen, that on both sides we meet only as common and indifferent +acquaintance.” + +β€œYes, very indifferent, indeed,” said Elizabeth, laughingly. β€œOh, Jane! +take care.” + +β€œMy dear Lizzy, you cannot think me so weak as to be in danger now.” + +β€œI think you are in very great danger of making him as much in love with +you as ever.” + +They did not see the gentlemen again till Tuesday; and Mrs. Bennet, in +the meanwhile, was giving way to all the happy schemes which the +good-humour and common politeness of Bingley, in half an hour’s visit, +had revived. + +On Tuesday there was a large party assembled at Longbourn; and the two +who were most anxiously expected, to the credit of their punctuality as +sportsmen, were in very good time. When they repaired to the +dining-room, Elizabeth eagerly watched to see whether Bingley would take +the place which, in all their former parties, had belonged to him, by +her sister. Her prudent mother, occupied by the same ideas, forbore to +invite him to sit by herself. On entering the room, he seemed to +hesitate; but Jane happened to look round, and happened to smile: it was +decided. He placed himself by her. + +Elizabeth, with a triumphant sensation, looked towards his friend. He +bore it with noble indifference; and she would have imagined that +Bingley had received his sanction to be happy, had she not seen his eyes +likewise turned towards Mr. Darcy, with an expression of half-laughing +alarm. + +His behaviour to her sister was such during dinnertime as showed an +admiration of her, which, though more guarded than formerly, persuaded +Elizabeth, that, if left wholly to himself, Jane’s happiness, and his +own, would be speedily secured. Though she dared not depend upon the +consequence, she yet received pleasure from observing his behaviour. It +gave her all the animation that her spirits could boast; for she was in +no cheerful humour. Mr. Darcy was almost as far from her as the table +could divide them. He was on one side of her mother. She knew how little +such a situation would give pleasure to either, or make either appear to +advantage. She was not near enough to hear any of their discourse; but +she could see how seldom they spoke to each other, and how formal and +cold was their manner whenever they did. Her mother’s ungraciousness +made the sense of what they owed him more painful to Elizabeth’s mind; +and she would, at times, have given anything to be privileged to tell +him, that his kindness was neither unknown nor unfelt by the whole of +the family. + +She was in hopes that the evening would afford some opportunity of +bringing them together; that the whole of the visit would not pass away +without enabling them to enter into something more of conversation, +than the mere ceremonious salutation attending his entrance. Anxious and +uneasy, the period which passed in the drawing-room before the gentlemen +came, was wearisome and dull to a degree that almost made her uncivil. +She looked forward to their entrance as the point on which all her +chance of pleasure for the evening must depend. + +β€œIf he does not come to me, _then_,” said she, β€œI shall give him up for +ever.” + +The gentlemen came; and she thought he looked as if he would have +answered her hopes; but, alas! the ladies had crowded round the table, +where Miss Bennet was making tea, and Elizabeth pouring out the coffee, +in so close a confederacy, that there was not a single vacancy near her +which would admit of a chair. And on the gentlemen’s approaching, one of +the girls moved closer to her than ever, and said, in a whisper,-- + +β€œThe men shan’t come and part us, I am determined. We want none of them; +do we?” + +Darcy had walked away to another part of the room. She followed him with +her eyes, envied everyone to whom he spoke, had scarcely patience enough +to help anybody to coffee, and then was enraged against herself for +being so silly! + +β€œA man who has once been refused! How could I ever be foolish enough to +expect a renewal of his love? Is there one among the sex who would not +protest against such a weakness as a second proposal to the same woman? +There is no indignity so abhorrent to their feelings.” + +She was a little revived, however, by his bringing back his coffee-cup +himself; and she seized the opportunity of saying,-- + +β€œIs your sister at Pemberley still?” + +β€œYes; she will remain there till Christmas.” + +β€œAnd quite alone? Have all her friends left her?” + +β€œMrs. Annesley is with her. The others have been gone on to Scarborough +these three weeks.” + +She could think of nothing more to say; but if he wished to converse +with her, he might have better success. He stood by her, however, for +some minutes, in silence; and, at last, on the young lady’s whispering +to Elizabeth again, he walked away. + +When the tea things were removed, and the card tables placed, the ladies +all rose; and Elizabeth was then hoping to be soon joined by him, when +all her views were overthrown, by seeing him fall a victim to her +mother’s rapacity for whist players, and in a few moments after seated +with the rest of the party. She now lost every expectation of pleasure. +They were confined for the evening at different tables; and she had +nothing to hope, but that his eyes were so often turned towards her side +of the room, as to make him play as unsuccessfully as herself. + +Mrs. Bennet had designed to keep the two Netherfield gentlemen to +supper; but their carriage was, unluckily, ordered before any of the +others, and she had no opportunity of detaining them. + +β€œWell, girls,” said she, as soon as they were left to themselves, β€œwhat +say you to the day? I think everything has passed off uncommonly well, I +assure you. The dinner was as well dressed as any I ever saw. The +venison was roasted to a turn--and everybody said, they never saw so fat +a haunch. The soup was fifty times better than what we had at the +Lucases’ last week; and even Mr. Darcy acknowledged that the partridges +were remarkably well done; and I suppose he has two or three French +cooks at least. And, my dear Jane, I never saw you look in greater +beauty. Mrs. Long said so too, for I asked her whether you did not. And +what do you think she said besides? β€˜Ah! Mrs. Bennet, we shall have her +at Netherfield at last!’ She did, indeed. I do think Mrs. Long is as +good a creature as ever lived--and her nieces are very pretty behaved +girls, and not at all handsome: I like them prodigiously.” + +[Illustration: + + β€œM^{rs}. Long and her nieces.” +] + +Mrs. Bennet, in short, was in very great spirits: she had seen enough of +Bingley’s behaviour to Jane to be convinced that she would get him at +last; and her expectations of advantage to her family, when in a happy +humour, were so far beyond reason, that she was quite disappointed at +not seeing him there again the next day, to make his proposals. + +β€œIt has been a very agreeable day,” said Miss Bennet to Elizabeth. β€œThe +party seemed so well selected, so suitable one with the other. I hope we +may often meet again.” + +Elizabeth smiled. + +β€œLizzy, you must not do so. You must not suspect me. It mortifies me. I +assure you that I have now learnt to enjoy his conversation as an +agreeable and sensible young man without having a wish beyond it. I am +perfectly satisfied, from what his manners now are, that he never had +any design of engaging my affection. It is only that he is blessed with +greater sweetness of address, and a stronger desire of generally +pleasing, than any other man.” + +β€œYou are very cruel,” said her sister, β€œyou will not let me smile, and +are provoking me to it every moment.” + +β€œHow hard it is in some cases to be believed! And how impossible in +others! But why should you wish to persuade me that I feel more than I +acknowledge?” + +β€œThat is a question which I hardly know how to answer. We all love to +instruct, though we can teach only what is not worth knowing. Forgive +me; and if you persist in indifference, do not make _me_ your +confidante.” + + + + +[Illustration: + + β€œLizzy, my dear, I want to speak to you.” +] + + + + +CHAPTER LV. + + +[Illustration] + +A few days after this visit, Mr. Bingley called again, and alone. His +friend had left him that morning for London, but was to return home in +ten days’ time. He sat with them above an hour, and was in remarkably +good spirits. Mrs. Bennet invited him to dine with them; but, with many +expressions of concern, he confessed himself engaged elsewhere. + +β€œNext time you call,” said she, β€œI hope we shall be more lucky.” + +He should be particularly happy at any time, etc., etc.; and if she +would give him leave, would take an early opportunity of waiting on +them. + +β€œCan you come to-morrow?” + +Yes, he had no engagement at all for to-morrow; and her invitation was +accepted with alacrity. + +He came, and in such very good time, that the ladies were none of them +dressed. In ran Mrs. Bennet to her daughters’ room, in her +dressing-gown, and with her hair half finished, crying out,-- + +β€œMy dear Jane, make haste and hurry down. He is come--Mr. Bingley is +come. He is, indeed. Make haste, make haste. Here, Sarah, come to Miss +Bennet this moment, and help her on with her gown. Never mind Miss +Lizzy’s hair.” + +β€œWe will be down as soon as we can,” said Jane; β€œbut I dare say Kitty is +forwarder than either of us, for she went upstairs half an hour ago.” + +β€œOh! hang Kitty! what has she to do with it? Come, be quick, be quick! +where is your sash, my dear?” + +But when her mother was gone, Jane would not be prevailed on to go down +without one of her sisters. + +The same anxiety to get them by themselves was visible again in the +evening. After tea, Mr. Bennet retired to the library, as was his +custom, and Mary went upstairs to her instrument. Two obstacles of the +five being thus removed, Mrs. Bennet sat looking and winking at +Elizabeth and Catherine for a considerable time, without making any +impression on them. Elizabeth would not observe her; and when at last +Kitty did, she very innocently said, β€œWhat is the matter, mamma? What do +you keep winking at me for? What am I to do?” + +β€œNothing, child, nothing. I did not wink at you.” She then sat still +five minutes longer; but unable to waste such a precious occasion, she +suddenly got up, and saying to Kitty,-- + +β€œCome here, my love, I want to speak to you,” took her out of the room. +Jane instantly gave a look at Elizabeth which spoke her distress at such +premeditation, and her entreaty that _she_ would not give in to it. In a +few minutes, Mrs. Bennet half opened the door and called out,-- + +β€œLizzy, my dear, I want to speak with you.” + +Elizabeth was forced to go. + +β€œWe may as well leave them by themselves, you know,” said her mother as +soon as she was in the hall. β€œKitty and I are going upstairs to sit in +my dressing-room.” + +Elizabeth made no attempt to reason with her mother, but remained +quietly in the hall till she and Kitty were out of sight, then returned +into the drawing-room. + +Mrs. Bennet’s schemes for this day were ineffectual. Bingley was +everything that was charming, except the professed lover of her +daughter. His ease and cheerfulness rendered him a most agreeable +addition to their evening party; and he bore with the ill-judged +officiousness of the mother, and heard all her silly remarks with a +forbearance and command of countenance particularly grateful to the +daughter. + +He scarcely needed an invitation to stay supper; and before he went away +an engagement was formed, chiefly through his own and Mrs. Bennet’s +means, for his coming next morning to shoot with her husband. + +After this day, Jane said no more of her indifference. Not a word passed +between the sisters concerning Bingley; but Elizabeth went to bed in the +happy belief that all must speedily be concluded, unless Mr. Darcy +returned within the stated time. Seriously, however, she felt tolerably +persuaded that all this must have taken place with that gentleman’s +concurrence. + +Bingley was punctual to his appointment; and he and Mr. Bennet spent the +morning together, as had been agreed on. The latter was much more +agreeable than his companion expected. There was nothing of presumption +or folly in Bingley that could provoke his ridicule, or disgust him into +silence; and he was more communicative, and less eccentric, than the +other had ever seen him. Bingley of course returned with him to dinner; +and in the evening Mrs. Bennet’s invention was again at work to get +everybody away from him and her daughter. Elizabeth, who had a letter to +write, went into the breakfast-room for that purpose soon after tea; for +as the others were all going to sit down to cards, she could not be +wanted to counteract her mother’s schemes. + +But on her returning to the drawing-room, when her letter was finished, +she saw, to her infinite surprise, there was reason to fear that her +mother had been too ingenious for her. On opening the door, she +perceived her sister and Bingley standing together over the hearth, as +if engaged in earnest conversation; and had this led to no suspicion, +the faces of both, as they hastily turned round and moved away from each +other, would have told it all. _Their_ situation was awkward enough; but +_hers_ she thought was still worse. Not a syllable was uttered by +either; and Elizabeth was on the point of going away again, when +Bingley, who as well as the other had sat down, suddenly rose, and, +whispering a few words to her sister, ran out of the room. + +Jane could have no reserves from Elizabeth, where confidence would give +pleasure; and, instantly embracing her, acknowledged, with the liveliest +emotion, that she was the happiest creature in the world. + +β€œβ€™Tis too much!” she added, β€œby far too much. I do not deserve it. Oh, +why is not everybody as happy?” + +Elizabeth’s congratulations were given with a sincerity, a warmth, a +delight, which words could but poorly express. Every sentence of +kindness was a fresh source of happiness to Jane. But she would not +allow herself to stay with her sister, or say half that remained to be +said, for the present. + +β€œI must go instantly to my mother,” she cried. β€œI would not on any +account trifle with her affectionate solicitude, or allow her to hear it +from anyone but myself. He is gone to my father already. Oh, Lizzy, to +know that what I have to relate will give such pleasure to all my dear +family! how shall I bear so much happiness?” + +She then hastened away to her mother, who had purposely broken up the +card-party, and was sitting upstairs with Kitty. + +Elizabeth, who was left by herself, now smiled at the rapidity and ease +with which an affair was finally settled, that had given them so many +previous months of suspense and vexation. + +β€œAnd this,” said she, β€œis the end of all his friend’s anxious +circumspection! of all his sister’s falsehood and contrivance! the +happiest, wisest, and most reasonable end!” + +In a few minutes she was joined by Bingley, whose conference with her +father had been short and to the purpose. + +β€œWhere is your sister?” said he hastily, as he opened the door. + +β€œWith my mother upstairs. She will be down in a moment, I dare say.” + +He then shut the door, and, coming up to her, claimed the good wishes +and affection of a sister. Elizabeth honestly and heartily expressed her +delight in the prospect of their relationship. They shook hands with +great cordiality; and then, till her sister came down, she had to listen +to all he had to say of his own happiness, and of Jane’s perfections; +and in spite of his being a lover, Elizabeth really believed all his +expectations of felicity to be rationally founded, because they had for +basis the excellent understanding and super-excellent disposition of +Jane, and a general similarity of feeling and taste between her and +himself. + +It was an evening of no common delight to them all; the satisfaction of +Miss Bennet’s mind gave such a glow of sweet animation to her face, as +made her look handsomer than ever. Kitty simpered and smiled, and hoped +her turn was coming soon. Mrs. Bennet could not give her consent, or +speak her approbation in terms warm enough to satisfy her feelings, +though she talked to Bingley of nothing else, for half an hour; and when +Mr. Bennet joined them at supper, his voice and manner plainly showed +how really happy he was. + +Not a word, however, passed his lips in allusion to it, till their +visitor took his leave for the night; but as soon as he was gone, he +turned to his daughter and said,-- + +β€œJane, I congratulate you. You will be a very happy woman.” + +Jane went to him instantly, kissed him, and thanked him for his +goodness. + +β€œYou are a good girl,” he replied, β€œand I have great pleasure in +thinking you will be so happily settled. I have not a doubt of your +doing very well together. Your tempers are by no means unlike. You are +each of you so complying, that nothing will ever be resolved on; so +easy, that every servant will cheat you; and so generous, that you will +always exceed your income.” + +β€œI hope not so. Imprudence or thoughtlessness in money matters would be +unpardonable in _me_.” + +β€œExceed their income! My dear Mr. Bennet,” cried his wife, β€œwhat are you +talking of? Why, he has four or five thousand a year, and very likely +more.” Then addressing her daughter, β€œOh, my dear, dear Jane, I am so +happy! I am sure I shan’t get a wink of sleep all night. I knew how it +would be. I always said it must be so, at last. I was sure you could not +be so beautiful for nothing! I remember, as soon as ever I saw him, when +he first came into Hertfordshire last year, I thought how likely it was +that you should come together. Oh, he is the handsomest young man that +ever was seen!” + +Wickham, Lydia, were all forgotten. Jane was beyond competition her +favourite child. At that moment she cared for no other. Her younger +sisters soon began to make interest with her for objects of happiness +which she might in future be able to dispense. + +Mary petitioned for the use of the library at Netherfield; and Kitty +begged very hard for a few balls there every winter. + +Bingley, from this time, was of course a daily visitor at Longbourn; +coming frequently before breakfast, and always remaining till after +supper; unless when some barbarous neighbour, who could not be enough +detested, had given him an invitation to dinner, which he thought +himself obliged to accept. + +Elizabeth had now but little time for conversation with her sister; for +while he was present Jane had no attention to bestow on anyone else: but +she found herself considerably useful to both of them, in those hours of +separation that must sometimes occur. In the absence of Jane, he always +attached himself to Elizabeth for the pleasure of talking of her; and +when Bingley was gone, Jane constantly sought the same means of relief. + +β€œHe has made me so happy,” said she, one evening, β€œby telling me that he +was totally ignorant of my being in town last spring! I had not believed +it possible.” + +β€œI suspected as much,” replied Elizabeth. β€œBut how did he account for +it?” + +β€œIt must have been his sisters’ doing. They were certainly no friends to +his acquaintance with me, which I cannot wonder at, since he might have +chosen so much more advantageously in many respects. But when they see, +as I trust they will, that their brother is happy with me, they will +learn to be contented, and we shall be on good terms again: though we +can never be what we once were to each other.” + +β€œThat is the most unforgiving speech,” said Elizabeth, β€œthat I ever +heard you utter. Good girl! It would vex me, indeed, to see you again +the dupe of Miss Bingley’s pretended regard.” + +β€œWould you believe it, Lizzy, that when he went to town last November he +really loved me, and nothing but a persuasion of _my_ being indifferent +would have prevented his coming down again?” + +β€œHe made a little mistake, to be sure; but it is to the credit of his +modesty.” + +This naturally introduced a panegyric from Jane on his diffidence, and +the little value he put on his own good qualities. + +Elizabeth was pleased to find that he had not betrayed the interference +of his friend; for, though Jane had the most generous and forgiving +heart in the world, she knew it was a circumstance which must prejudice +her against him. + +β€œI am certainly the most fortunate creature that ever existed!” cried +Jane. β€œOh, Lizzy, why am I thus singled from my family, and blessed +above them all? If I could but see you as happy! If there were but such +another man for you!” + +β€œIf you were to give me forty such men I never could be so happy as you. +Till I have your disposition, your goodness, I never can have your +happiness. No, no, let me shift for myself; and, perhaps, if I have very +good luck, I may meet with another Mr. Collins in time.” + +The situation of affairs in the Longbourn family could not be long a +secret. Mrs. Bennet was privileged to whisper it to Mrs. Philips, and +she ventured, without any permission, to do the same by all her +neighbours in Meryton. + +The Bennets were speedily pronounced to be the luckiest family in the +world; though only a few weeks before, when Lydia had first run away, +they had been generally proved to be marked out for misfortune. + + + + +[Illustration] + + + + +CHAPTER LVI. + + +[Illustration] + +One morning, about a week after Bingley’s engagement with Jane had been +formed, as he and the females of the family were sitting together in the +dining-room, their attention was suddenly drawn to the window by the +sound of a carriage; and they perceived a chaise and four driving up the +lawn. It was too early in the morning for visitors; and besides, the +equipage did not answer to that of any of their neighbours. The horses +were post; and neither the carriage, nor the livery of the servant who +preceded it, were familiar to them. As it was certain, however, that +somebody was coming, Bingley instantly prevailed on Miss Bennet to avoid +the confinement of such an intrusion, and walk away with him into the +shrubbery. They both set off; and the conjectures of the remaining three +continued, though with little satisfaction, till the door was thrown +open, and their visitor entered. It was Lady Catherine de Bourgh. + +They were of course all intending to be surprised: but their +astonishment was beyond their expectation; and on the part of Mrs. +Bennet and Kitty, though she was perfectly unknown to them, even +inferior to what Elizabeth felt. + +She entered the room with an air more than usually ungracious, made no +other reply to Elizabeth’s salutation than a slight inclination of the +head, and sat down without saying a word. Elizabeth had mentioned her +name to her mother on her Ladyship’s entrance, though no request of +introduction had been made. + +Mrs. Bennet, all amazement, though flattered by having a guest of such +high importance, received her with the utmost politeness. After sitting +for a moment in silence, she said, very stiffly, to Elizabeth,-- + +β€œI hope you are well, Miss Bennet. That lady, I suppose, is your +mother?” + +Elizabeth replied very concisely that she was. + +β€œAnd _that_, I suppose, is one of your sisters?” + +β€œYes, madam,” said Mrs. Bennet, delighted to speak to a Lady Catherine. +β€œShe is my youngest girl but one. My youngest of all is lately married, +and my eldest is somewhere about the ground, walking with a young man, +who, I believe, will soon become a part of the family.” + +β€œYou have a very small park here,” returned Lady Catherine, after a +short silence. + +β€œIt is nothing in comparison of Rosings, my Lady, I dare say; but, I +assure you, it is much larger than Sir William Lucas’s.” + +β€œThis must be a most inconvenient sitting-room for the evening in +summer: the windows are full west.” + +Mrs. Bennet assured her that they never sat there after dinner; and then +added,-- + +β€œMay I take the liberty of asking your Ladyship whether you left Mr. and +Mrs. Collins well?” + +β€œYes, very well. I saw them the night before last.” + +Elizabeth now expected that she would produce a letter for her from +Charlotte, as it seemed the only probable motive for her calling. But no +letter appeared, and she was completely puzzled. + +Mrs. Bennet, with great civility, begged her Ladyship to take some +refreshment: but Lady Catherine very resolutely, and not very politely, +declined eating anything; and then, rising up, said to Elizabeth,-- + +β€œMiss Bennet, there seemed to be a prettyish kind of a little wilderness +on one side of your lawn. I should be glad to take a turn in it, if you +will favour me with your company.” + +β€œGo, my dear,” cried her mother, β€œand show her Ladyship about the +different walks. I think she will be pleased with the hermitage.” + +Elizabeth obeyed; and, running into her own room for her parasol, +attended her noble guest downstairs. As they passed through the hall, +Lady Catherine opened the doors into the dining-parlour and +drawing-room, and pronouncing them, after a short survey, to be +decent-looking rooms, walked on. + +Her carriage remained at the door, and Elizabeth saw that her +waiting-woman was in it. They proceeded in silence along the gravel walk +that led to the copse; Elizabeth was determined to make no effort for +conversation with a woman who was now more than usually insolent and +disagreeable. + +[Illustration: + +β€œAfter a short survey” + +[_Copyright 1894 by George Allen._]] + +β€œHow could I ever think her like her nephew?” said she, as she looked in +her face. + +As soon as they entered the copse, Lady Catherine began in the following +manner:-- + +β€œYou can be at no loss, Miss Bennet, to understand the reason of my +journey hither. Your own heart, your own conscience, must tell you why I +come.” + +Elizabeth looked with unaffected astonishment. + +β€œIndeed, you are mistaken, madam; I have not been at all able to account +for the honour of seeing you here.” + +β€œMiss Bennet,” replied her Ladyship, in an angry tone, β€œyou ought to +know that I am not to be trifled with. But however insincere _you_ may +choose to be, you shall not find _me_ so. My character has ever been +celebrated for its sincerity and frankness; and in a cause of such +moment as this, I shall certainly not depart from it. A report of a most +alarming nature reached me two days ago. I was told, that not only your +sister was on the point of being most advantageously married, but that +_you_--that Miss Elizabeth Bennet would, in all likelihood, be soon +afterwards united to my nephew--my own nephew, Mr. Darcy. Though I +_know_ it must be a scandalous falsehood, though I would not injure him +so much as to suppose the truth of it possible, I instantly resolved on +setting off for this place, that I might make my sentiments known to +you.” + +β€œIf you believed it impossible to be true,” said Elizabeth, colouring +with astonishment and disdain, β€œI wonder you took the trouble of coming +so far. What could your Ladyship propose by it?” + +β€œAt once to insist upon having such a report universally contradicted.” + +β€œYour coming to Longbourn, to see me and my family,” said Elizabeth +coolly, β€œwill be rather a confirmation of it--if, indeed, such a report +is in existence.” + +β€œIf! do you then pretend to be ignorant of it? Has it not been +industriously circulated by yourselves? Do you not know that such a +report is spread abroad?” + +β€œI never heard that it was.” + +β€œAnd can you likewise declare, that there is no _foundation_ for it?” + +β€œI do not pretend to possess equal frankness with your Ladyship. _You_ +may ask questions which _I_ shall not choose to answer.” + +β€œThis is not to be borne. Miss Bennet, I insist on being satisfied. Has +he, has my nephew, made you an offer of marriage?” + +β€œYour Ladyship has declared it to be impossible.” + +β€œIt ought to be so; it must be so, while he retains the use of his +reason. But _your_ arts and allurements may, in a moment of infatuation, +have made him forget what he owes to himself and to all his family. You +may have drawn him in.” + +β€œIf I have, I shall be the last person to confess it.” + +β€œMiss Bennet, do you know who I am? I have not been accustomed to such +language as this. I am almost the nearest relation he has in the world, +and am entitled to know all his dearest concerns.” + +β€œBut you are not entitled to know _mine_; nor will such behaviour as +this ever induce me to be explicit.” + +β€œLet me be rightly understood. This match, to which you have the +presumption to aspire, can never take place. No, never. Mr. Darcy is +engaged to _my daughter_. Now, what have you to say?” + +β€œOnly this,--that if he is so, you can have no reason to suppose he will +make an offer to me.” + +Lady Catherine hesitated for a moment, and then replied,-- + +β€œThe engagement between them is of a peculiar kind. From their infancy, +they have been intended for each other. It was the favourite wish of +_his_ mother, as well as of hers. While in their cradles we planned the +union; and now, at the moment when the wishes of both sisters would be +accomplished, is their marriage to be prevented by a young woman of +inferior birth, of no importance in the world, and wholly unallied to +the family? Do you pay no regard to the wishes of his friends--to his +tacit engagement with Miss de Bourgh? Are you lost to every feeling of +propriety and delicacy? Have you not heard me say, that from his +earliest hours he was destined for his cousin?” + +β€œYes; and I had heard it before. But what is that to me? If there is no +other objection to my marrying your nephew, I shall certainly not be +kept from it by knowing that his mother and aunt wished him to marry +Miss de Bourgh. You both did as much as you could in planning the +marriage. Its completion depended on others. If Mr. Darcy is neither by +honour nor inclination confined to his cousin, why is not he to make +another choice? And if I am that choice, why may not I accept him?” + +β€œBecause honour, decorum, prudence--nay, interest--forbid it. Yes, Miss +Bennet, interest; for do not expect to be noticed by his family or +friends, if you wilfully act against the inclinations of all. You will +be censured, slighted, and despised, by everyone connected with him. +Your alliance will be a disgrace; your name will never even be mentioned +by any of us.” + +β€œThese are heavy misfortunes,” replied Elizabeth. β€œBut the wife of Mr. +Darcy must have such extraordinary sources of happiness necessarily +attached to her situation, that she could, upon the whole, have no cause +to repine.” + +β€œObstinate, headstrong girl! I am ashamed of you! Is this your gratitude +for my attentions to you last spring? Is nothing due to me on that +score? Let us sit down. You are to understand, Miss Bennet, that I came +here with the determined resolution of carrying my purpose; nor will I +be dissuaded from it. I have not been used to submit to any person’s +whims. I have not been in the habit of brooking disappointment.” + +β€œ_That_ will make your Ladyship’s situation at present more pitiable; +but it will have no effect on _me_.” + +β€œI will not be interrupted! Hear me in silence. My daughter and my +nephew are formed for each other. They are descended, on the maternal +side, from the same noble line; and, on the father’s, from respectable, +honourable, and ancient, though untitled, families. Their fortune on +both sides is splendid. They are destined for each other by the voice of +every member of their respective houses; and what is to divide +them?--the upstart pretensions of a young woman without family, +connections, or fortune! Is this to be endured? But it must not, shall +not be! If you were sensible of your own good, you would not wish to +quit the sphere in which you have been brought up.” + +β€œIn marrying your nephew, I should not consider myself as quitting that +sphere. He is a gentleman; I am a gentleman’s daughter; so far we are +equal.” + +β€œTrue. You _are_ a gentleman’s daughter. But what was your mother? Who +are your uncles and aunts? Do not imagine me ignorant of their +condition.” + +β€œWhatever my connections may be,” said Elizabeth, β€œif your nephew does +not object to them, they can be nothing to _you_.” + +β€œTell me, once for all, are you engaged to him?” + +Though Elizabeth would not, for the mere purpose of obliging Lady +Catherine, have answered this question, she could not but say, after a +moment’s deliberation,-- + +β€œI am not.” + +Lady Catherine seemed pleased. + +β€œAnd will you promise me never to enter into such an engagement?” + +β€œI will make no promise of the kind.” + +β€œMiss Bennet, I am shocked and astonished. I expected to find a more +reasonable young woman. But do not deceive yourself into a belief that I +will ever recede. I shall not go away till you have given me the +assurance I require.” + +β€œAnd I certainly _never_ shall give it. I am not to be intimidated into +anything so wholly unreasonable. Your Ladyship wants Mr. Darcy to marry +your daughter; but would my giving you the wished-for promise make +_their_ marriage at all more probable? Supposing him to be attached to +me, would _my_ refusing to accept his hand make him wish to bestow it on +his cousin? Allow me to say, Lady Catherine, that the arguments with +which you have supported this extraordinary application have been as +frivolous as the application was ill-judged. You have widely mistaken my +character, if you think I can be worked on by such persuasions as these. +How far your nephew might approve of your interference in _his_ affairs, +I cannot tell; but you have certainly no right to concern yourself in +mine. I must beg, therefore, to be importuned no further on the +subject.” + +β€œNot so hasty, if you please. I have by no means done. To all the +objections I have already urged I have still another to add. I am no +stranger to the particulars of your youngest sister’s infamous +elopement. I know it all; that the young man’s marrying her was a +patched-up business, at the expense of your father and uncle. And is +_such_ a girl to be my nephew’s sister? Is _her_ husband, who is the son +of his late father’s steward, to be his brother? Heaven and earth!--of +what are you thinking? Are the shades of Pemberley to be thus polluted?” + +β€œYou can _now_ have nothing further to say,” she resentfully answered. +β€œYou have insulted me, in every possible method. I must beg to return to +the house.” + +And she rose as she spoke. Lady Catherine rose also, and they turned +back. Her Ladyship was highly incensed. + +β€œYou have no regard, then, for the honour and credit of my nephew! +Unfeeling, selfish girl! Do you not consider that a connection with you +must disgrace him in the eyes of everybody?” + +β€œLady Catherine, I have nothing further to say. You know my sentiments.” + +β€œYou are then resolved to have him?” + +β€œI have said no such thing. I am only resolved to act in that manner, +which will, in my own opinion, constitute my happiness, without +reference to _you_, or to any person so wholly unconnected with me.” + +β€œIt is well. You refuse, then, to oblige me. You refuse to obey the +claims of duty, honour, and gratitude. You are determined to ruin him in +the opinion of all his friends, and make him the contempt of the world.” + +β€œNeither duty, nor honour, nor gratitude,” replied Elizabeth, β€œhas any +possible claim on me, in the present instance. No principle of either +would be violated by my marriage with Mr. Darcy. And with regard to the +resentment of his family, or the indignation of the world, if the former +_were_ excited by his marrying me, it would not give me one moment’s +concern--and the world in general would have too much sense to join in +the scorn.” + +β€œAnd this is your real opinion! This is your final resolve! Very well. I +shall now know how to act. Do not imagine, Miss Bennet, that your +ambition will ever be gratified. I came to try you. I hoped to find you +reasonable; but depend upon it I will carry my point.” + +In this manner Lady Catherine talked on till they were at the door of +the carriage, when, turning hastily round, she added,-- + +β€œI take no leave of you, Miss Bennet. I send no compliments to your +mother. You deserve no such attention. I am most seriously displeased.” + +Elizabeth made no answer; and without attempting to persuade her +Ladyship to return into the house, walked quietly into it herself. She +heard the carriage drive away as she proceeded upstairs. Her mother +impatiently met her at the door of her dressing-room, to ask why Lady +Catherine would not come in again and rest herself. + +β€œShe did not choose it,” said her daughter; β€œshe would go.” + +β€œShe is a very fine-looking woman! and her calling here was prodigiously +civil! for she only came, I suppose, to tell us the Collinses were well. +She is on her road somewhere, I dare say; and so, passing through +Meryton, thought she might as well call on you. I suppose she had +nothing particular to say to you, Lizzy?” + +Elizabeth was forced to give in to a little falsehood here; for to +acknowledge the substance of their conversation was impossible. + + + + +[Illustration: + + β€œBut now it comes out” +] + + + + +CHAPTER LVII. + + +[Illustration] + +The discomposure of spirits which this extraordinary visit threw +Elizabeth into could not be easily overcome; nor could she for many +hours learn to think of it less than incessantly. Lady Catherine, it +appeared, had actually taken the trouble of this journey from Rosings +for the sole purpose of breaking off her supposed engagement with Mr. +Darcy. It was a rational scheme, to be sure! but from what the report of +their engagement could originate, Elizabeth was at a loss to imagine; +till she recollected that _his_ being the intimate friend of Bingley, +and _her_ being the sister of Jane, was enough, at a time when the +expectation of one wedding made everybody eager for another, to supply +the idea. She had not herself forgotten to feel that the marriage of her +sister must bring them more frequently together. And her neighbours at +Lucas Lodge, therefore, (for through their communication with the +Collinses, the report, she concluded, had reached Lady Catherine,) had +only set _that_ down as almost certain and immediate which _she_ had +looked forward to as possible at some future time. + +In revolving Lady Catherine’s expressions, however, she could not help +feeling some uneasiness as to the possible consequence of her persisting +in this interference. From what she had said of her resolution to +prevent the marriage, it occurred to Elizabeth that she must meditate an +application to her nephew; and how he might take a similar +representation of the evils attached to a connection with her she dared +not pronounce. She knew not the exact degree of his affection for his +aunt, or his dependence on her judgment, but it was natural to suppose +that he thought much higher of her Ladyship than _she_ could do; and it +was certain, that in enumerating the miseries of a marriage with _one_ +whose immediate connections were so unequal to his own, his aunt would +address him on his weakest side. With his notions of dignity, he would +probably feel that the arguments, which to Elizabeth had appeared weak +and ridiculous, contained much good sense and solid reasoning. + +If he had been wavering before, as to what he should do, which had often +seemed likely, the advice and entreaty of so near a relation might +settle every doubt, and determine him at once to be as happy as dignity +unblemished could make him. In that case he would return no more. Lady +Catherine might see him in her way through town; and his engagement to +Bingley of coming again to Netherfield must give way. + +β€œIf, therefore, an excuse for not keeping his promise should come to his +friend within a few days,” she added, β€œI shall know how to understand +it. I shall then give over every expectation, every wish of his +constancy. If he is satisfied with only regretting me, when he might +have obtained my affections and hand, I shall soon cease to regret him +at all.” + +The surprise of the rest of the family, on hearing who their visitor had +been, was very great: but they obligingly satisfied it with the same +kind of supposition which had appeased Mrs. Bennet’s curiosity; and +Elizabeth was spared from much teasing on the subject. + +The next morning, as she was going down stairs, she was met by her +father, who came out of his library with a letter in his hand. + +β€œLizzy,” said he, β€œI was going to look for you: come into my room.” + +She followed him thither; and her curiosity to know what he had to tell +her was heightened by the supposition of its being in some manner +connected with the letter he held. It suddenly struck her that it might +be from Lady Catherine, and she anticipated with dismay all the +consequent explanations. + +She followed her father to the fireplace, and they both sat down. He +then said,-- + +β€œI have received a letter this morning that has astonished me +exceedingly. As it principally concerns yourself, you ought to know its +contents. I did not know before that I had _two_ daughters on the brink +of matrimony. Let me congratulate you on a very important conquest.” + +The colour now rushed into Elizabeth’s cheeks in the instantaneous +conviction of its being a letter from the nephew, instead of the aunt; +and she was undetermined whether most to be pleased that he explained +himself at all, or offended that his letter was not rather addressed to +herself, when her father continued,-- + +β€œYou look conscious. Young ladies have great penetration in such matters +as these; but I think I may defy even _your_ sagacity to discover the +name of your admirer. This letter is from Mr. Collins.” + +β€œFrom Mr. Collins! and what can _he_ have to say?” + +β€œSomething very much to the purpose, of course. He begins with +congratulations on the approaching nuptials of my eldest daughter, of +which, it seems, he has been told by some of the good-natured, gossiping +Lucases. I shall not sport with your impatience by reading what he says +on that point. What relates to yourself is as follows:--β€˜Having thus +offered you the sincere congratulations of Mrs. Collins and myself on +this happy event, let me now add a short hint on the subject of another, +of which we have been advertised by the same authority. Your daughter +Elizabeth, it is presumed, will not long bear the name of Bennet, after +her eldest sister has resigned it; and the chosen partner of her fate +may be reasonably looked up to as one of the most illustrious personages +in this land.’ Can you possibly guess, Lizzy, who is meant by this? +β€˜This young gentleman is blessed, in a peculiar way, with everything the +heart of mortal can most desire,--splendid property, noble kindred, and +extensive patronage. Yet, in spite of all these temptations, let me warn +my cousin Elizabeth, and yourself, of what evils you may incur by a +precipitate closure with this gentleman’s proposals, which, of course, +you will be inclined to take immediate advantage of.’ Have you any idea, +Lizzy, who this gentleman is? But now it comes out. β€˜My motive for +cautioning you is as follows:--We have reason to imagine that his aunt, +Lady Catherine de Bourgh, does not look on the match with a friendly +eye.’ _Mr. Darcy_, you see, is the man! Now, Lizzy, I think I _have_ +surprised you. Could he, or the Lucases, have pitched on any man, within +the circle of our acquaintance, whose name would have given the lie more +effectually to what they related? Mr. Darcy, who never looks at any +woman but to see a blemish, and who probably never looked at _you_ in +his life! It is admirable!” + +Elizabeth tried to join in her father’s pleasantry, but could only force +one most reluctant smile. Never had his wit been directed in a manner so +little agreeable to her. + +β€œAre you not diverted?” + +β€œOh, yes. Pray read on.” + +β€œβ€˜After mentioning the likelihood of this marriage to her Ladyship last +night, she immediately, with her usual condescension, expressed what she +felt on the occasion; when it became apparent, that, on the score of +some family objections on the part of my cousin, she would never give +her consent to what she termed so disgraceful a match. I thought it my +duty to give the speediest intelligence of this to my cousin, that she +and her noble admirer may be aware of what they are about, and not run +hastily into a marriage which has not been properly sanctioned.’ Mr. +Collins, moreover, adds, β€˜I am truly rejoiced that my cousin Lydia’s sad +business has been so well hushed up, and am only concerned that their +living together before the marriage took place should be so generally +known. I must not, however, neglect the duties of my station, or refrain +from declaring my amazement, at hearing that you received the young +couple into your house as soon as they were married. It was an +encouragement of vice; and had I been the rector of Longbourn, I should +very strenuously have opposed it. You ought certainly to forgive them as +a Christian, but never to admit them in your sight, or allow their +names to be mentioned in your hearing.’ _That_ is his notion of +Christian forgiveness! The rest of his letter is only about his dear +Charlotte’s situation, and his expectation of a young olive-branch. But, +Lizzy, you look as if you did not enjoy it. You are not going to be +_missish_, I hope, and pretend to be affronted at an idle report. For +what do we live, but to make sport for our neighbours, and laugh at them +in our turn?” + +β€œOh,” cried Elizabeth, β€œI am exceedingly diverted. But it is so +strange!” + +β€œYes, _that_ is what makes it amusing. Had they fixed on any other man +it would have been nothing; but _his_ perfect indifference and _your_ +pointed dislike make it so delightfully absurd! Much as I abominate +writing, I would not give up Mr. Collins’s correspondence for any +consideration. Nay, when I read a letter of his, I cannot help giving +him the preference even over Wickham, much as I value the impudence and +hypocrisy of my son-in-law. And pray, Lizzy, what said Lady Catherine +about this report? Did she call to refuse her consent?” + +To this question his daughter replied only with a laugh; and as it had +been asked without the least suspicion, she was not distressed by his +repeating it. Elizabeth had never been more at a loss to make her +feelings appear what they were not. It was necessary to laugh when she +would rather have cried. Her father had most cruelly mortified her by +what he said of Mr. Darcy’s indifference; and she could do nothing but +wonder at such a want of penetration, or fear that, perhaps, instead of +his seeing too _little_, she might have fancied too _much_. + + + + +[Illustration: + +β€œThe efforts of his aunt” + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER LVIII. + + +[Illustration] + +Instead of receiving any such letter of excuse from his friend, as +Elizabeth half expected Mr. Bingley to do, he was able to bring Darcy +with him to Longbourn before many days had passed after Lady Catherine’s +visit. The gentlemen arrived early; and, before Mrs. Bennet had time to +tell him of their having seen his aunt, of which her daughter sat in +momentary dread, Bingley, who wanted to be alone with Jane, proposed +their all walking out. It was agreed to. Mrs. Bennet was not in the +habit of walking, Mary could never spare time, but the remaining five +set off together. Bingley and Jane, however, soon allowed the others to +outstrip them. They lagged behind, while Elizabeth, Kitty, and Darcy +were to entertain each other. Very little was said by either; Kitty was +too much afraid of him to talk; Elizabeth was secretly forming a +desperate resolution; and, perhaps, he might be doing the same. + +They walked towards the Lucases’, because Kitty wished to call upon +Maria; and as Elizabeth saw no occasion for making it a general concern, +when Kitty left them she went boldly on with him alone. Now was the +moment for her resolution to be executed; and while her courage was +high, she immediately said,-- + +β€œMr. Darcy, I am a very selfish creature, and for the sake of giving +relief to my own feelings care not how much I may be wounding yours. I +can no longer help thanking you for your unexampled kindness to my poor +sister. Ever since I have known it I have been most anxious to +acknowledge to you how gratefully I feel it. Were it known to the rest +of my family I should not have merely my own gratitude to express.” + +β€œI am sorry, exceedingly sorry,” replied Darcy, in a tone of surprise +and emotion, β€œthat you have ever been informed of what may, in a +mistaken light, have given you uneasiness. I did not think Mrs. Gardiner +was so little to be trusted.” + +β€œYou must not blame my aunt. Lydia’s thoughtlessness first betrayed to +me that you had been concerned in the matter; and, of course, I could +not rest till I knew the particulars. Let me thank you again and again, +in the name of all my family, for that generous compassion which induced +you to take so much trouble, and bear so many mortifications, for the +sake of discovering them.” + +β€œIf you _will_ thank me,” he replied, β€œlet it be for yourself alone. +That the wish of giving happiness to you might add force to the other +inducements which led me on, I shall not attempt to deny. But your +_family_ owe me nothing. Much as I respect them, I believe I thought +only of _you_.” + +Elizabeth was too much embarrassed to say a word. After a short pause, +her companion added, β€œYou are too generous to trifle with me. If your +feelings are still what they were last April, tell me so at once. _My_ +affections and wishes are unchanged; but one word from you will silence +me on this subject for ever.” + +Elizabeth, feeling all the more than common awkwardness and anxiety of +his situation, now forced herself to speak; and immediately, though not +very fluently, gave him to understand that her sentiments had undergone +so material a change since the period to which he alluded, as to make +her receive with gratitude and pleasure his present assurances. The +happiness which this reply produced was such as he had probably never +felt before; and he expressed himself on the occasion as sensibly and as +warmly as a man violently in love can be supposed to do. Had Elizabeth +been able to encounter his eyes, she might have seen how well the +expression of heartfelt delight diffused over his face became him: but +though she could not look she could listen; and he told her of feelings +which, in proving of what importance she was to him, made his affection +every moment more valuable. + +They walked on without knowing in what direction. There was too much to +be thought, and felt, and said, for attention to any other objects. She +soon learnt that they were indebted for their present good understanding +to the efforts of his aunt, who _did_ call on him in her return through +London, and there relate her journey to Longbourn, its motive, and the +substance of her conversation with Elizabeth; dwelling emphatically on +every expression of the latter, which, in her Ladyship’s apprehension, +peculiarly denoted her perverseness and assurance, in the belief that +such a relation must assist her endeavours to obtain that promise from +her nephew which _she_ had refused to give. But, unluckily for her +Ladyship, its effect had been exactly contrariwise. + +β€œIt taught me to hope,” said he, β€œas I had scarcely ever allowed myself +to hope before. I knew enough of your disposition to be certain, that +had you been absolutely, irrevocably decided against me, you would have +acknowledged it to Lady Catherine frankly and openly.” + +Elizabeth coloured and laughed as she replied, β€œYes, you know enough of +my _frankness_ to believe me capable of _that_. After abusing you so +abominably to your face, I could have no scruple in abusing you to all +your relations.” + +β€œWhat did you say of me that I did not deserve? For though your +accusations were ill-founded, formed on mistaken premises, my behaviour +to you at the time had merited the severest reproof. It was +unpardonable. I cannot think of it without abhorrence.” + +β€œWe will not quarrel for the greater share of blame annexed to that +evening,” said Elizabeth. β€œThe conduct of neither, if strictly +examined, will be irreproachable; but since then we have both, I hope, +improved in civility.” + +β€œI cannot be so easily reconciled to myself. The recollection of what I +then said, of my conduct, my manners, my expressions during the whole of +it, is now, and has been many months, inexpressibly painful to me. Your +reproof, so well applied, I shall never forget: β€˜Had you behaved in a +more gentlemanlike manner.’ Those were your words. You know not, you can +scarcely conceive, how they have tortured me; though it was some time, I +confess, before I was reasonable enough to allow their justice.” + +β€œI was certainly very far from expecting them to make so strong an +impression. I had not the smallest idea of their being ever felt in such +a way.” + +β€œI can easily believe it. You thought me then devoid of every proper +feeling, I am sure you did. The turn of your countenance I shall never +forget, as you said that I could not have addressed you in any possible +way that would induce you to accept me.” + +β€œOh, do not repeat what I then said. These recollections will not do at +all. I assure you that I have long been most heartily ashamed of it.” + +Darcy mentioned his letter. β€œDid it,” said he,--β€œdid it _soon_ make you +think better of me? Did you, on reading it, give any credit to its +contents?” + +She explained what its effects on her had been, and how gradually all +her former prejudices had been removed. + +β€œI knew,” said he, β€œthat what I wrote must give you pain, but it was +necessary. I hope you have destroyed the letter. There was one part, +especially the opening of it, which I should dread your having the power +of reading again. I can remember some expressions which might justly +make you hate me.” + +β€œThe letter shall certainly be burnt, if you believe it essential to the +preservation of my regard; but, though we have both reason to think my +opinions not entirely unalterable, they are not, I hope, quite so easily +changed as that implies.” + +β€œWhen I wrote that letter,” replied Darcy, β€œI believed myself perfectly +calm and cool; but I am since convinced that it was written in a +dreadful bitterness of spirit.” + +β€œThe letter, perhaps, began in bitterness, but it did not end so. The +adieu is charity itself. But think no more of the letter. The feelings +of the person who wrote and the person who received it are now so widely +different from what they were then, that every unpleasant circumstance +attending it ought to be forgotten. You must learn some of my +philosophy. Think only of the past as its remembrance gives you +pleasure.” + +β€œI cannot give you credit for any philosophy of the kind. _Your_ +retrospections must be so totally void of reproach, that the contentment +arising from them is not of philosophy, but, what is much better, of +ignorance. But with _me_, it is not so. Painful recollections will +intrude, which cannot, which ought not to be repelled. I have been a +selfish being all my life, in practice, though not in principle. As a +child I was taught what was _right_, but I was not taught to correct my +temper. I was given good principles, but left to follow them in pride +and conceit. Unfortunately an only son (for many years an only _child_), +I was spoiled by my parents, who, though good themselves, (my father +particularly, all that was benevolent and amiable,) allowed, encouraged, +almost taught me to be selfish and overbearing, to care for none beyond +my own family circle, to think meanly of all the rest of the world, to +_wish_ at least to think meanly of their sense and worth compared with +my own. Such I was, from eight to eight-and-twenty; and such I might +still have been but for you, dearest, loveliest Elizabeth! What do I not +owe you! You taught me a lesson, hard indeed at first, but most +advantageous. By you, I was properly humbled. I came to you without a +doubt of my reception. You showed me how insufficient were all my +pretensions to please a woman worthy of being pleased.” + +β€œHad you then persuaded yourself that I should?” + +β€œIndeed I had. What will you think of my vanity? I believed you to be +wishing, expecting my addresses.” + +β€œMy manners must have been in fault, but not intentionally, I assure +you. I never meant to deceive you, but my spirits might often lead me +wrong. How you must have hated me after _that_ evening!” + +β€œHate you! I was angry, perhaps, at first, but my anger soon began to +take a proper direction.” + +β€œI am almost afraid of asking what you thought of me when we met at +Pemberley. You blamed me for coming?” + +β€œNo, indeed, I felt nothing but surprise.” + +β€œYour surprise could not be greater than _mine_ in being noticed by you. +My conscience told me that I deserved no extraordinary politeness, and I +confess that I did not expect to receive _more_ than my due.” + +β€œMy object _then_,” replied Darcy, β€œwas to show you, by every civility +in my power, that I was not so mean as to resent the past; and I hoped +to obtain your forgiveness, to lessen your ill opinion, by letting you +see that your reproofs had been attended to. How soon any other wishes +introduced themselves, I can hardly tell, but I believe in about half +an hour after I had seen you.” + +He then told her of Georgiana’s delight in her acquaintance, and of her +disappointment at its sudden interruption; which naturally leading to +the cause of that interruption, she soon learnt that his resolution of +following her from Derbyshire in quest of her sister had been formed +before he quitted the inn, and that his gravity and thoughtfulness there +had arisen from no other struggles than what such a purpose must +comprehend. + +She expressed her gratitude again, but it was too painful a subject to +each to be dwelt on farther. + +After walking several miles in a leisurely manner, and too busy to know +anything about it, they found at last, on examining their watches, that +it was time to be at home. + +β€œWhat could have become of Mr. Bingley and Jane?” was a wonder which +introduced the discussion of _their_ affairs. Darcy was delighted with +their engagement; his friend had given him the earliest information of +it. + +β€œI must ask whether you were surprised?” said Elizabeth. + +β€œNot at all. When I went away, I felt that it would soon happen.” + +β€œThat is to say, you had given your permission. I guessed as much.” And +though he exclaimed at the term, she found that it had been pretty much +the case. + +β€œOn the evening before my going to London,” said he, β€œI made a +confession to him, which I believe I ought to have made long ago. I told +him of all that had occurred to make my former interference in his +affairs absurd and impertinent. His surprise was great. He had never had +the slightest suspicion. I told him, moreover, that I believed myself +mistaken in supposing, as I had done, that your sister was indifferent +to him; and as I could easily perceive that his attachment to her was +unabated, I felt no doubt of their happiness together.” + +Elizabeth could not help smiling at his easy manner of directing his +friend. + +β€œDid you speak from your own observation,” said she, β€œwhen you told him +that my sister loved him, or merely from my information last spring?” + +β€œFrom the former. I had narrowly observed her, during the two visits +which I had lately made her here; and I was convinced of her affection.” + +β€œAnd your assurance of it, I suppose, carried immediate conviction to +him.” + +β€œIt did. Bingley is most unaffectedly modest. His diffidence had +prevented his depending on his own judgment in so anxious a case, but +his reliance on mine made everything easy. I was obliged to confess one +thing, which for a time, and not unjustly, offended him. I could not +allow myself to conceal that your sister had been in town three months +last winter, that I had known it, and purposely kept it from him. He was +angry. But his anger, I am persuaded, lasted no longer than he remained +in any doubt of your sister’s sentiments. He has heartily forgiven me +now.” + +Elizabeth longed to observe that Mr. Bingley had been a most delightful +friend; so easily guided that his worth was invaluable; but she checked +herself. She remembered that he had yet to learn to be laughed at, and +it was rather too early to begin. In anticipating the happiness of +Bingley, which of course was to be inferior only to his own, he +continued the conversation till they reached the house. In the hall they +parted. + + + + +[Illustration: + + β€œUnable to utter a syllable” + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER LIX. + + +[Illustration] + +β€œMy dear Lizzy, where can you have been walking to?” was a question +which Elizabeth received from Jane as soon as she entered the room, and +from all the others when they sat down to table. She had only to say in +reply, that they had wandered about till she was beyond her own +knowledge. She coloured as she spoke; but neither that, nor anything +else, awakened a suspicion of the truth. + +The evening passed quietly, unmarked by anything extraordinary. The +acknowledged lovers talked and laughed; the unacknowledged were silent. +Darcy was not of a disposition in which happiness overflows in mirth; +and Elizabeth, agitated and confused, rather _knew_ that she was happy +than _felt_ herself to be so; for, besides the immediate embarrassment, +there were other evils before her. She anticipated what would be felt in +the family when her situation became known: she was aware that no one +liked him but Jane; and even feared that with the others it was a +_dislike_ which not all his fortune and consequence might do away. + +At night she opened her heart to Jane. Though suspicion was very far +from Miss Bennet’s general habits, she was absolutely incredulous here. + +β€œYou are joking, Lizzy. This cannot be! Engaged to Mr. Darcy! No, no, +you shall not deceive me: I know it to be impossible.” + +β€œThis is a wretched beginning, indeed! My sole dependence was on you; +and I am sure nobody else will believe me, if you do not. Yet, indeed, I +am in earnest. I speak nothing but the truth. He still loves me, and we +are engaged.” + +Jane looked at her doubtingly. β€œOh, Lizzy! it cannot be. I know how much +you dislike him.” + +β€œYou know nothing of the matter. _That_ is all to be forgot. Perhaps I +did not always love him so well as I do now; but in such cases as these +a good memory is unpardonable. This is the last time I shall ever +remember it myself.” + +Miss Bennet still looked all amazement. Elizabeth again, and more +seriously, assured her of its truth. + +β€œGood heaven! can it be really so? Yet now I must believe you,” cried +Jane. β€œMy dear, dear Lizzy, I would, I do congratulate you; but are you +certain--forgive the question--are you quite certain that you can be +happy with him?” + +β€œThere can be no doubt of that. It is settled between us already that we +are to be the happiest couple in the world. But are you pleased, Jane? +Shall you like to have such a brother?” + +β€œVery, very much. Nothing could give either Bingley or myself more +delight. But we considered it, we talked of it as impossible. And do you +really love him quite well enough? Oh, Lizzy! do anything rather than +marry without affection. Are you quite sure that you feel what you ought +to do?” + +β€œOh, yes! You will only think I feel _more_ than I ought to do when I +tell you all.” + +β€œWhat do you mean?” + +β€œWhy, I must confess that I love him better than I do Bingley. I am +afraid you will be angry.” + +β€œMy dearest sister, now be, _be_ serious. I want to talk very seriously. +Let me know everything that I am to know without delay. Will you tell me +how long you have loved him?” + +β€œIt has been coming on so gradually, that I hardly know when it began; +but I believe I must date it from my first seeing his beautiful grounds +at Pemberley.” + +Another entreaty that she would be serious, however, produced the +desired effect; and she soon satisfied Jane by her solemn assurances of +attachment. When convinced on that article, Miss Bennet had nothing +further to wish. + +β€œNow I am quite happy,” said she, β€œfor you will be as happy as myself. I +always had a value for him. Were it for nothing but his love of you, I +must always have esteemed him; but now, as Bingley’s friend and your +husband, there can be only Bingley and yourself more dear to me. But, +Lizzy, you have been very sly, very reserved with me. How little did you +tell me of what passed at Pemberley and Lambton! I owe all that I know +of it to another, not to you.” + +Elizabeth told her the motives of her secrecy. She had been unwilling to +mention Bingley; and the unsettled state of her own feelings had made +her equally avoid the name of his friend: but now she would no longer +conceal from her his share in Lydia’s marriage. All was acknowledged, +and half the night spent in conversation. + +β€œGood gracious!” cried Mrs. Bennet, as she stood at a window the next +morning, β€œif that disagreeable Mr. Darcy is not coming here again with +our dear Bingley! What can he mean by being so tiresome as to be always +coming here? I had no notion but he would go a-shooting, or something or +other, and not disturb us with his company. What shall we do with him? +Lizzy, you must walk out with him again, that he may not be in Bingley’s +way.” + +Elizabeth could hardly help laughing at so convenient a proposal; yet +was really vexed that her mother should be always giving him such an +epithet. + +As soon as they entered, Bingley looked at her so expressively, and +shook hands with such warmth, as left no doubt of his good information; +and he soon afterwards said aloud, β€œMrs. Bennet, have you no more lanes +hereabouts in which Lizzy may lose her way again to-day?” + +β€œI advise Mr. Darcy, and Lizzy, and Kitty,” said Mrs. Bennet, β€œto walk +to Oakham Mount this morning. It is a nice long walk, and Mr. Darcy has +never seen the view.” + +β€œIt may do very well for the others,” replied Mr. Bingley; β€œbut I am +sure it will be too much for Kitty. Won’t it, Kitty?” + +Kitty owned that she had rather stay at home. Darcy professed a great +curiosity to see the view from the Mount, and Elizabeth silently +consented. As she went upstairs to get ready, Mrs. Bennet followed her, +saying,-- + +β€œI am quite sorry, Lizzy, that you should be forced to have that +disagreeable man all to yourself; but I hope you will not mind it. It is +all for Jane’s sake, you know; and there is no occasion for talking to +him except just now and then; so do not put yourself to inconvenience.” + +During their walk, it was resolved that Mr. Bennet’s consent should be +asked in the course of the evening: Elizabeth reserved to herself the +application for her mother’s. She could not determine how her mother +would take it; sometimes doubting whether all his wealth and grandeur +would be enough to overcome her abhorrence of the man; but whether she +were violently set against the match, or violently delighted with it, it +was certain that her manner would be equally ill adapted to do credit to +her sense; and she could no more bear that Mr. Darcy should hear the +first raptures of her joy, than the first vehemence of her +disapprobation. + +In the evening, soon after Mr. Bennet withdrew to the library, she saw +Mr. Darcy rise also and follow him, and her agitation on seeing it was +extreme. She did not fear her father’s opposition, but he was going to +be made unhappy, and that it should be through her means; that _she_, +his favourite child, should be distressing him by her choice, should be +filling him with fears and regrets in disposing of her, was a wretched +reflection, and she sat in misery till Mr. Darcy appeared again, when, +looking at him, she was a little relieved by his smile. In a few minutes +he approached the table where she was sitting with Kitty; and, while +pretending to admire her work, said in a whisper, β€œGo to your father; he +wants you in the library.” She was gone directly. + +Her father was walking about the room, looking grave and anxious. +β€œLizzy,” said he, β€œwhat are you doing? Are you out of your senses to be +accepting this man? Have not you always hated him?” + +How earnestly did she then wish that her former opinions had been more +reasonable, her expressions more moderate! It would have spared her from +explanations and professions which it was exceedingly awkward to give; +but they were now necessary, and she assured him, with some confusion, +of her attachment to Mr. Darcy. + +β€œOr, in other words, you are determined to have him. He is rich, to be +sure, and you may have more fine clothes and fine carriages than Jane. +But will they make you happy?” + +β€œHave you any other objection,” said Elizabeth, β€œthan your belief of my +indifference?” + +β€œNone at all. We all know him to be a proud, unpleasant sort of man; but +this would be nothing if you really liked him.” + +β€œI do, I do like him,” she replied, with tears in her eyes; β€œI love him. +Indeed he has no improper pride. He is perfectly amiable. You do not +know what he really is; then pray do not pain me by speaking of him in +such terms.” + +β€œLizzy,” said her father, β€œI have given him my consent. He is the kind +of man, indeed, to whom I should never dare refuse anything, which he +condescended to ask. I now give it to _you_, if you are resolved on +having him. But let me advise you to think better of it. I know your +disposition, Lizzy. I know that you could be neither happy nor +respectable, unless you truly esteemed your husband, unless you looked +up to him as a superior. Your lively talents would place you in the +greatest danger in an unequal marriage. You could scarcely escape +discredit and misery. My child, let me not have the grief of seeing +_you_ unable to respect your partner in life. You know not what you are +about.” + +Elizabeth, still more affected, was earnest and solemn in her reply; +and, at length, by repeated assurances that Mr. Darcy was really the +object of her choice, by explaining the gradual change which her +estimation of him had undergone, relating her absolute certainty that +his affection was not the work of a day, but had stood the test of many +months’ suspense, and enumerating with energy all his good qualities, +she did conquer her father’s incredulity, and reconcile him to the +match. + +β€œWell, my dear,” said he, when she ceased speaking, β€œI have no more to +say. If this be the case, he deserves you. I could not have parted with +you, my Lizzy, to anyone less worthy.” + +To complete the favourable impression, she then told him what Mr. Darcy +had voluntarily done for Lydia. He heard her with astonishment. + +β€œThis is an evening of wonders, indeed! And so, Darcy did everything; +made up the match, gave the money, paid the fellow’s debts, and got him +his commission! So much the better. It will save me a world of trouble +and economy. Had it been your uncle’s doing, I must and _would_ have +paid him; but these violent young lovers carry everything their own +way. I shall offer to pay him to-morrow, he will rant and storm about +his love for you, and there will be an end of the matter.” + +He then recollected her embarrassment a few days before on his reading +Mr. Collins’s letter; and after laughing at her some time, allowed her +at last to go, saying, as she quitted the room, β€œIf any young men come +for Mary or Kitty, send them in, for I am quite at leisure.” + +Elizabeth’s mind was now relieved from a very heavy weight; and, after +half an hour’s quiet reflection in her own room, she was able to join +the others with tolerable composure. Everything was too recent for +gaiety, but the evening passed tranquilly away; there was no longer +anything material to be dreaded, and the comfort of ease and familiarity +would come in time. + +When her mother went up to her dressing-room at night, she followed her, +and made the important communication. Its effect was most extraordinary; +for, on first hearing it, Mrs. Bennet sat quite still, and unable to +utter a syllable. Nor was it under many, many minutes, that she could +comprehend what she heard, though not in general backward to credit what +was for the advantage of her family, or that came in the shape of a +lover to any of them. She began at length to recover, to fidget about in +her chair, get up, sit down again, wonder, and bless herself. + +β€œGood gracious! Lord bless me! only think! dear me! Mr. Darcy! Who would +have thought it? And is it really true? Oh, my sweetest Lizzy! how rich +and how great you will be! What pin-money, what jewels, what carriages +you will have! Jane’s is nothing to it--nothing at all. I am so +pleased--so happy. Such a charming man! so handsome! so tall! Oh, my +dear Lizzy! pray apologize for my having disliked him so much before. I +hope he will overlook it. Dear, dear Lizzy. A house in town! Everything +that is charming! Three daughters married! Ten thousand a year! Oh, +Lord! what will become of me? I shall go distracted.” + +This was enough to prove that her approbation need not be doubted; and +Elizabeth, rejoicing that such an effusion was heard only by herself, +soon went away. But before she had been three minutes in her own room, +her mother followed her. + +β€œMy dearest child,” she cried, β€œI can think of nothing else. Ten +thousand a year, and very likely more! ’Tis as good as a lord! And a +special licence--you must and shall be married by a special licence. +But, my dearest love, tell me what dish Mr. Darcy is particularly fond +of, that I may have it to-morrow.” + +This was a sad omen of what her mother’s behaviour to the gentleman +himself might be; and Elizabeth found that, though in the certain +possession of his warmest affection, and secure of her relations’ +consent, there was still something to be wished for. But the morrow +passed off much better than she expected; for Mrs. Bennet luckily stood +in such awe of her intended son-in-law, that she ventured not to speak +to him, unless it was in her power to offer him any attention, or mark +her deference for his opinion. + +Elizabeth had the satisfaction of seeing her father taking pains to get +acquainted with him; and Mr. Bennet soon assured her that he was rising +every hour in his esteem. + +β€œI admire all my three sons-in-law highly,” said he. β€œWickham, perhaps, +is my favourite; but I think I shall like _your_ husband quite as well +as Jane’s.” + + + + +[Illustration: + +β€œThe obsequious civility.” + +[_Copyright 1894 by George Allen._]] + + + + +CHAPTER LX. + + +[Illustration] + +Elizabeth’s spirits soon rising to playfulness again, she wanted Mr. +Darcy to account for his having ever fallen in love with her. β€œHow could +you begin?” said she. β€œI can comprehend your going on charmingly, when +you had once made a beginning; but what could set you off in the first +place?” + +β€œI cannot fix on the hour, or the spot, or the look, or the words, which +laid the foundation. It is too long ago. I was in the middle before I +knew that I _had_ begun.” + +β€œMy beauty you had early withstood, and as for my manners--my behaviour +to _you_ was at least always bordering on the uncivil, and I never spoke +to you without rather wishing to give you pain than not. Now, be +sincere; did you admire me for my impertinence?” + +β€œFor the liveliness of your mind I did.” + +β€œYou may as well call it impertinence at once. It was very little less. +The fact is, that you were sick of civility, of deference, of officious +attention. You were disgusted with the women who were always speaking, +and looking, and thinking for _your_ approbation alone. I roused and +interested you, because I was so unlike _them_. Had you not been really +amiable you would have hated me for it: but in spite of the pains you +took to disguise yourself, your feelings were always noble and just; and +in your heart you thoroughly despised the persons who so assiduously +courted you. There--I have saved you the trouble of accounting for it; +and really, all things considered, I begin to think it perfectly +reasonable. To be sure you know no actual good of me--but nobody thinks +of _that_ when they fall in love.” + +β€œWas there no good in your affectionate behaviour to Jane, while she was +ill at Netherfield?” + +β€œDearest Jane! who could have done less for her? But make a virtue of it +by all means. My good qualities are under your protection, and you are +to exaggerate them as much as possible; and, in return, it belongs to me +to find occasions for teasing and quarrelling with you as often as may +be; and I shall begin directly, by asking you what made you so unwilling +to come to the point at last? What made you so shy of me, when you +first called, and afterwards dined here? Why, especially, when you +called, did you look as if you did not care about me?” + +β€œBecause you were grave and silent, and gave me no encouragement.” + +β€œBut I was embarrassed.” + +β€œAnd so was I.” + +β€œYou might have talked to me more when you came to dinner.” + +β€œA man who had felt less might.” + +β€œHow unlucky that you should have a reasonable answer to give, and that +I should be so reasonable as to admit it! But I wonder how long you +_would_ have gone on, if you had been left to yourself. I wonder when +you _would_ have spoken if I had not asked you! My resolution of +thanking you for your kindness to Lydia had certainly great effect. _Too +much_, I am afraid; for what becomes of the moral, if our comfort +springs from a breach of promise, for I ought not to have mentioned the +subject? This will never do.” + +β€œYou need not distress yourself. The moral will be perfectly fair. Lady +Catherine’s unjustifiable endeavours to separate us were the means of +removing all my doubts. I am not indebted for my present happiness to +your eager desire of expressing your gratitude. I was not in a humour to +wait for an opening of yours. My aunt’s intelligence had given me hope, +and I was determined at once to know everything.” + +β€œLady Catherine has been of infinite use, which ought to make her happy, +for she loves to be of use. But tell me, what did you come down to +Netherfield for? Was it merely to ride to Longbourn and be embarrassed? +or had you intended any more serious consequences?” + +β€œMy real purpose was to see _you_, and to judge, if I could, whether I +might ever hope to make you love me. My avowed one, or what I avowed to +myself, was to see whether your sister was still partial to Bingley, and +if she were, to make the confession to him which I have since made.” + +β€œShall you ever have courage to announce to Lady Catherine what is to +befall her?” + +β€œI am more likely to want time than courage, Elizabeth. But it ought to +be done; and if you will give me a sheet of paper it shall be done +directly.” + +β€œAnd if I had not a letter to write myself, I might sit by you, and +admire the evenness of your writing, as another young lady once did. But +I have an aunt, too, who must not be longer neglected.” + +From an unwillingness to confess how much her intimacy with Mr. Darcy +had been overrated, Elizabeth had never yet answered Mrs. Gardiner’s +long letter; but now, having _that_ to communicate which she knew would +be most welcome, she was almost ashamed to find that her uncle and aunt +had already lost three days of happiness, and immediately wrote as +follows:-- + +β€œI would have thanked you before, my dear aunt, as I ought to have done, +for your long, kind, satisfactory detail of particulars; but, to say the +truth, I was too cross to write. You supposed more than really existed. +But _now_ suppose as much as you choose; give a loose to your fancy, +indulge your imagination in every possible flight which the subject will +afford, and unless you believe me actually married, you cannot greatly +err. You must write again very soon, and praise him a great deal more +than you did in your last. I thank you again and again, for not going to +the Lakes. How could I be so silly as to wish it! Your idea of the +ponies is delightful. We will go round the park every day. I am the +happiest creature in the world. Perhaps other people have said so +before, but no one with such justice. I am happier even than Jane; she +only smiles, I laugh. Mr. Darcy sends you all the love in the world that +can be spared from me. You are all to come to Pemberley at Christmas. +Yours,” etc. + +Mr. Darcy’s letter to Lady Catherine was in a different style, and still +different from either was what Mr. Bennet sent to Mr. Collins, in return +for his last. + + /* β€œDear Sir, */ + + β€œI must trouble you once more for congratulations. Elizabeth will + soon be the wife of Mr. Darcy. Console Lady Catherine as well as + you can. But, if I were you, I would stand by the nephew. He has + more to give. + +β€œYours sincerely,” etc. + +Miss Bingley’s congratulations to her brother on his approaching +marriage were all that was affectionate and insincere. She wrote even to +Jane on the occasion, to express her delight, and repeat all her former +professions of regard. Jane was not deceived, but she was affected; and +though feeling no reliance on her, could not help writing her a much +kinder answer than she knew was deserved. + +The joy which Miss Darcy expressed on receiving similar information was +as sincere as her brother’s in sending it. Four sides of paper were +insufficient to contain all her delight, and all her earnest desire of +being loved by her sister. + +Before any answer could arrive from Mr. Collins, or any congratulations +to Elizabeth from his wife, the Longbourn family heard that the +Collinses were come themselves to Lucas Lodge. The reason of this +sudden removal was soon evident. Lady Catherine had been rendered so +exceedingly angry by the contents of her nephew’s letter, that +Charlotte, really rejoicing in the match, was anxious to get away till +the storm was blown over. At such a moment, the arrival of her friend +was a sincere pleasure to Elizabeth, though in the course of their +meetings she must sometimes think the pleasure dearly bought, when she +saw Mr. Darcy exposed to all the parading and obsequious civility of her +husband. He bore it, however, with admirable calmness. He could even +listen to Sir William Lucas, when he complimented him on carrying away +the brightest jewel of the country, and expressed his hopes of their all +meeting frequently at St. James’s, with very decent composure. If he did +shrug his shoulders, it was not till Sir William was out of sight. + +Mrs. Philips’s vulgarity was another, and, perhaps, a greater tax on his +forbearance; and though Mrs. Philips, as well as her sister, stood in +too much awe of him to speak with the familiarity which Bingley’s +good-humour encouraged; yet, whenever she _did_ speak, she must be +vulgar. Nor was her respect for him, though it made her more quiet, at +all likely to make her more elegant. Elizabeth did all she could to +shield him from the frequent notice of either, and was ever anxious to +keep him to herself, and to those of her family with whom he might +converse without mortification; and though the uncomfortable feelings +arising from all this took from the season of courtship much of its +pleasure, it added to the hope of the future; and she looked forward +with delight to the time when they should be removed from society so +little pleasing to either, to all the comfort and elegance of their +family party at Pemberley. + + + + +[Illustration] + + + + +CHAPTER LXI. + + +[Illustration] + +Happy for all her maternal feelings was the day on which Mrs. Bennet got +rid of her two most deserving daughters. With what delighted pride she +afterwards visited Mrs. Bingley, and talked of Mrs. Darcy, may be +guessed. I wish I could say, for the sake of her family, that the +accomplishment of her earnest desire in the establishment of so many of +her children produced so happy an effect as to make her a sensible, +amiable, well-informed woman for the rest of her life; though, perhaps, +it was lucky for her husband, who might not have relished domestic +felicity in so unusual a form, that she still was occasionally nervous +and invariably silly. + +Mr. Bennet missed his second daughter exceedingly; his affection for her +drew him oftener from home than anything else could do. He delighted in +going to Pemberley, especially when he was least expected. + +Mr. Bingley and Jane remained at Netherfield only a twelvemonth. So near +a vicinity to her mother and Meryton relations was not desirable even to +_his_ easy temper, or _her_ affectionate heart. The darling wish of his +sisters was then gratified: he bought an estate in a neighbouring county +to Derbyshire; and Jane and Elizabeth, in addition to every other source +of happiness, were within thirty miles of each other. + +Kitty, to her very material advantage, spent the chief of her time with +her two elder sisters. In society so superior to what she had generally +known, her improvement was great. She was not of so ungovernable a +temper as Lydia; and, removed from the influence of Lydia’s example, she +became, by proper attention and management, less irritable, less +ignorant, and less insipid. From the further disadvantage of Lydia’s +society she was of course carefully kept; and though Mrs. Wickham +frequently invited her to come and stay with her, with the promise of +balls and young men, her father would never consent to her going. + +Mary was the only daughter who remained at home; and she was necessarily +drawn from the pursuit of accomplishments by Mrs. Bennet’s being quite +unable to sit alone. Mary was obliged to mix more with the world, but +she could still moralize over every morning visit; and as she was no +longer mortified by comparisons between her sisters’ beauty and her own, +it was suspected by her father that she submitted to the change without +much reluctance. + +As for Wickham and Lydia, their characters suffered no revolution from +the marriage of her sisters. He bore with philosophy the conviction that +Elizabeth must now become acquainted with whatever of his ingratitude +and falsehood had before been unknown to her; and, in spite of +everything, was not wholly without hope that Darcy might yet be +prevailed on to make his fortune. The congratulatory letter which +Elizabeth received from Lydia on her marriage explained to her that, by +his wife at least, if not by himself, such a hope was cherished. The +letter was to this effect:-- + + /* β€œMy dear Lizzy, */ + + β€œI wish you joy. If you love Mr. Darcy half so well as I do my dear + Wickham, you must be very happy. It is a great comfort to have you + so rich; and when you have nothing else to do, I hope you will + think of us. I am sure Wickham would like a place at court very + much; and I do not think we shall have quite money enough to live + upon without some help. Any place would do of about three or four + hundred a year; but, however, do not speak to Mr. Darcy about it, + if you had rather not. + +β€œYours,” etc. + +As it happened that Elizabeth had much rather not, she endeavoured in +her answer to put an end to every entreaty and expectation of the kind. +Such relief, however, as it was in her power to afford, by the practice +of what might be called economy in her own private expenses, she +frequently sent them. It had always been evident to her that such an +income as theirs, under the direction of two persons so extravagant in +their wants, and heedless of the future, must be very insufficient to +their support; and whenever they changed their quarters, either Jane or +herself were sure of being applied to for some little assistance towards +discharging their bills. Their manner of living, even when the +restoration of peace dismissed them to a home, was unsettled in the +extreme. They were always moving from place to place in quest of a +cheap situation, and always spending more than they ought. His affection +for her soon sunk into indifference: hers lasted a little longer; and, +in spite of her youth and her manners, she retained all the claims to +reputation which her marriage had given her. Though Darcy could never +receive _him_ at Pemberley, yet, for Elizabeth’s sake, he assisted him +further in his profession. Lydia was occasionally a visitor there, when +her husband was gone to enjoy himself in London or Bath; and with the +Bingleys they both of them frequently stayed so long, that even +Bingley’s good-humour was overcome, and he proceeded so far as to _talk_ +of giving them a hint to be gone. + +Miss Bingley was very deeply mortified by Darcy’s marriage; but as she +thought it advisable to retain the right of visiting at Pemberley, she +dropped all her resentment; was fonder than ever of Georgiana, almost as +attentive to Darcy as heretofore, and paid off every arrear of civility +to Elizabeth. + +Pemberley was now Georgiana’s home; and the attachment of the sisters +was exactly what Darcy had hoped to see. They were able to love each +other, even as well as they intended. Georgiana had the highest opinion +in the world of Elizabeth; though at first she often listened with an +astonishment bordering on alarm at her lively, sportive manner of +talking to her brother. He, who had always inspired in herself a respect +which almost overcame her affection, she now saw the object of open +pleasantry. Her mind received knowledge which had never before fallen in +her way. By Elizabeth’s instructions she began to comprehend that a +woman may take liberties with her husband, which a brother will not +always allow in a sister more than ten years younger than himself. + +Lady Catherine was extremely indignant on the marriage of her nephew; +and as she gave way to all the genuine frankness of her character, in +her reply to the letter which announced its arrangement, she sent him +language so very abusive, especially of Elizabeth, that for some time +all intercourse was at an end. But at length, by Elizabeth’s persuasion, +he was prevailed on to overlook the offence, and seek a reconciliation; +and, after a little further resistance on the part of his aunt, her +resentment gave way, either to her affection for him, or her curiosity +to see how his wife conducted herself; and she condescended to wait on +them at Pemberley, in spite of that pollution which its woods had +received, not merely from the presence of such a mistress, but the +visits of her uncle and aunt from the city. + +With the Gardiners they were always on the most intimate terms. Darcy, +as well as Elizabeth, really loved them; and they were both ever +sensible of the warmest gratitude towards the persons who, by bringing +her into Derbyshire, had been the means of uniting them. + + [Illustration: + + THE + END + ] + + + + + CHISWICK PRESS:--CHARLES WHITTINGHAM AND CO. + TOOKS COURT, CHANCERY LANE, LONDON. + + + + +*** END OF THE PROJECT GUTENBERG EBOOK PRIDE AND PREJUDICE *** + + + + +Updated editions will replace the previous oneβ€”the old editions will +be renamed. + +Creating the works from print editions not protected by U.S. copyright +law means that no one owns a United States copyright in these works, +so the Foundation (and you!) can copy and distribute it in the United +States without permission and without paying copyright +royalties. Special rules, set forth in the General Terms of Use part +of this license, apply to copying and distributing Project +Gutenbergβ„’ electronic works to protect the PROJECT GUTENBERGβ„’ +concept and trademark. Project Gutenberg is a registered trademark, +and may not be used if you charge for an eBook, except by following +the terms of the trademark license, including paying royalties for use +of the Project Gutenberg trademark. If you do not charge anything for +copies of this eBook, complying with the trademark license is very +easy. You may use this eBook for nearly any purpose such as creation +of derivative works, reports, performances and research. Project +Gutenberg eBooks may be modified and printed and given awayβ€”you may +do practically ANYTHING in the United States with eBooks not protected +by U.S. copyright law. Redistribution is subject to the trademark +license, especially commercial redistribution. + + +START: FULL LICENSE + +THE FULL PROJECT GUTENBERG LICENSE + +PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK + +To protect the Project Gutenbergβ„’ mission of promoting the free +distribution of electronic works, by using or distributing this work +(or any other work associated in any way with the phrase β€œProject +Gutenberg”), you agree to comply with all the terms of the Full +Project Gutenbergβ„’ License available with this file or online at +www.gutenberg.org/license. + +Section 1. General Terms of Use and Redistributing Project Gutenbergβ„’ +electronic works + +1.A. By reading or using any part of this Project Gutenbergβ„’ +electronic work, you indicate that you have read, understand, agree to +and accept all the terms of this license and intellectual property +(trademark/copyright) agreement. If you do not agree to abide by all +the terms of this agreement, you must cease using and return or +destroy all copies of Project Gutenbergβ„’ electronic works in your +possession. If you paid a fee for obtaining a copy of or access to a +Project Gutenbergβ„’ electronic work and you do not agree to be bound +by the terms of this agreement, you may obtain a refund from the person +or entity to whom you paid the fee as set forth in paragraph 1.E.8. + +1.B. β€œProject Gutenberg” is a registered trademark. It may only be +used on or associated in any way with an electronic work by people who +agree to be bound by the terms of this agreement. There are a few +things that you can do with most Project Gutenbergβ„’ electronic works +even without complying with the full terms of this agreement. See +paragraph 1.C below. There are a lot of things you can do with Project +Gutenbergβ„’ electronic works if you follow the terms of this +agreement and help preserve free future access to Project Gutenbergβ„’ +electronic works. See paragraph 1.E below. + +1.C. The Project Gutenberg Literary Archive Foundation (β€œthe +Foundation” or PGLAF), owns a compilation copyright in the collection +of Project Gutenbergβ„’ electronic works. Nearly all the individual +works in the collection are in the public domain in the United +States. If an individual work is unprotected by copyright law in the +United States and you are located in the United States, we do not +claim a right to prevent you from copying, distributing, performing, +displaying or creating derivative works based on the work as long as +all references to Project Gutenberg are removed. Of course, we hope +that you will support the Project Gutenbergβ„’ mission of promoting +free access to electronic works by freely sharing Project Gutenbergβ„’ +works in compliance with the terms of this agreement for keeping the +Project Gutenbergβ„’ name associated with the work. You can easily +comply with the terms of this agreement by keeping this work in the +same format with its attached full Project Gutenbergβ„’ License when +you share it without charge with others. + +1.D. The copyright laws of the place where you are located also govern +what you can do with this work. Copyright laws in most countries are +in a constant state of change. If you are outside the United States, +check the laws of your country in addition to the terms of this +agreement before downloading, copying, displaying, performing, +distributing or creating derivative works based on this work or any +other Project Gutenbergβ„’ work. The Foundation makes no +representations concerning the copyright status of any work in any +country other than the United States. + +1.E. Unless you have removed all references to Project Gutenberg: + +1.E.1. The following sentence, with active links to, or other +immediate access to, the full Project Gutenbergβ„’ License must appear +prominently whenever any copy of a Project Gutenbergβ„’ work (any work +on which the phrase β€œProject Gutenberg” appears, or with which the +phrase β€œProject Gutenberg” is associated) is accessed, displayed, +performed, viewed, copied or distributed: + + This eBook is for the use of anyone anywhere in the United States and most + other parts of the world at no cost and with almost no restrictions + whatsoever. You may copy it, give it away or re-use it under the terms + of the Project Gutenberg License included with this eBook or online + at www.gutenberg.org. If you + are not located in the United States, you will have to check the laws + of the country where you are located before using this eBook. + +1.E.2. If an individual Project Gutenbergβ„’ electronic work is +derived from texts not protected by U.S. copyright law (does not +contain a notice indicating that it is posted with permission of the +copyright holder), the work can be copied and distributed to anyone in +the United States without paying any fees or charges. If you are +redistributing or providing access to a work with the phrase β€œProject +Gutenberg” associated with or appearing on the work, you must comply +either with the requirements of paragraphs 1.E.1 through 1.E.7 or +obtain permission for the use of the work and the Project Gutenbergβ„’ +trademark as set forth in paragraphs 1.E.8 or 1.E.9. + +1.E.3. If an individual Project Gutenbergβ„’ electronic work is posted +with the permission of the copyright holder, your use and distribution +must comply with both paragraphs 1.E.1 through 1.E.7 and any +additional terms imposed by the copyright holder. Additional terms +will be linked to the Project Gutenbergβ„’ License for all works +posted with the permission of the copyright holder found at the +beginning of this work. + +1.E.4. Do not unlink or detach or remove the full Project Gutenbergβ„’ +License terms from this work, or any files containing a part of this +work or any other work associated with Project Gutenbergβ„’. + +1.E.5. Do not copy, display, perform, distribute or redistribute this +electronic work, or any part of this electronic work, without +prominently displaying the sentence set forth in paragraph 1.E.1 with +active links or immediate access to the full terms of the Project +Gutenbergβ„’ License. + +1.E.6. You may convert to and distribute this work in any binary, +compressed, marked up, nonproprietary or proprietary form, including +any word processing or hypertext form. However, if you provide access +to or distribute copies of a Project Gutenbergβ„’ work in a format +other than β€œPlain Vanilla ASCII” or other format used in the official +version posted on the official Project Gutenbergβ„’ website +(www.gutenberg.org), you must, at no additional cost, fee or expense +to the user, provide a copy, a means of exporting a copy, or a means +of obtaining a copy upon request, of the work in its original β€œPlain +Vanilla ASCII” or other form. Any alternate format must include the +full Project Gutenbergβ„’ License as specified in paragraph 1.E.1. + +1.E.7. Do not charge a fee for access to, viewing, displaying, +performing, copying or distributing any Project Gutenbergβ„’ works +unless you comply with paragraph 1.E.8 or 1.E.9. + +1.E.8. You may charge a reasonable fee for copies of or providing +access to or distributing Project Gutenbergβ„’ electronic works +provided that: + + β€’ You pay a royalty fee of 20% of the gross profits you derive from + the use of Project Gutenbergβ„’ works calculated using the method + you already use to calculate your applicable taxes. The fee is owed + to the owner of the Project Gutenbergβ„’ trademark, but he has + agreed to donate royalties under this paragraph to the Project + Gutenberg Literary Archive Foundation. Royalty payments must be paid + within 60 days following each date on which you prepare (or are + legally required to prepare) your periodic tax returns. Royalty + payments should be clearly marked as such and sent to the Project + Gutenberg Literary Archive Foundation at the address specified in + Section 4, β€œInformation about donations to the Project Gutenberg + Literary Archive Foundation.” + + β€’ You provide a full refund of any money paid by a user who notifies + you in writing (or by e-mail) within 30 days of receipt that s/he + does not agree to the terms of the full Project Gutenbergβ„’ + License. You must require such a user to return or destroy all + copies of the works possessed in a physical medium and discontinue + all use of and all access to other copies of Project Gutenbergβ„’ + works. + + β€’ You provide, in accordance with paragraph 1.F.3, a full refund of + any money paid for a work or a replacement copy, if a defect in the + electronic work is discovered and reported to you within 90 days of + receipt of the work. + + β€’ You comply with all other terms of this agreement for free + distribution of Project Gutenbergβ„’ works. + + +1.E.9. If you wish to charge a fee or distribute a Project +Gutenbergβ„’ electronic work or group of works on different terms than +are set forth in this agreement, you must obtain permission in writing +from the Project Gutenberg Literary Archive Foundation, the manager of +the Project Gutenbergβ„’ trademark. Contact the Foundation as set +forth in Section 3 below. + +1.F. + +1.F.1. Project Gutenberg volunteers and employees expend considerable +effort to identify, do copyright research on, transcribe and proofread +works not protected by U.S. copyright law in creating the Project +Gutenbergβ„’ collection. Despite these efforts, Project Gutenbergβ„’ +electronic works, and the medium on which they may be stored, may +contain β€œDefects,” such as, but not limited to, incomplete, inaccurate +or corrupt data, transcription errors, a copyright or other +intellectual property infringement, a defective or damaged disk or +other medium, a computer virus, or computer codes that damage or +cannot be read by your equipment. + +1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the β€œRight +of Replacement or Refund” described in paragraph 1.F.3, the Project +Gutenberg Literary Archive Foundation, the owner of the Project +Gutenbergβ„’ trademark, and any other party distributing a Project +Gutenbergβ„’ electronic work under this agreement, disclaim all +liability to you for damages, costs and expenses, including legal +fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT +LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE +PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE FOUNDATION, THE +TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE +LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR +INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH +DAMAGE. + +1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a +defect in this electronic work within 90 days of receiving it, you can +receive a refund of the money (if any) you paid for it by sending a +written explanation to the person you received the work from. If you +received the work on a physical medium, you must return the medium +with your written explanation. The person or entity that provided you +with the defective work may elect to provide a replacement copy in +lieu of a refund. If you received the work electronically, the person +or entity providing it to you may choose to give you a second +opportunity to receive the work electronically in lieu of a refund. If +the second copy is also defective, you may demand a refund in writing +without further opportunities to fix the problem. + +1.F.4. Except for the limited right of replacement or refund set forth +in paragraph 1.F.3, this work is provided to you β€˜AS-IS’, WITH NO +OTHER WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PURPOSE. + +1.F.5. Some states do not allow disclaimers of certain implied +warranties or the exclusion or limitation of certain types of +damages. If any disclaimer or limitation set forth in this agreement +violates the law of the state applicable to this agreement, the +agreement shall be interpreted to make the maximum disclaimer or +limitation permitted by the applicable state law. The invalidity or +unenforceability of any provision of this agreement shall not void the +remaining provisions. + +1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the +trademark owner, any agent or employee of the Foundation, anyone +providing copies of Project Gutenbergβ„’ electronic works in +accordance with this agreement, and any volunteers associated with the +production, promotion and distribution of Project Gutenbergβ„’ +electronic works, harmless from all liability, costs and expenses, +including legal fees, that arise directly or indirectly from any of +the following which you do or cause to occur: (a) distribution of this +or any Project Gutenbergβ„’ work, (b) alteration, modification, or +additions or deletions to any Project Gutenbergβ„’ work, and (c) any +Defect you cause. + +Section 2. Information about the Mission of Project Gutenbergβ„’ + +Project Gutenbergβ„’ is synonymous with the free distribution of +electronic works in formats readable by the widest variety of +computers including obsolete, old, middle-aged and new computers. It +exists because of the efforts of hundreds of volunteers and donations +from people in all walks of life. + +Volunteers and financial support to provide volunteers with the +assistance they need are critical to reaching Project Gutenbergℒ’s +goals and ensuring that the Project Gutenbergβ„’ collection will +remain freely available for generations to come. In 2001, the Project +Gutenberg Literary Archive Foundation was created to provide a secure +and permanent future for Project Gutenbergβ„’ and future +generations. To learn more about the Project Gutenberg Literary +Archive Foundation and how your efforts and donations can help, see +Sections 3 and 4 and the Foundation information page at www.gutenberg.org. + +Section 3. Information about the Project Gutenberg Literary Archive Foundation + +The Project Gutenberg Literary Archive Foundation is a non-profit +501(c)(3) educational corporation organized under the laws of the +state of Mississippi and granted tax exempt status by the Internal +Revenue Service. The Foundation’s EIN or federal tax identification +number is 64-6221541. Contributions to the Project Gutenberg Literary +Archive Foundation are tax deductible to the full extent permitted by +U.S. federal laws and your state’s laws. + +The Foundation’s business office is located at 809 North 1500 West, +Salt Lake City, UT 84116, (801) 596-1887. Email contact links and up +to date contact information can be found at the Foundation’s website +and official page at www.gutenberg.org/contact + +Section 4. Information about Donations to the Project Gutenberg +Literary Archive Foundation + +Project Gutenbergβ„’ depends upon and cannot survive without widespread +public support and donations to carry out its mission of +increasing the number of public domain and licensed works that can be +freely distributed in machine-readable form accessible by the widest +array of equipment including outdated equipment. Many small donations +($1 to $5,000) are particularly important to maintaining tax exempt +status with the IRS. + +The Foundation is committed to complying with the laws regulating +charities and charitable donations in all 50 states of the United +States. Compliance requirements are not uniform and it takes a +considerable effort, much paperwork and many fees to meet and keep up +with these requirements. We do not solicit donations in locations +where we have not received written confirmation of compliance. To SEND +DONATIONS or determine the status of compliance for any particular state +visit www.gutenberg.org/donate. + +While we cannot and do not solicit contributions from states where we +have not met the solicitation requirements, we know of no prohibition +against accepting unsolicited donations from donors in such states who +approach us with offers to donate. + +International donations are gratefully accepted, but we cannot make +any statements concerning tax treatment of donations received from +outside the United States. U.S. laws alone swamp our small staff. + +Please check the Project Gutenberg web pages for current donation +methods and addresses. Donations are accepted in a number of other +ways including checks, online payments and credit card donations. To +donate, please visit: www.gutenberg.org/donate. + +Section 5. General Information About Project Gutenbergβ„’ electronic works + +Professor Michael S. Hart was the originator of the Project +Gutenbergβ„’ concept of a library of electronic works that could be +freely shared with anyone. For forty years, he produced and +distributed Project Gutenbergβ„’ eBooks with only a loose network of +volunteer support. + +Project Gutenbergβ„’ eBooks are often created from several printed +editions, all of which are confirmed as not protected by copyright in +the U.S. unless a copyright notice is included. Thus, we do not +necessarily keep eBooks in compliance with any particular paper +edition. + +Most people start at our website which has the main PG search +facility: www.gutenberg.org. + +This website includes information about Project Gutenbergβ„’, +including how to make donations to the Project Gutenberg Literary +Archive Foundation, how to help produce our new eBooks, and how to +subscribe to our email newsletter to hear about new eBooks. diff --git a/data/huawei_pangu.md b/data/huawei_pangu.md new file mode 100644 index 0000000..0dd35da --- /dev/null +++ b/data/huawei_pangu.md @@ -0,0 +1,82 @@ +# η›˜ε€δΉ‹ζ‡οΌšεŽδΈΊθ―ΊδΊšη›˜ε€ε€§ζ¨‘εž‹η ”ε‘εŽ†η¨‹ηš„εΏƒι…ΈδΈŽι»‘ζš— + +各位ε₯½οΌŒ + +ζˆ‘ζ˜―δΈ€εη›˜ε€ε€§ζ¨‘εž‹ε›’ι˜ŸοΌŒεŽδΈΊθ―ΊδΊšζ–ΉθˆŸεžιͺŒε€ηš„ε‘˜ε·₯。 + +ι¦–ε…ˆδΈΊθ‡ͺθ―θΊ«δ»½οΌŒεˆ—δΈΎδΈ€δΊ›η»†θŠ‚οΌš + +1. ηŽ°θ―ΊδΊšδΈ»δ»»οΌŒε‰η—ζ³•εΊ”η”¨ιƒ¨ιƒ¨ι•ΏοΌŒεŽζ”ΉεδΈΊε°ζ¨‘εž‹εžιͺŒε€ηš„δΈ»δ»»ηŽ‹δΊ‘ιΉ€γ€‚ε‰θ―ΊδΊšδΈ»δ»»οΌšε§šιͺοΌˆε€§εΆη§°ε§šθ€εΈˆοΌ‰γ€‚ε‡ δΈͺεžιͺŒε€δΈ»δ»»οΌšε”ηΏζ˜ŽοΌˆζ˜Žε“₯οΌŒζ˜Žι˜ŸοΌŒε·²η¦»θŒοΌ‰οΌŒε°šεˆ©ε³°οΌŒεΌ η»΄οΌˆη»΄ε“₯οΌ‰οΌŒιƒε»ΊδΈšοΌˆιƒθ€εΈˆοΌ‰οΌŒεˆ˜ζ­¦ιΎ™οΌˆη§°ε‘ΌδΈΊζ­¦ιΎ™ζ‰€οΌ‰η­‰γ€‚ε…Άδ»–ιͺ¨εΉ²ζˆε‘˜ε’ŒδΈ“εΆι™†η»­ζœ‰εΎˆε€šδΊΊη¦»θŒγ€‚ +2. ζˆ‘δ»¬ιšΆε±žδΊŽβ€œε››ι‡Žβ€θΏ™δΈͺη»„η»‡γ€‚ε››ι‡ŽδΈ‹ε±žζœ‰θΈε€šηΊ΅ι˜ŸοΌŒεŸΊη‘€θ―­θ¨€ε€§ζ¨‘εž‹ζ˜―ε››ηΊ΅γ€‚ηŽ‹δΊ‘ιΉ€ηš„ε°ζ¨‘εž‹ζ˜―εε…­ηΊ΅ι˜Ÿγ€‚ζˆ‘δ»¬ε‚εŠ θΏ‡θ‹ε·žηš„ι›†η»“οΌŒζœ‰ε„η§ζœˆδ»½ηš„ζ—Άι—΄θŠ‚η‚Ήγ€‚εœ¨θ‹ε·žζ”»ε…³δΌšι’ε‘δ»»εŠ‘δ»€οΌŒιœ€θ¦εœ¨θŠ‚η‚Ήε‰θΎΎζˆη›ζ ‡γ€‚θ‹ε·žι›†η»“δΌšζŠŠε„εœ°ηš„δΊΊε‘˜ιƒ½ι›†δΈ­εœ¨θ‹ε·žη ”η©Άζ‰€οΌŒεΉ³εΈΈδ½εΎι¦†οΌŒζ―”ε¦‚εœ¨η”ͺη›΄ηš„ι…’εΊ—οΌŒδΈŽεΆδΊΊε­©ε­ε€©ε„一方。 +3. εœ¨θ‹ε·žι›†η»“ηš„ζ—Άε€™ε‘¨ε…­ι»˜θ€δΈŠη­οΌŒιžεΈΈθΎ›θ‹¦οΌŒδΈθΏ‡ε‘¨ε…­ζœ‰δΈ‹εˆθŒΆοΌŒζœ‰δΈ€ζ¬‘θΏ˜ζœ‰ε°ιΎ™θ™Ύγ€‚εœ¨θ‹ε·žη ”η©Άζ‰€ηš„ε·₯δ½ζ¬θΏθΏ‡δΈ€ζ¬‘οΌŒδ»ŽδΈ€ζ ‹ζ₯Όζ’εˆ°δΊ†ε¦δΈ€ζ ‹γ€‚θ‹ε·žη ”η©Άζ‰€ζ₯Όζ ‹ιƒ½ζ˜―欧式装δΏοΌŒι—¨ε£ζœ‰ε€§ε‘οΌŒι‡Œι’ζ™―θ‰²εΎˆδΈι”™γ€‚εŽ»θ‹ε·žι›†η»“δΈ€θˆ¬θ‡³ε°‘θ¦εŽ»δΈ€ε‘¨οΌŒη”šθ‡³ζ›΄δΉ…οΌŒε€šηš„δΊΊη”šθ‡³δΈ€δΈ€δΈͺζœˆιƒ½ε›žδΈδΊ†εΆγ€‚ +4. θ―ΊδΊšζ›Ύη»δΌ θ―΄ζ˜―η ”η©Άεž‹ηš„οΌŒδ½†ζ˜―ζ₯δΊ†δΉ‹εŽε› δΈΊεœ¨ε››ι‡Žεšε€§ζ¨‘εž‹ι‘Ήη›οΌŒι‘Ήη›ζˆε‘˜εŒε…¨ε˜ζˆδΊ†δΊ€δ»˜εž‹ηš„οΌŒδΈ”ε……ζ»‘δΊ†δΎ‹δΌšοΌŒθ―„ε‘οΌŒζ±‡ζŠ₯γ€‚εΎˆε€šζ—Άε€™εšεžιͺŒιƒ½θ¦η”³θ―·γ€‚ε›’ι˜Ÿιœ€θ¦ε―ΉζŽ₯η»ˆη«―ε°θ‰ΊοΌŒεŽδΈΊδΊ‘οΌŒICTη­‰θ―Έε€šδΈšεŠ‘ηΊΏοΌŒδΊ€δ»˜εŽ‹εŠ›δΈε°γ€‚ +5. θ―ΊδΊšη ”ε‘ηš„η›˜ε€ζ¨‘εž‹ζ—©ζœŸε†…ιƒ¨δ»£ε·ε«εšβ€œη›˜ε€ζ™Ίε­β€οΌŒδΈ€εΌ€ε§‹εͺζœ‰ε†…ιƒ¨ιœ€θ¦η”³θ―·θ―•η”¨ηš„η½‘ι‘΅η‰ˆοΌŒεˆ°εŽη»­θΏ«δΊŽεŽ‹εŠ›εœ¨welink上ζŽ₯ε…₯ε’Œε…¬ζ΅‹εΌ€ζ”Ύγ€‚ + +θΏ™δΊ›ε€©ε‘η”Ÿε…³δΊŽθ΄¨η–‘η›˜ε€ε€§ζ¨‘εž‹ζŠ„θ’­εƒι—ηš„δΊ‹ζƒ…ι—Ήηš„ζ²Έζ²Έζ‰¬ζ‰¬γ€‚δ½œδΈΊδΈ€δΈͺη›˜ε€ε›’ι˜Ÿηš„ζˆε‘˜οΌŒζˆ‘ζœ€θΏ‘ε€œε€œθΎ—θ½¬εδΎ§οΌŒιšΎδ»₯ε…₯ηœ γ€‚η›˜ε€ηš„ε“η‰Œε—εˆ°ε¦‚ζ­€ε€§ηš„ε½±ε“οΌŒδΈ€ζ–Ήι’οΌŒζˆ‘θ‡ͺη§ηš„δΈΊζˆ‘ηš„θŒδΈšε‘ε±•ζ‹…εΏ§οΌŒδΉŸδΈΊθ‡ͺε·±θΏ‡εŽ»ηš„εŠͺεŠ›ε·₯δ½œζ„Ÿεˆ°δΈε€Όγ€‚ε¦δΈ€ζ–Ήι’οΌŒη”±δΊŽζœ‰δΊΊεΌ€ε§‹ζ­ιœ²θΏ™δΊ›δΊ‹ζƒ…ζˆ‘ε†…εΏƒεˆζ„Ÿεˆ°ε€§εΏ«δΊΊεΏƒγ€‚εœ¨ε€šε°‘δΈͺζ—₯ζ—₯ε€œε€œοΌŒζˆ‘δ»¬ε―Ήε†…ιƒ¨ζŸδΊ›δΊΊδΈ€ζ¬‘ζ¬‘ι η€ι€ ε‡θ€ŒεˆθŽ·εΎ—δΊ†ζ— ζ•°εˆ©η›Šηš„θ‘ŒδΈΊε’¬η‰™εˆ‡ι½Ώθ€Œεˆζ— θƒ½δΈΊεŠ›γ€‚θΏ™η§εŽ‹ζŠ‘ε’ŒηΎžθΎ±δΉŸι€ζΈζΆˆη£¨δΊ†ζˆ‘ε―ΉεŽδΈΊηš„ζ„Ÿζƒ…οΌŒθ©ζˆ‘εœ¨θΏ™ι‡Œηš„ζ—Άζ—₯ι€ζΈζ΅‘ζ΅‘ε™©ε™©οΌŒθΏ·θŒ«ζ— ζŽͺοΌŒζ—ΆεΈΈζ€€η–‘θ‡ͺε·±ηš„δΊΊη”Ÿε’Œθ‡ͺζˆ‘δ»·ε€Όγ€‚ + +ζˆ‘ζ‰Ώθ€ζˆ‘ζ˜―δΈ€δΈͺζ‡¦εΌ±ηš„δΊΊοΌŒδ½œδΈΊδΈ€δΈͺε°ε°ηš„ζ‰“ε·₯δΊΊοΌŒζˆ‘δΈδ»…δΈζ•’ε’ŒηŽ‹δΊ‘ιΉ€η­‰ε†…ιƒ¨ζ‰‹ηœΌι€šε€©ηš„δΊΊεšε―ΉοΌŒζ›΄δΈζ•’ε’ŒεŽδΈΊθΏ™ζ ·ηš„εΊžη„Άε€§η‰©εšε―Ήγ€‚ζˆ‘εΎˆζ€•ε€±εŽ»ζˆ‘ηš„ε·₯δ½œοΌŒζ―•η«Ÿζˆ‘δΉŸζœ‰εΆδΊΊε’Œε­©ε­οΌŒζ‰€δ»₯ζˆ‘ζ‰“εΏƒηœΌι‡ŒεΎˆδ½©ζœζ­ιœ²θ€…γ€‚δ½†ζ˜―οΌŒηœ‹εˆ°ε†…ιƒ¨θΏ˜εœ¨θ―•ε›Ύζ΄—εœ°ζŽ©η›–δΊ‹εžοΌŒθ’™θ”½ε…¬δΌ—ηš„ζ—Άε€™οΌŒζˆ‘εžεœ¨δΈθƒ½εΉεΏδΊ†γ€‚ζˆ‘δΉŸεΈŒζœ›ε‹‡ζ•’δΈ€ζ¬‘οΌŒι‘Ίδ»Žθ‡ͺε·±ζœ¬εΏƒγ€‚ε°±η—θ‡ͺζŸε…«η™ΎοΌŒζˆ‘δΉŸεΈŒζœ›θƒ½δΌ€ζ•ŒδΈ€εƒγ€‚ζˆ‘ε†³εšζŠŠζˆ‘εœ¨θΏ™ι‡Œηš„ζ‰€θ§ζ‰€ι—»οΌˆιƒ¨εˆ†ζ₯θ‡ͺδΊŽεŒδΊ‹ε£θΏ°οΌ‰ε…¬εΈƒε‡Ίζ₯οΌŒε…³δΊŽη›˜ε€ε€§ζ¨‘εž‹ηš„β€œδΌ ε₯‡ζ•…δΊ‹β€οΌš + +华为η‘εžδΈ»θ¦εœ¨ζ˜‡θ…Ύε‘上θ­η»ƒε€§ζ¨‘εž‹οΌˆε°ζ¨‘εž‹εžιͺŒε€ζœ‰δΈε°‘θ‹±δΌŸθΎΎηš„ε‘οΌŒδ»–δ»¬δΉ‹ε‰δΉŸδΌšη”¨ζ₯θ­η»ƒοΌŒεŽι’θ½¬η§»εˆ°ζ˜‡θ…ΎοΌ‰γ€‚ζ›Ύη»ζˆ‘θ’«εŽδΈΊβ€œζ‰“ι€ δΈ–η•Œη¬¬δΊŒι€‰ζ‹©β€ηš„ε†³εΏƒθ€ŒζŠ˜ζœοΌŒζˆ‘ζœ¬θΊ«δΉŸζ›Ύη»ε―ΉεŽδΈΊζœ‰ζ·±εŽšηš„ζ„Ÿζƒ…γ€‚ζˆ‘δ»¬ι™ͺη€ζ˜‡θ…ΎδΈ€ζ­₯ζ­₯ζ‘Έηˆ¬ζ»šζ‰“οΌŒδ»Žε……ζ»‘bugεˆ°ηŽ°εœ¨θƒ½θ­ε‡Ίζ¨‘εž‹οΌŒδ»˜ε‡ΊδΊ†ε·¨ε€§ηš„εΏƒθ‘€ε’Œδ»£δ»·γ€‚ + +ζœ€εˆζˆ‘δ»¬ηš„η—εŠ›ιžεΈΈζœ‰ι™οΌŒεœ¨910A上θ­η»ƒζ¨‘εž‹γ€‚ι‚£δΌšεͺζ”―ζŒfp16,θ­η»ƒηš„稳εšζ€§θΏœδΈε¦‚bf16γ€‚η›˜ε€ηš„moeεΌ€ε§‹εΎˆζ—©οΌŒ23年就主要是θ­η»ƒ38Bmoeζ¨‘εž‹ε’ŒεŽη»­ηš„71B denseζ¨‘εž‹γ€‚71Bηš„denseζ¨‘εž‹ι€šθΏ‡ζ‰©ε’žε˜ζˆδΊ†η¬¬δΈ€δ»£ηš„135Bdenseζ¨‘εž‹οΌŒεŽι’δΈ»εŠ›ζ¨‘εž‹δΉŸι€ζΈεœ¨910B上θ­η»ƒγ€‚ + +71Bε’Œ135Bζ¨‘εž‹ιƒ½ζœ‰δΈ€δΈͺε·¨ε€§ηš„η‘¬δΌ€ε°±ζ˜―tokenizerγ€‚ε½“ζ—Άδ½Ώη”¨ηš„tokenizerηΌ–η ζ•ˆηŽ‡ζžδ½ŽοΌŒζ―δΈͺ单δΈͺηš„η¬¦ε·οΌŒζ•°ε­—οΌŒη©Ίζ ΌοΌŒδΉƒθ‡³ζ±‰ε­—ιƒ½δΌšε η”¨δΈ€δΈͺtokenγ€‚ε―ζƒ³θ€ŒηŸ₯θΏ™δΌšιžεΈΈζ΅ͺθ΄Ήη—εŠ›οΌŒδΈ”δ½ΏεΎ—ζ¨‘εž‹ηš„ζ•ˆζžœεΎˆε·γ€‚θΏ™ζ—Άε€™ε°ζ¨‘εž‹εžιͺŒε€ζ­£ε₯½ζœ‰δΈͺθ‡ͺε·±θ­ηš„θ―θ‘¨γ€‚ε§šθ€εΈˆε½“ζ—Άζ€€η–‘ζ˜―δΈζ˜―ζ¨‘εž‹ηš„tokenizer不ε₯½οΌˆθ™½η„ΆδΊ‹εŽζ₯ηœ‹οΌŒδ»–ηš„ζ€€η–‘ζ˜―ζ— η–‘ζ­£η‘ηš„οΌ‰οΌŒδΊŽζ˜―ε°±ε†³εšοΌŒθ©71Bε’Œ135B捒tokenizerοΌŒε› δΈΊε°ζ¨‘εž‹εžιͺŒε€ζ›Ύη»ε°θ―•θΏ‡γ€‚ε›’ι˜ŸηΌεˆδΊ†δΈ€δΈͺtokenizerοΌŒεΌ€ε§‹δΊ†tokenizerηš„ζ›΄ζ’γ€‚71Bζ¨‘εž‹ηš„ζ›΄ζ’ε€±θ΄₯δΊ†οΌŒθ€Œ135Bε› δΈΊι‡‡η”¨δΊ†ζ›΄η²Ύη»†ηš„embeddingεˆε§‹εŒ–η­–η•₯,续θ­δΊ†θ‡³ε°‘1Tηš„ζ•°ζεŽθ―θ‘¨ζ€»η—ζ›΄ζ’ζˆεŠŸοΌŒδ½†ε―ζƒ³θ€ŒηŸ₯οΌŒζ•ˆζžœεΉΆδΈδΌšε˜ε₯½γ€‚ + +δΊŽζ­€εŒζœŸοΌŒι˜Ώι‡Œε’Œζ™Ίθ°±η­‰ε›½ε†…ε…Άδ»–ε…¬εΈεœ¨GPU上θ­η»ƒοΌŒδΈ”已经摸紒出了正η‘ηš„ζ–Ήζ³•οΌŒη›˜ε€ε’Œη«žε“ηš„ε·θ·θΆŠζ₯θΆŠε€§γ€‚ε†…ιƒ¨δΈ€δΈͺ230B从倴θ­η»ƒηš„denseζ¨‘εž‹εˆε› δΈΊε„η§εŽŸε› θ­η»ƒε€±θ΄₯οΌŒε―Όθ‡΄ι‘Ήη›ηš„ηŠΆε†΅ε‡ δΉŽι™·ε…₯绝咃。青临几δΈͺθŠ‚η‚Ήηš„εŽ‹εŠ›δ»₯εŠε†…ιƒ¨ε―Ήη›˜ε€ηš„εΌΊηƒˆθ΄¨η–‘ζ—ΆοΌŒε›’ι˜Ÿηš„ε£«ζ°”δ½ŽθΏ·εˆ°δΊ†ζžη‚Ήγ€‚ε›’ι˜Ÿεœ¨η—εŠ›ζžε…Άζœ‰ι™ηš„ζ—Άε€™οΌŒεšε‡ΊδΊ†εΎˆε€šεŠͺεŠ›ε’ŒζŒ£ζ‰Žγ€‚ζ―”ε¦‚οΌŒε›’ι˜ŸεΆη„Άε‘ηŽ°ε½“ζ—Άηš„38B moeεΉΆζ²‘ζœ‰ι’„ζœŸmoeηš„ζ•ˆζžœγ€‚δΊŽζ˜―εŽ»ζŽ‰δΊ†moeε‚ζ•°οΌŒθΏ˜εŽŸδΈΊδΊ†13Bηš„denseζ¨‘εž‹γ€‚η”±δΊŽ38Bηš„moe源θ‡ͺεΎˆζ—©ηš„pangu alpha 13BοΌŒζžΆζž„η›Έε―Ήθ½εŽοΌŒε›’ι˜ŸθΏ›θ‘ŒδΊ†δΈ€η³»εˆ—ηš„ζ“δ½œοΌŒζ―”ε¦‚εˆ‡ζ’η»ε―Ήδ½η½ηΌ–η εˆ°ropeοΌŒεŽ»ζŽ‰biasοΌŒεˆ‡ζ’δΈΊrmsnormγ€‚εŒζ—Άι‰΄δΊŽtokenizerηš„δΈ€δΊ›ε€±θ΄₯ε’Œζ’θ―θ‘¨ηš„η»ιͺŒοΌŒθΏ™δΈͺζ¨‘εž‹ηš„θ―θ‘¨δΉŸζ›΄ζ’δΈΊδΊ†ηŽ‹δΊ‘ιΉ€ηš„ε°ζ¨‘εž‹εžιͺŒε€7Bζ¨‘εž‹ζ‰€δ½Ώη”¨ηš„θ―θ‘¨γ€‚εŽι’θΏ™δΈͺ13Bζ¨‘εž‹θΏ›θ‘ŒδΊ†ζ‰©ε’žη»­θ­οΌŒε˜ζˆδΊ†η¬¬δΊŒδ»£38B denseζ¨‘εž‹οΌˆεœ¨ε‡ δΈͺζœˆε†…θΏ™δΈͺζ¨‘εž‹ιƒ½ζ˜―δΈ»θ¦ηš„η›˜ε€δΈ­ζ‘£δ½ζ¨‘εž‹οΌ‰οΌŒζ›Ύη»ε…·ζœ‰δΈ€εšηš„η«žδΊ‰εŠ›γ€‚δ½†ζ˜―οΌŒη”±δΊŽζ›΄ε€§ηš„135Bζ¨‘εž‹ζžΆζž„θ½εŽοΌŒδΈ”ζ›΄ζ’θ―θ‘¨ζ¨‘εž‹ζŸδΌ€ε·¨ε€§οΌˆεŽη»­εˆ†ζžε‘ηŽ°ε½“ζ—Άζ›΄ζ’ηš„ηΌεˆθ―θ‘¨ζœ‰ζ›΄δΈ₯ι‡ηš„bugοΌ‰οΌŒη»­θ­εŽδΉŸδΈŽεƒι—η­‰ε½“ζ—Άε›½ε†…ι’†ε…ˆζ¨‘εž‹ε­˜εœ¨εΎˆε€§ε·θ·γ€‚θΏ™ζ—Άη”±δΊŽε†…ιƒ¨ηš„θ΄¨η–‘ε£°ε’Œι’†ε―Όηš„εŽ‹εŠ›δΉŸθΆŠζ₯θΆŠε€§γ€‚ε›’ι˜Ÿηš„ηŠΆζ€ε‡ δΉŽι™·ε…₯了绝咃。 + +εœ¨θΏ™η§ζƒ…ε†΅δΈ‹οΌŒηŽ‹δΊ‘ιΉ€ε’Œδ»–ηš„ε°ζ¨‘εž‹εžιͺŒε€ε‡Ίζ‰‹δΊ†γ€‚δ»–δ»¬ε£°η§°ζ˜―δ»Žζ—§ηš„135Bε‚ζ•°η»§ζ‰Ώζ”Ήι€ θ€Œζ₯οΌŒι€šθΏ‡θ­η»ƒηŸ­ηŸ­ηš„ε‡ η™ΎBζ•°ζοΌŒε„ι‘ΉζŒ‡ζ ‡εΉ³ε‡ζε‡δΊ†εδΈͺ点左右。εžι™…δΈŠοΌŒθΏ™ε°±ζ˜―他们ε₯—ε£³εΊ”η”¨εˆ°ε€§ζ¨‘εž‹ηš„η¬¬δΈ€ζ¬‘ζ°δ½œγ€‚εŽδΈΊηš„ε€–θ‘Œι’†ε―Όε†…θ‘ŒοΌŒδ½ΏεΎ—ι’†ε―ΌεŒε…¨ε―ΉδΊŽθΏ™η§ζ‰―ζ·‘ηš„δΊ‹ζƒ…ζ²‘ζœ‰ζ¦‚εΏ΅οΌŒδ»–δ»¬εͺδΌšθ§‰εΎ—θ‚―εšζ˜―ζœ‰δ»€δΉˆη—ζ³•εˆ›ζ–°γ€‚η»θΏ‡ε†…ιƒ¨ηš„εˆ†ζžοΌŒδ»–δ»¬εžι™…δΈŠζ˜―使用Qwen 1.5 110Bη»­θ­θ€Œζ₯οΌŒι€šθΏ‡εŠ ε±‚οΌŒζ‰©ε’žffnη»΄εΊ¦οΌŒζ·»εŠ η›˜ε€piθΊζ–‡ηš„δΈ€δΊ›ζœΊεˆΆεΎ—ζ₯οΌŒε‡‘ε€ŸδΊ†ε€§ζ¦‚135Bηš„ε‚ζ•°γ€‚εžι™…δΈŠοΌŒζ—§ηš„135Bζœ‰107ε±‚οΌŒθ€ŒθΏ™δΈͺζ¨‘εž‹εͺζœ‰82ε±‚οΌŒε„η§ι…η½δΉŸιƒ½δΈδΈ€ζ ·γ€‚ζ–°ηš„ζ₯θ·―δΈζ˜Žηš„135Bθ­η»ƒεŒεΎˆε€šε‚ζ•°ηš„εˆ†εΈƒδΉŸε’ŒQwen 110Bε‡ δΉŽδΈ€ζ¨‘δΈ€ζ ·γ€‚θΏžζ¨‘εž‹δ»£η ηš„η±»εε½“ζ—Άιƒ½ζ˜―QwenοΌŒη”šθ‡³ζ‡’εΎ—ζ”Ήεγ€‚εŽη»­θΏ™δΈͺζ¨‘εž‹ε°±ζ˜―ζ‰€θ°“ηš„135B V2γ€‚θ€ŒθΏ™δΈͺζ¨‘εž‹ε½“ζ—ΆδΉŸζδΎ›η»™δΊ†εΎˆε€šδΈ‹ζΈΈοΌŒη”šθ‡³εŒ…ζ‹¬ε€–ιƒ¨ε’ζˆ·γ€‚ + +θΏ™δ»ΆδΊ‹ε―ΉδΊŽζˆ‘δ»¬θΏ™δΊ›θ€ηœŸθ―šεžεšδΊ‹ηš„εŒδΊ‹δ»¬εΈ¦ζ₯δΊ†ε·¨ε€§ηš„ε†²ε‡»οΌŒε†…ιƒ¨εΎˆε€šδΊΊε…Άεžιƒ½ηŸ₯ι“θΏ™δ»ΆδΊ‹οΌŒη”šθ‡³εŒ…ζ‹¬η»ˆη«―ε’ŒεŽδΈΊδΊ‘γ€‚ζˆ‘δ»¬ιƒ½ζˆη§°δ»₯εŽεˆ«ε«η›˜ε€ζ¨‘εž‹δΊ†οΌŒε«εƒε€ε§γ€‚ε½“ζ—Άε›’ι˜Ÿζˆε‘˜ε°±ζƒ³ε‘bcgδΈΎζŠ₯δΊ†οΌŒζ―•η«ŸθΏ™ε·²η»ζ˜―ι‡ε€§ηš„δΈšεŠ‘ι€ ε‡δΊ†γ€‚δ½†ζ˜―εŽι’ζθ―΄θ’«ι’†ε―Όζ‹¦δΊ†δΈ‹ζ₯οΌŒε› δΈΊζ›΄ι«˜ηΊ§εˆ«ηš„ι’†ε―ΌοΌˆζ―”ε¦‚ε§šθ€εΈˆοΌŒδ»₯εŠε―θƒ½η†Šζ€»ε’ŒζŸ₯老)兢εžεŽι’也ηŸ₯ι“δΊ†οΌŒδ½†ζ˜―εΉΆδΈη‘οΌŒε› δΈΊι€šθΏ‡ε₯—ε£³ζ‹Ώε‡Ίε₯½ηš„η»“ζžœοΌŒε―Ήδ»–δ»¬δΉŸζ˜―ζœ‰εˆ©ηš„γ€‚θΏ™δ»ΆδΊ‹δ½ΏεΎ—ε½“ζ—Άε›’ι˜Ÿε‡ δ½ζœ€εΌΊηš„εŒδΊ‹εΌ€ε§‹εΏƒη°ζ„ε†·οΌŒη¦»θŒθ·‘θ·―δΉŸι€ζΈζˆδΈΊζŒ‚εœ¨ε˜΄θΎΉηš„δΊ‹γ€‚ + +ζ­€ζ—ΆοΌŒη›˜ε€δΌΌδΉŽθΏŽζ₯δΊ†θ½¬ζœΊγ€‚η”±δΊŽε‰ι’ζ‰€θΏ°ηš„θΏ™δΊ›η›˜ε€ζ¨‘εž‹εŸΊζœ¬ιƒ½ζ˜―η»­θ­ε’Œζ”Ήι€ θ€Œζ₯οΌŒε½“ζ—Άθ―ΊδΊšεŒε…¨ζ²‘ζœ‰ζŽŒζ‘δ»Žε€΄θ­η»ƒηš„ζŠ€ζœ―οΌŒδ½•ε†΅θΏ˜ζ˜―εœ¨ζ˜‡θ…Ύηš„NPUδΈŠθΏ›θ‘Œθ­η»ƒγ€‚εœ¨ε½“ζ—Άε›’ι˜Ÿηš„ζ ΈεΏƒζˆε‘˜ηš„ζžεŠ›δΊ‰ε–δΈ‹οΌŒη›˜ε€εΌ€ε§‹δΊ†η¬¬δΈ‰δ»£ζ¨‘εž‹ηš„θ­η»ƒοΌŒδ»˜ε‡ΊδΊ†ε·¨ε€§ηš„εŠͺεŠ›εŽοΌŒεœ¨ζ•°ζζžΆζž„ε’Œθ­η»ƒη—ζ³•ζ–Ήι’ιƒ½δΈŽδΈšη•Œι€ζΈζŽ₯θ½¨οΌŒθ€ŒθΏ™ε…ΆδΈ­ηš„θ‰°θΎ›ε’Œε°ζ¨‘εž‹εžιͺŒε€ηš„δΊΊδΈ€η‚Ήε…³η³»ιƒ½ζ²‘ζœ‰γ€‚ + +δΈ€εΌ€ε§‹ε›’ι˜Ÿζˆε‘˜ζ―«ζ— δΏ‘εΏƒοΌŒεͺδ»ŽδΈ€δΈͺ13Bηš„ζ¨‘εž‹εΌ€ε§‹θ­η»ƒοΌŒδ½†ζ˜―εŽι’ε‘ηŽ°ζ•ˆζžœθΏ˜δΈι”™οΌŒδΊŽζ˜―θΏ™δΈͺζ¨‘εž‹εŽη»­ε†ζ¬‘θΏ›θ‘ŒδΊ†δΈ€ζ¬‘ε‚ζ•°ζ‰©ε’žοΌŒε˜ζˆδΊ†η¬¬δΈ‰δ»£ηš„38B,代号38B V3γ€‚ζƒ³εΏ…εΎˆε€šδΊ§ε“ηΊΏηš„ε…„εΌŸιƒ½ε―ΉθΏ™δΈͺζ¨‘εž‹εΎˆη†Ÿζ‚‰γ€‚ε½“ζ—ΆθΏ™δΈͺζ¨‘εž‹ηš„tokenizer是基于llamaηš„θ―θ‘¨θΏ›θ‘Œζ‰©ε±•ηš„οΌˆδΉŸζ˜―δΈšη•ŒεΈΈθ§ηš„εšζ³•οΌ‰γ€‚θ€Œε½“ζ—ΆηŽ‹δΊ‘ιΉ€ηš„εžιͺŒε€εšε‡Ίζ₯了另一δΈͺ词葨(也就是后续panguη³»εˆ—ηš„θ―θ‘¨οΌ‰γ€‚ε½“ζ—ΆδΈ€δΈͺθ―θ‘¨θΏ˜θ’«θΏ«θΏ›θ‘ŒδΊ†δΈ€ζ¬‘θ΅›ι©¬οΌŒζœ€η»ˆζ²‘ζœ‰ζ˜Žζ˜Ύηš„ε₯½εη»“θΊγ€‚δΊŽζ˜―οΌŒι’†ε―Όε½“ε³ε†³εšοΌŒεΊ”θ―₯η»ŸδΈ€θ―θ‘¨οΌŒδ½Ώη”¨ηŽ‹δΊ‘ιΉ€δ»–δ»¬ηš„γ€‚δΊŽζ˜―οΌŒεœ¨εŽη»­δ»Žε€΄θ­η»ƒηš„135B V3οΌˆδΉŸε°±ζ˜―ε―Ήε€–ηš„Pangu UltraοΌ‰οΌŒδΎΏζ˜―ι‡‡η”¨δΊ†θΏ™δΈͺtokenizerγ€‚θΏ™δΉŸθ§£ι‡ŠδΊ†εΎˆε€šδ½Ώη”¨ζˆ‘δ»¬ζ¨‘εž‹ηš„ε…„εΌŸηš„η–‘ζƒ‘οΌŒδΈΊδ»€δΉˆε½“ζ—ΆεŒδΈΊV3δ»£ηš„δΈ€δΈͺδΈεŒζ‘£δ½ηš„ζ¨‘εž‹οΌŒδΌšδ½Ώη”¨δΈεŒηš„tokenizer。 + + +ζˆ‘δ»¬ζ‰“εΏƒηœΌι‡Œθ§‰εΎ—οΌŒ135B V3ζ˜―ζˆ‘δ»¬ε››ηΊ΅ε›’ι˜Ÿε½“ζ—Άηš„ιͺ„ε‚²γ€‚θΏ™ζ˜―第一δΈͺηœŸζ­£ζ„δΉ‰δΈŠηš„οΌŒεŽδΈΊε…¨ζ ˆθ‡ͺη ”οΌŒζ­£η»δ»Žε€΄θ­η»ƒηš„εƒδΊΏηΊ§εˆ«ηš„ζ¨‘εž‹οΌŒδΈ”ζ•ˆζžœδΈŽ24εΉ΄εŒζœŸη«žε“ε―ζ―”ηš„γ€‚ε†™εˆ°θΏ™ι‡Œζˆ‘ε·²η»ηƒ­ζ³ͺη›ˆηœΆοΌŒε€ͺ不εΉζ˜“了。当既为了稳εšθ­η»ƒοΌŒε›’ι˜ŸεšδΊ†ε€§ι‡εžιͺŒε―Ήζ―”οΌŒεΉΆδΈ”ε€šζ¬‘εœ¨ζ¨‘εž‹ζ’―εΊ¦ε‡ΊηŽ°εΌ‚εΈΈηš„ζ—Άε€™θΏ›θ‘ŒεŠζ—Άε›žι€€ι‡ε―γ€‚θΏ™δΈͺζ¨‘εž‹ηœŸζ­£εšεˆ°δΊ†εŽι’ζŠ€ζœ―ζŠ₯ε‘Šζ‰€θ―΄ηš„θ­η»ƒε…¨η¨‹ζ²‘ζœ‰δΈ€δΈͺloss spikeγ€‚ζˆ‘δ»¬ε…‹ζœδΊ†δΈηŸ₯ι“ε€šε°‘ε›°ιšΎοΌŒζˆ‘δ»¬εšεˆ°δΊ†οΌŒζˆ‘δ»¬ζ„Ώη”¨η”Ÿε‘½ε’Œθ£θͺ‰δΏθ―θΏ™δΈͺζ¨‘εž‹θ­η»ƒηš„ηœŸεžζ€§γ€‚ε€šε°‘δΈͺε‡Œζ™¨οΌŒζˆ‘δ»¬δΈΊδΊ†εƒηš„θ­η»ƒθ€ŒδΈηœ γ€‚εœ¨θ’«ε†…ιƒ¨εΏƒε£°ιͺ‚ηš„δΈ€ζ–‡δΈε€Όηš„ζ—Άε€™οΌŒζˆ‘δ»¬ζœ‰ε€šδΉˆδΈη”˜οΌŒζœ‰ε€šε°‘ηš„ε§”ε±ˆοΌŒζˆ‘δ»¬ζŒΊδ½δΊ†γ€‚ + +ζˆ‘δ»¬θΏ™εΈδΊΊζ˜―ηœŸηš„εœ¨δΈΊζ‰“η£¨ε›½δΊ§η—εŠ›εΊ•εΊ§η‡ƒηƒ§θ‡ͺε·±ηš„ι’ζ˜₯ε•Šβ€¦β€¦ε’ε±…δ»–δΉ‘οΌŒζˆ‘δ»¬ζ”ΎεΌƒδΊ†εΆεΊ­οΌŒζ”ΎεΌƒδΊ†ε‡ζœŸοΌŒζ”ΎεΌƒδΊ†ε₯εΊ·οΌŒζ”ΎεΌƒδΊ†ε¨±δΉοΌŒζŠ›ε€΄ι’…ζ΄’ηƒ­θ‘€οΌŒε…ΆδΈ­ηš„θ‰°θΎ›δΈŽε›°θ‹¦οΌŒε―₯ε―₯数笔不袳δ»₯ζ¦‚ζ‹¬ε…ΆδΈ‡δΈ€γ€‚εœ¨ε„η§εŠ¨ε‘˜ε€§δΌšδΈŠοΌŒε½“ζ—Άε£ε·δΈ­ε–Šε‡Ίηš„η›˜ε€εΏ…θƒœοΌŒεŽδΈΊεΏ…θƒœοΌŒζˆ‘δ»¬εΏƒι‡Œζ˜―ηœŸηš„ζ·±ζ·±θ’«ζ„ŸεŠ¨γ€‚ + +η„Άθ€ŒοΌŒζˆ‘δ»¬ηš„ζ‰€ζœ‰θΎ›θ‹¦ηš„ζˆζžœοΌŒη»εΈΈθ’«ε°ζ¨‘εž‹εžιͺŒε€θ½»ι£˜ι£˜ηš„拿衰了。数ζοΌŒη›΄ζŽ₯θ¦θ΅°γ€‚δ»£η οΌŒη›΄ζŽ₯θ¦θ΅°οΌŒθΏ˜θ¦ζ±‚ζˆ‘δ»¬ι…εˆι€‚ι…εˆ°θƒ½δΈ€ι”θΏθ‘Œγ€‚ζˆ‘δ»¬ε½“ζ—Άζˆη§°ε°ζ¨‘εž‹εžιͺŒε€δΈΊη‚ΉιΌ ζ ‡εžιͺŒε€γ€‚ζˆ‘δ»¬δ»˜ε‡ΊθΎ›θ‹¦οΌŒδ»–δ»¬ε–εΎ—θ£θ€€γ€‚ζžœη„ΆεΊ”δΊ†ι‚£ε₯θ―οΌŒδ½ εœ¨θ΄Ÿι‡ε‰θ‘Œζ˜―ε› δΈΊζœ‰δΊΊζ›Ώδ½ ε²ζœˆι™ε₯½γ€‚εœ¨θΏ™η§ζƒ…ε†΅δΈ‹οΌŒθΆŠζ₯θΆŠε€šηš„ζˆ˜ε‹ε†δΉŸεšζŒδΈδΈ‹εŽ»δΊ†οΌŒι€‰ζ‹©δΊ†η¦»εΌ€γ€‚ηœ‹εˆ°θΊ«θΎΉι‚£δΊ›δΌ˜η§€ηš„εŒδΊ‹δΈ€δΈͺδΈͺη¦»θŒοΌŒζˆ‘ηš„ε†…εΏƒεˆζ„ŸεΉεˆιšΎθΏ‡γ€‚εœ¨θΏ™η§δ½œζˆ˜δΈ€ζ ·ηš„ηŽ―ε’ƒδΈ‹οΌŒζˆ‘δ»¬ζ―”θ΅·εŒδΊ‹ζ₯θ―΄ζ›΄εƒζ˜―ζˆ˜ε‹γ€‚δ»–δ»¬εœ¨ζŠ€ζœ―δΈŠδΉŸζœ‰ζ— ζ•°ε€ΌεΎ—ζˆ‘ε­¦δΉ ηš„εœ°ζ–ΉοΌŒε ͺη§°θ‰―εΈˆγ€‚ηœ‹εˆ°δ»–δ»¬εŽ»δΊ†θ―Έε¦‚ε­—θŠ‚Seed,DeepseekοΌŒζœˆδΉ‹ζš—ι’οΌŒθ…Ύθ―ε’ŒεΏ«ζ‰‹η­‰η­‰εΎˆε€šε‡Ίθ‰²ηš„ε›’ι˜ŸοΌŒζˆ‘ζ‰“εΏƒηœΌι‡ŒδΈΊδ»–δ»¬ι«˜ε…΄ε’Œη₯η¦οΌŒθ„±η¦»δΊ†θΏ™δΈͺ辛苦却θ‚θ„ηš„εœ°ζ–Ήγ€‚ζˆ‘θ‡³δ»ŠθΏ˜ε―ΉδΈ€δ½η¦»θŒεŒδΊ‹ηš„θ―θ°εΏ†ηŠΉζ–°οΌŒtaθ―΄οΌšβ€œζ₯θΏ™ι‡Œζ˜―ζˆ‘ζŠ€ζœ―η”ŸζΆ―δΈ­ηš„θ€»θΎ±οΌŒεœ¨θΏ™ι‡Œε†ε‘†ζ―δΈ€ε€©ιƒ½ζ˜―ζ΅ͺθ΄Ήη”Ÿε‘½β€γ€‚θ―θ™½ιšΎε¬ε΄θ©ζˆ‘无言δ»₯ε―Ήγ€‚ζˆ‘ζ‹…εΏƒζˆ‘θ‡ͺε·±ζŠ€ζœ―ζ–Ήι’ηš„η§―η΄―δΈθΆ³οΌŒδ»₯εŠζ²‘ζ³•ι€‚εΊ”δΊ’θ”η½‘ε…¬εΈι«˜ζ·˜ζ±°ηš„ηŽ―ε’ƒοΌŒθ©ζˆ‘ε€šζ¬‘ζƒ³η¦»θŒηš„εΏƒε§‹η»ˆζ²‘ζœ‰θΏˆε‡ΊθΏ™δΈ€ζ­₯。 + +η›˜ε€ι™€δΊ†denseζ¨‘εž‹οΌŒεŽη»­δΉŸε―εŠ¨δΊ†moeηš„ζŽ’η΄’γ€‚δΈ€εΌ€ε§‹θ­η»ƒηš„ζ˜―δΈ€δΈͺ224Bηš„moeζ¨‘εž‹γ€‚θ€ŒδΈŽδΉ‹εΉ³θ‘Œηš„οΌŒε°ζ¨‘εž‹εžιͺŒε€δΉŸεΌ€ε―δΊ†η¬¬δΊŒζ¬‘δΈ»θ¦ηš„ε₯—ε£³θ‘ŒεŠ¨οΌˆζ¬‘θ¦ηš„ζ’ζ›²ε―θƒ½θΏ˜εŒ…ζ‹¬δΈ€δΊ›εˆ«ηš„ζ¨‘εž‹οΌŒζ―”ε¦‚mathζ¨‘εž‹οΌ‰οΌŒε³θΏ™ζ¬‘ζ΅δΌ η”šεΉΏηš„pangu pro moe 72B。这δΈͺζ¨‘εž‹ε†…ιƒ¨θ‡ͺη§°ζ˜―δ»Žε°ζ¨‘εž‹εžιͺŒε€ηš„7Bζ‰©ε’žδΈŠζ₯ηš„οΌˆε°±η—ε¦‚ζ­€οΌŒθΏ™δΉŸδΈŽζŠ€ζœ―ζŠ₯ε‘ŠδΈη¬¦οΌŒδ½•ε†΅ζ˜―ε₯—ε£³qwen 2.5ηš„14bη»­θ­οΌ‰γ€‚θΏ˜θ°εΎ—他们θ­δΊ†ζ²‘ε‡ ε€©οΌŒε†…ιƒ¨ηš„θ―„ζ΅‹ε°±η«‹εˆ»θΏ½δΈŠδΊ†ε½“ζ—Άηš„38B V3。AI系统εžιͺŒε€εΎˆε€šε…„εΌŸε› δΈΊιœ€θ¦ι€‚ι…ζ¨‘εž‹οΌŒιƒ½ηŸ₯ι“δ»–δ»¬ηš„ε₯—ε£³θ‘ŒεŠ¨οΌŒεͺζ˜―θΏ«δΊŽε„η§εŽŸε› οΌŒζ— ζ³•δΌΈεΌ ζ­£δΉ‰γ€‚εžι™…δΈŠοΌŒε―ΉδΊŽεŽη»­θ­δΊ†εΎˆδΉ…εΎˆδΉ…ηš„θΏ™δΈͺζ¨‘εž‹οΌŒHonestagiθƒ½ε€Ÿεˆ†ζžε‡ΊθΏ™δΈͺι‡ηΊ§ηš„η›ΈδΌΌζ€§ζˆ‘ε·²η»εΎˆθ―§εΌ‚δΊ†οΌŒε› δΈΊθΏ™δΈͺζ¨‘εž‹δΈΊδΊ†η»­θ­ζ΄—ε‚ζ•°οΌŒζ‰€δ»˜ε‡Ίηš„η—εŠ›η”šθ‡³ζ—©ε°±θΆ³ε€Ÿδ»Žε€΄θ­δΈ€δΈͺεŒζ‘£δ½ηš„ζ¨‘εž‹δΊ†γ€‚ε¬εŒδΊ‹θ―΄δ»–δ»¬δΈΊδΊ†ζ΄—ζŽ‰εƒι—ηš„ζ°΄ε°οΌŒι‡‡ε–δΊ†δΈε°‘εŠžζ³•οΌŒη”šθ‡³εŒ…ζ‹¬ζ•…ζ„θ­δΊ†θ„ζ•°ζγ€‚θΏ™δΉŸδΈΊε­¦ζœ―η•Œη ”η©Άζ¨‘εž‹θ‘€ηΌ˜ζδΎ›δΊ†δΈ€δΈͺ前所ζœͺζœ‰ηš„η‰ΉζŠζ¨‘θŒƒε§γ€‚δ»₯εŽζ–°ηš„θ‘€ηΌ˜ζ–Ήζ³•ζε‡Ίε―δ»₯ζ‹Ώε‡Ίζ₯ζΊœζΊœγ€‚ + +24εΉ΄εΊ•ε’Œ25年初,在Deepseek v3ε’Œr1ε‘εΈƒδΉ‹εŽοΌŒη”±δΊŽε…ΆζƒŠθ‰³ηš„ζŠ€ζœ―ζ°΄εΉ³οΌŒε›’ι˜Ÿε—εˆ°δΊ†ε·¨ε€§ηš„ε†²ε‡»οΌŒδΉŸε—εˆ°δΊ†ζ›΄ε€§ηš„θ΄¨η–‘γ€‚δΊŽζ˜―δΈΊδΊ†η΄§θ·Ÿζ½ζ΅οΌŒη›˜ε€ζ¨‘δ»ΏDeepseekηš„ζ¨‘εž‹ε°Ίε―ΈοΌŒεΌ€ε―δΊ†718B moeηš„θ­η»ƒγ€‚θΏ™δΈͺζ—Άε€™οΌŒε°ζ¨‘εž‹εžιͺŒε€ε†ζ¬‘出手了。他们选择了ε₯—ε£³Deepseekv3η»­θ­γ€‚δ»–δ»¬ι€šθΏ‡ε†»δ½DeepseekεŠ θ½½ηš„ε‚ζ•°οΌŒθΏ›θ‘Œθ­η»ƒγ€‚θΏžδ»»εŠ‘加载ckptηš„η›ε½•ιƒ½ζ˜―deepseekv3οΌŒζ”Ήιƒ½δΈζ”ΉοΌŒδ½•ε…Άεš£εΌ οΌŸδΈŽδΉ‹η›ΈεοΌŒδΈ€δΊ›ζœ‰ηœŸζ­£ζŠ€ζœ―δΏ‘δ»°ηš„εŒδΊ‹οΌŒεœ¨δ»Žε€΄θ­η»ƒε¦δΈ€δΈͺ718Bηš„moeγ€‚δ½†ε…ΆδΈ­ε‡ΊηŽ°δΊ†ε„η§ε„ζ ·ηš„ι—ι’˜γ€‚δ½†ζ˜―εΎˆζ˜Ύη„ΆοΌŒθΏ™δΈͺζ¨‘εž‹ζ€ŽδΉˆε―θƒ½ζ―”η›΄ζŽ₯ε₯—ε£³ηš„ε₯½ε‘’οΌŸε¦‚ζžœδΈζ˜―ε›’ι˜ŸleaderεšζŒοΌŒζ—©ε°±θ’«ε«εœδΊ†γ€‚ + +εŽδΈΊηš„ζ΅η¨‹η‘η†δΉ‹ηΉι‡οΌŒδΈ₯ι‡ζ‹–η΄―δΊ†ε€§ζ¨‘εž‹ηš„η ”ε‘θŠ‚ε₯οΌŒδΎ‹ε¦‚η‰ˆζœ¬η‘η†οΌŒζ¨‘εž‹θ‘€ηΌ˜οΌŒε„η§ζ΅η¨‹εŒ–οΌŒε„η§ε―θΏ½ζΊ―γ€‚θ½εˆΊηš„ζ˜―οΌŒε°ζ¨‘εž‹εžιͺŒε€ηš„ζ¨‘εž‹δΌΌδΉŽδ»Žζ₯δΈε—θΏ™δΊ›ζ΅η¨‹ηš„ηΊ¦ζŸοΌŒζƒ³ε₯—ε£³ε°±ε₯—ε£³οΌŒζƒ³η»­θ­ε°±η»­θ­οΌŒη—εŠ›ζΊζΊδΈζ–­ηš„δΌΈζ‰‹ζ‹Ώθ΅°γ€‚θΏ™η§εΌΊηƒˆεˆ°θΏ‘δΉŽι­”εΉ»ηš„ε―Ήζ―”οΌŒθ―΄ζ˜ŽδΊ†ε½“ε‰ζ΅η¨‹η‘η†ηš„ζƒ…ε†΅οΌšεͺθΈε·žε˜ζ”Ύη«οΌŒδΈθΈη™Ύε§“η‚Ήη―γ€‚δ½•ε…Άε―η¬‘οΌŸδ½•ε…Άε―ζ‚²οΌŸδ½•ε…Άε―ζΆοΌŸδ½•ε…Άε―θ€»οΌ + +HonestAGIηš„δΊ‹ζƒ…ε‡Ίζ₯εŽοΌŒε†…ιƒ¨θ©ε€§εΆδΈεœηš„η ”θ¨εˆ†ζžοΌŒε¦‚δ½•ε…¬ε…³ε’Œβ€œε›žεΊ”β€γ€‚θ―šη„ΆοΌŒθΏ™δΈͺεŽŸζ–‡ηš„εˆ†ζžδΉŸθΈδΈε€Ÿζœ‰εŠ›οΌŒη»™δΊ†ηŽ‹δΊ‘ιΉ€δΈŽε°ζ¨‘εž‹εžιͺŒε€δ»–δ»¬η‹‘θΎ©ε’Œι’ ε€’ι»‘η™½ηš„ζœΊδΌšγ€‚δΈΊζ­€οΌŒθΏ™δΈ€ε€©ζˆ‘ε†…εΏƒζ„Ÿεˆ°δ½œε‘•οΌŒζ—Άζ—Άζ€€η–‘θ‡ͺε·±ηš„δΊΊη”Ÿζ„δΉ‰δ»₯εŠθ‹ε€©ζ— ηœΌγ€‚ζˆ‘δΈε₯‰ι™ͺδΊ†οΌŒζˆ‘θ¦η¦»θŒδΊ†οΌŒεŒζ—Άζˆ‘δΉŸεœ¨η”³θ―·δ»Žη›˜ε€ιƒ¨εˆ†ζŠ€ζœ―ζŠ₯ε‘Šηš„δ½œθ€…εε•δΈ­η§»ι™€γ€‚ζ›Ύη»εœ¨θΏ™δΊ›ζŠ€ζœ―ζŠ₯ε‘ŠδΈŠη½²εζ˜―ζˆ‘δΈ€η”Ÿιƒ½ζ— ζ³•ζŠΉι™€ηš„ζ±‘η‚Ήγ€‚ε½“ζ—Άζˆ‘ζ²‘ζƒ³εˆ°οΌŒδ»–δ»¬η«Ÿη„ΆηŒ–η‹‚εˆ°ζ•’εΌ€ζΊγ€‚ζˆ‘ζ²‘ζƒ³εˆ°οΌŒδ»–δ»¬ζ•’ε¦‚ζ­€ζ„šεΌ„δΈ–δΊΊοΌŒε€§θ‚†ε£ε‘γ€‚ε½“ζ—ΆοΌŒζˆ‘δΉŸθΈζ˜―ε­˜δΊ†δΎ₯εΉΈεΏƒη†οΌŒζ²‘ζœ‰ζ‹’η»η½²εγ€‚ζˆ‘η›ΈδΏ‘εΎˆε€šζ‰ŽεžεšδΊ‹ηš„ζˆ˜ε‹οΌŒδΉŸεͺζ˜―θ’«θΏ«δΈŠδΊ†θ΄ΌθˆΉοΌŒζˆ–θ€…δΈηŸ₯ζƒ…γ€‚δ½†θΏ™δ»ΆδΊ‹ε·²η»ζ— ζ³•ζŒ½ε›žοΌŒζˆ‘εΈŒζœ›ζˆ‘ηš„δ½™η”Ÿθƒ½ε€ŸεšζŒζ‰ŽεžεšηœŸζ­£ζœ‰ζ„δΉ‰ηš„δΊ‹οΌŒδΈΊζˆ‘ε½“ζ—Άηš„θ½―εΌ±ε’ŒδΈεšεšθ΅Žη½ͺ。 + +ζ·±ε€œε†™εˆ°θΏ™ι‡ŒοΌŒζˆ‘ε·²η»ζ³ͺζ΅ζ»‘ι’οΌŒζ³£δΈζˆε£°γ€‚θΏ˜θ°εΎ—δΈ€δΊ›ε‡Ίθ‰²ηš„εŒδΊ‹η¦»θŒζ—ΆοΌŒζˆ‘θ‹¦η¬‘ι—他们要不要发δΈͺι•Ώι•Ώηš„εΏƒε£°ζƒ―δΎ‹εΈ–οΌŒζ­ιœ²δΈ€δΈ‹ηŽ°ηŠΆγ€‚ε―Ήζ–Ήθ―΄οΌšδΈδΊ†οΌŒζ΅ͺθ΄Ήζ—Άι—΄οΌŒθ€ŒδΈ”ζˆ‘δΉŸζ€•ζ­ιœ²ε‡Ίζ₯δ½ δ»¬θΏ‡ηš„ζ›΄η³Ÿγ€‚ζˆ‘ε½“ζ—ΆδΈ€δΈ‹ι»―η„Άη₯žδΌ€οΌŒε› δΈΊζ›Ύη»ε…±εŒδΈΊδΊ†η†ζƒ³ε₯‹ζ–—θΏ‡ηš„ζˆ˜ε‹ε·²η»ε½»εΊ•ε―ΉεŽδΈΊε½»εΊ•η°εΏƒδΊ†γ€‚ε½“ζ—Άε€§εΆθ°ƒδΎƒοΌŒζˆ‘δ»¬η”¨η€ε½“εΉ΄ε…±δΊ§ε…šηš„ε°η±³εŠ ζ­₯ζžͺοΌŒη»„η»‡ε΄ζœ‰η€ε ͺζ―”ε½“εΉ΄ε›½ζ°‘ε…šηš„δ½œι£Žγ€‚ + +ζ›Ύε‡ δ½•ζ—ΆοΌŒζˆ‘δΈΊζˆ‘δ»¬η”¨η€ε°η±³εŠ ζ­₯ζžͺ打θ΄₯ζ΄‹ζžͺζ΄‹η‚θ€Œθ‡ͺθ±ͺ。 + +ηŽ°εœ¨οΌŒζˆ‘η΄―δΊ†οΌŒζˆ‘ζƒ³ζŠ•ι™γ€‚ + +ε…Άεžζ—Άθ‡³δ»Šζ—₯οΌŒζˆ‘θΏ˜ζ˜―ηœŸεΏƒεΈŒζœ›εŽδΈΊθƒ½θ€ηœŸεΈε–ζ•™θ­οΌŒθƒ½εšε₯½η›˜ε€οΌŒζŠŠη›˜ε€εšεˆ°δΈ–η•ŒδΈ€ζ΅οΌŒζŠŠζ˜‡θ…Ύε˜ζˆθ‹±δΌŸθΎΎηš„ζ°΄εΉ³γ€‚ε†…ιƒ¨ηš„εŠ£εΈι©±ι€θ‰―εΈοΌŒδ½ΏεΎ—θ―ΊδΊšδΉƒθ‡³εŽδΈΊεœ¨ηŸ­ζ—Άι—΄ε†…ζ€₯ε‰§ζ΅ε€±δΊ†ε€§ι‡ε‡Ίθ‰²ηš„ε€§ζ¨‘εž‹δΊΊζ‰γ€‚η›ΈδΏ‘δ»–δ»¬δΉŸζ­£εœ¨ε¦‚Deepseek等各δΈͺε›’ι˜Ÿι—ͺθ€€η€οΌŒζ–½ε±•η€δ»–δ»¬ηš„ζŠ±θ΄Ÿζ‰εŽοΌŒδΈΊδΈ­ηΎŽεœ¨AIηš„ζΏ€ηƒˆη«žθ΅›δΈ­ε₯‰ηŒεŠ›ι‡γ€‚ζˆ‘ζ—ΆεΈΈζ„ŸεΉοΌŒεŽδΈΊδΈζ˜―ζ²‘ζœ‰δΊΊζ‰οΌŒθ€Œζ˜―ζ Ήζœ¬δΈηŸ₯ι“ζ€ŽδΉˆη•™δ½δΊΊζ‰γ€‚ε¦‚ζžœη»™θΏ™δΊ›δΊΊεˆι€‚ηš„ηŽ―ε’ƒοΌŒεˆι€‚ηš„θ΅„ζΊοΌŒζ›΄ε°‘ηš„ζž·ι”οΌŒζ›΄ε°‘ηš„ζ”Ώζ²»ζ–—δΊ‰οΌŒη›˜ε€δ½•ζ„δΈζˆοΌŸ + +ζœ€εŽοΌšζˆ‘δ»₯η”Ÿε‘½οΌŒδΊΊζ Όε’Œθ£θͺ‰ε‘θͺ“οΌŒζˆ‘ε†™ηš„δ»₯δΈŠζ‰€ζœ‰ε†…εΉε‡δΈΊηœŸεžοΌˆθ‡³ε°‘εœ¨ζˆ‘ζœ‰ι™ηš„θ€ηŸ₯θŒƒε›΄ε†…οΌ‰γ€‚ζˆ‘ζ²‘ζœ‰ι‚£δΉˆι«˜ηš„ζŠ€ζœ―ζ°΄εΉ³δ»₯εŠζœΊδΌšεŽ»εšθ―¦ε°½ζ‰Žεžηš„εˆ†ζžοΌŒδΉŸδΈζ•’η›΄ζŽ₯用内部θ°ε½•δΈΎθ―οΌŒζ€•ε› δΈΊδΏ‘息ε‰ε…¨ζŠ“εˆ°γ€‚δ½†ζ˜―ζˆ‘η›ΈδΏ‘ζˆ‘εΎˆε€šζ›Ύη»ηš„ζˆ˜ε‹οΌŒδΌšδΈΊζˆ‘δ½œθ―γ€‚εœ¨εŽδΈΊε†…ιƒ¨ηš„ε…„εΌŸοΌŒεŒ…ζ‹¬ζˆ‘δ»¬ζ›Ύη»ζœεŠ‘θΏ‡ηš„δΊ§ε“ηΊΏε…„εΌŸδ»¬οΌŒη›ΈδΏ‘ζœ¬ζ–‡ηš„ζ— ζ•°η»†θŠ‚θƒ½ε’Œδ½ δ»¬ηš„ε°θ±‘ε―Ήη…§οΌŒε°θ―ζˆ‘ηš„θ―΄ζ³•γ€‚δ½ δ»¬ε―θƒ½δΉŸζ›Ύη»θ’«θ’™ιͺ—οΌŒδ½†θΏ™δΊ›ζ‹ι…·ηš„ηœŸη›ΈδΈδΌšθ’«ε°˜ε°γ€‚ζˆ‘δ»¬ε₯‹ζˆ˜θΏ‡ηš„η—•θΏΉοΌŒδΉŸδΈεΊ”θ―₯θ’«ζ‰­ζ›²ε’ŒεŸ‹θ‘¬γ€‚ + +ε†™δΊ†θΏ™δΉˆε€šοΌŒζŸδΊ›δΊΊθ‚―εšζƒ³ζŠŠζˆ‘ζ‰Ύε‡Ίζ₯οΌŒζŠΉζ€ζŽ‰γ€‚ε…¬εΈζžδΈε₯½δΉŸζƒ³θ©ζˆ‘ε™€ε£°δΉƒθ‡³θΏ½θ΄£γ€‚ε¦‚ζžœηœŸηš„θΏ™ζ ·οΌŒζˆ‘οΌŒδΉƒθ‡³ζˆ‘ηš„εΆδΊΊηš„δΊΊθΊ«δΉƒθ‡³η”Ÿε‘½ε‰ε…¨ε―θƒ½ιƒ½δΌšε—εˆ°ε¨θƒγ€‚δΈΊδΊ†θ‡ͺζˆ‘δΏζŠ€οΌŒζˆ‘θΏ‘ζœŸζ―ε€©δΌšθ·Ÿε€§εΆζŠ₯εΉ³ε‰γ€‚ + +ε¦‚ζžœζˆ‘ζΆˆε€±δΊ†οΌŒε°±ε½“ζ˜―ζˆ‘δΈΊδΊ†ηœŸη†ε’Œη†ζƒ³οΌŒδΈΊδΊ†εŽδΈΊδΉƒθ‡³δΈ­ε›½θƒ½ε€Ÿζ›΄ε₯½εœ°ε‘展η—εŠ›ε’ŒAIθ€Œη‰Ίη‰²δΊ†ε§οΌŒζˆ‘ζ„ΏεŸ‹θ‘¬δΊŽι‚£η‰‡ζ›Ύη»ε₯‹ζ–—θΏ‡ηš„εœ°ζ–Ήγ€‚ + +θ―ΊδΊšοΌŒε†θ§ + +2025εΉ΄7月6ζ—₯ε‡Œζ™¨ ε†™δΊŽζ·±εœ³ + +--- + +各位ε₯½οΌŒ + +ζ„Ÿθ°’ε€§εΆηš„ε…³εΏƒδΈŽη₯η¦γ€‚ζˆ‘η›ε‰ζš‚ζ—Άε‰ε…¨οΌŒδ½†ε…¬εΈεΊ”θ―₯εœ¨θΏ›θ‘ŒζŽ’ζŸ₯δΈŽζŸδΊ›εε•ζ”Άι›†οΌŒεŽη»­ζƒ…ε†΅ζœͺηŸ₯。 + +ζˆ‘θ‘₯ε……δΈ€δΊ›η»†θŠ‚οΌŒδ»₯ε…ζŸδΊ›δΊΊη»§η»­ι’ ε€’ι»‘η™½γ€‚ + +ε…³δΊŽ135B V2οΌŒε°ζ¨‘εž‹εžιͺŒε€εœ¨θΏ…ι€Ÿεœ°εŒζˆε₯—ε£³εΉΆζ‹ΏεŒζ‰€ζœ‰ε₯—ε£³εΈ¦ζ₯ηš„ε₯½ε€„εŽοΌˆζ―”ε¦‚δ»»εŠ‘δ»€θ‘¨ε½°ε’ŒεŠζ—ΆζΏ€εŠ±οΌ‰οΌŒε› δΈΊδΈζƒ³η»§η»­ζ”―ζ’‘δΈ‹ζΈΈεΊ”η”¨ε’Œζ¨‘εž‹θΏ­δ»£οΌŒεˆζŠŠθΏ™δΈͺηƒ«ζ‰‹ε±±θŠ‹η”©η»™δΊ†ε››ηΊ΅γ€‚η‘εžζŠ€ι«˜δΈ€η­ΉοΌŒη›΄ζŽ₯ζŠŠε››ηΊ΅ηš„ε…„εΌŸδ»¬ζ‹‰δΈ‹ζ°΄γ€‚εŒδΊ‹ζδΎ›θΏ‡εŽ»δΈ€δΈͺθ€ζ—§ηš„ζ¨‘εž‹οΌŒζœ€η»ˆζ‹Ώε›žδΊ†δΈ€δΈͺ当既一δΈͺι­”ζ”Ήηš„ε…ˆθΏ›ηš„εƒι—γ€‚εšε€§ζ¨‘εž‹ηš„δΊΊοΌŒθ‡ͺε·±εšηš„ζ¨‘εž‹ε°±εƒθ‡ͺε·±ε­©ε­δΈ€ζ ·η†Ÿζ‚‰οΌŒδΈθ¦ζŠŠεˆ«δΊΊιƒ½ε½“ε‚»ε­γ€‚ε°±εƒθ‡ͺεΆε„Ώε­ε‡Ίι—¨δΈ€θΆŸοΌŒε›žζ₯δΈͺ别人εΆε­©ε­γ€‚ + +η›˜ε€reportηš„η½²εζ˜―δΈη¬¦εˆε­¦ζœ―θ§„θŒƒηš„γ€‚δΎ‹ε¦‚οΌŒ135B V3ζœ‰δΈε°‘ζœ‰ζŠ€ζœ―θ΄‘ηŒηš„δΊΊοΌŒε› δΈΊδ½œθ€…ει’ζ•°ι‡ι™εˆΆοΌŒεŠ³εŠ¨ζˆζžœζ²‘ζœ‰εΎ—εˆ°εΊ”ζœ‰ηš„ε›žζŠ₯οΌŒε›’ι˜Ÿε†…ζ›Ύη»ζœ‰δΈε°ηš„ζ„θ§γ€‚θΏ™δΈͺζ¨‘εž‹ε½“ζ—Άζ˜―ε€§εΆζ™Ίζ…§ε’Œζ±—ζ°΄ηš„η»“ζ™ΆοΌŒη”šθ‡³ζ˜―ε›’ι˜Ÿε½“ζ—Άηš„η²Ύη₯žζ”―ζŸ±οΌŒζ”―ζ’‘η€δΈε°‘ε…„εΌŸδ»¬η»§η»­η•™εœ¨θ―ΊδΊšγ€‚ζ‰€θ°“ηš„ει’ι™εˆΆοΌŒδ»₯εŠζŒ‚εδΊ†δΈ€δΊ›ζ―«ζ— ζŠ€ζœ―θ΄‘ηŒηš„δΊΊοΌˆε¦‚δΈ€δΊ›ε°ζ¨‘εž‹εžιͺŒε€ηš„δΊΊοΌ‰οΌŒθ©ε…„εΌŸδ»¬δ½•ε…ΆεΏƒε―’γ€‚ + +--- + +ζš‚ζ—ΆεΉ³ε‰γ€‚ε¦ε€–οΌŒζ”―ζŒζˆ‘ε‹‡δΊŽθ―΄ε‡ΊηœŸη›Έηš„ζˆ˜ε‹δ»¬ https://github.com/HW-whistleblower/True-Story-of-Pangu/issues/317 diff --git a/demo.ipynb b/demo.ipynb new file mode 100644 index 0000000..e91ec01 --- /dev/null +++ b/demo.ipynb @@ -0,0 +1,138 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quick Start \n", + "\n", + "**Home GitHub Repository:** [LEANN on GitHub](https://github.com/yichuan-w/LEANN)\n", + "\n", + "**Important for Colab users:** Set your runtime type to T4 GPU for optimal performance. Go to Runtime β†’ Change runtime type β†’ Hardware accelerator β†’ T4 GPU." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# install this if you are using colab\n", + "! uv pip install leann-core leann-backend-hnsw --no-deps\n", + "! uv pip install leann --no-deps\n", + "# For Colab environment, we need to set some environment variables\n", + "import os\n", + "\n", + "os.environ[\"LEANN_LOG_LEVEL\"] = \"INFO\" # Enable more detailed logging" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "INDEX_DIR = Path(\"./\").resolve()\n", + "INDEX_PATH = str(INDEX_DIR / \"demo.leann\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build the index" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from leann.api import LeannBuilder\n", + "\n", + "builder = LeannBuilder(backend_name=\"hnsw\")\n", + "builder.add_text(\"C# is a powerful programming language and it is good at game development\")\n", + "builder.add_text(\n", + " \"Python is a powerful programming language and it is good at machine learning tasks\"\n", + ")\n", + "builder.add_text(\"Machine learning transforms industries\")\n", + "builder.add_text(\"Neural networks process complex data\")\n", + "builder.add_text(\"Leann is a great storage saving engine for RAG on your MacBook\")\n", + "builder.build_index(INDEX_PATH)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Search with real-time embeddings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from leann.api import LeannSearcher\n", + "\n", + "searcher = LeannSearcher(INDEX_PATH)\n", + "results = searcher.search(\"programming languages\", top_k=2)\n", + "results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Chat with LEANN using retrieved results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from leann.api import LeannChat\n", + "\n", + "llm_config = {\n", + " \"type\": \"hf\",\n", + " \"model\": \"Qwen/Qwen3-0.6B\",\n", + "}\n", + "\n", + "chat = LeannChat(index_path=INDEX_PATH, llm_config=llm_config)\n", + "response = chat.ask(\n", + " \"Compare the two retrieved programming languages and tell me their advantages.\",\n", + " top_k=2,\n", + " llm_kwargs={\"max_tokens\": 128},\n", + ")\n", + "response" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..8ca4af6 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,23 @@ +ARG PYTHON_VERSION=3.11 +FROM python:${PYTHON_VERSION}-slim + +ARG LEANN_VERSION=0.3.6 + +ENV PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_ROOT_USER_ACTION=ignore + +# Keep runtime image minimal while ensuring common C++ runtime libs are present. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libgomp1 \ + libstdc++6 \ + && rm -rf /var/lib/apt/lists/* + +RUN python -m pip install --upgrade pip \ + && pip install --prefer-binary "leann==${LEANN_VERSION}" \ + && python -c "import leann; import leann_backend_hnsw; import leann_backend_diskann" + +WORKDIR /workspace + +CMD ["python", "-c", "import leann; print('LEANN installed and importable.')"] diff --git a/docker/Dockerfile.cpu b/docker/Dockerfile.cpu new file mode 100644 index 0000000..cc0873c --- /dev/null +++ b/docker/Dockerfile.cpu @@ -0,0 +1,25 @@ +ARG PYTHON_VERSION=3.11 +FROM python:${PYTHON_VERSION}-slim + +ARG LEANN_VERSION=0.3.6 + +ENV PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_ROOT_USER_ACTION=ignore \ + PIP_INDEX_URL=https://download.pytorch.org/whl/cpu \ + PIP_EXTRA_INDEX_URL=https://pypi.org/simple + +# Keep runtime image minimal while ensuring common C++ runtime libs are present. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libgomp1 \ + libstdc++6 \ + && rm -rf /var/lib/apt/lists/* + +RUN python -m pip install --upgrade pip \ + && pip install --prefer-binary "leann==${LEANN_VERSION}" \ + && python -c "import leann; import leann_backend_hnsw; import leann_backend_diskann" + +WORKDIR /workspace + +CMD ["python", "-c", "import leann; print('LEANN installed and importable (CPU image).')"] diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100644 index 0000000..f05b314 --- /dev/null +++ b/docker/Dockerfile.dev @@ -0,0 +1,38 @@ +ARG PYTHON_VERSION=3.11 +FROM python:${PYTHON_VERSION}-slim + +ENV DEBIAN_FRONTEND=noninteractive \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_ROOT_USER_ACTION=ignore + +# Build + test toolchain for local development in container. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + git \ + swig \ + libomp-dev \ + libboost-all-dev \ + protobuf-compiler \ + libzmq3-dev \ + pkg-config \ + libabsl-dev \ + libaio-dev \ + libprotobuf-dev \ + libopenblas-dev \ + liblapack-dev \ + liblapacke-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN python -m pip install --upgrade pip \ + && pip install uv + +WORKDIR /workspace +COPY . /workspace + +# Keep dependency install explicit; default includes lint + test groups. +RUN uv sync --group lint --group test + +CMD ["/bin/bash"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..eb36cc4 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,50 @@ +# Docker Setup + +This folder provides reference Docker images for LEANN. + +## Build (default) + +Default image (no forced CPU index): + +```bash +docker build -f docker/Dockerfile docker -t leann:latest +``` + +## Build (CPU) + +CPU-optimized image (forces PyTorch CPU index): + +```bash +docker build \ + --build-arg PYTHON_VERSION=3.12 \ + --build-arg LEANN_VERSION=0.3.6 \ + -f docker/Dockerfile.cpu docker -t leann:cpu +``` + +## Build (development) + +Development image (source tree + build/test toolchain + `uv sync`): + +```bash +docker build -f docker/Dockerfile.dev . -t leann:dev +``` + +## Run + +```bash +docker run --rm leann:latest +docker run --rm leann:cpu +docker run --rm -it leann:dev +``` + +Expected output: + +```text +LEANN installed and importable. +``` + +## Notes + +- Both images keep LEANN package semantics unchanged (`pip install leann` with both backends). +- `Dockerfile.cpu` uses the PyTorch CPU index to avoid downloading CUDA wheels. +- `Dockerfile.dev` is for local development, not production deployment. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..2ce4387 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog + +Append-only log of major changes to LEANN (new features, breaking changes, important +fixes). Newest entries at the bottom. + +## 2026-03-05: IVF backend incremental update support + +- Added `leann-backend-ivf` with FAISS IndexIVFFlat + DirectMap.Hashtable. +- IVF supports in-place `add_vectors` and `remove_ids` without full rebuild. +- `leann build` is now idempotent: re-running on an existing index does incremental update (add new, remove deleted, re-index modified files). +- Fixed incremental build chunking inconsistency and shared metadata dict bug. +- Fixed IVF incremental update duplicate chunks from stale `passages.jsonl`. + +## 2026-03-05: MCP server v2 β€” build, status, and structured search + +- Added `leann_build` MCP tool: build or incrementally update indexes directly from Claude Code. +- Added `leann_status` MCP tool: inspect index details (backend, embedding model, chunk/file count, size). +- `leann_search` now uses `--json` output with file paths always included, formatted as markdown code blocks. +- Fixed `float32` JSON serialization bug in `leann search --json`. +- Cleaned up MCP tool descriptions (concise, no emoji). + +## 2026-03-05: Documentation β€” roadmap, vision, and dev guidelines + +- Rewrote `docs/roadmap.md` with current P0/P1 priorities from GitHub issue #237. +- Added `docs/ultimate_goal.md` β€” long-term vision (personal data platform, best code retrieval MCP, multimodal, local-first). +- Added self-contained documentation principle and dev doc maintenance rules to `CLAUDE.md`. + +## 2026-06-02: GPU FlashLib IVF backend (`flashlib_ivf`) + +- Add `leann-backend-flashlib-ivf`, a GPU IVF-Flat (inverted file) approximate-NN + backend built on FlashLib (`flash_ivf_flat`, Triton/CuteDSL) β€” the GPU counterpart + of the FAISS `ivf` backend. Registered as backend name `flashlib_ivf`; install via + `uv sync --extra flashlib-ivf` or `pip install leann-backend-flashlib-ivf`. Shares + the `nlist`/`nprobe` recall knobs with the `ivf` backend, so the two are drop-in + comparable. Requires a CUDA GPU at build (k-means) and search. +- Add `benchmarks/flashlib_ivf_vs_faiss_ivf.py`: head-to-head `flashlib_ivf` (GPU) vs + `ivf` (FAISS, CPU) at matched `nlist` across an `nprobe` sweep (build time, + single-query latency, batched throughput, recall@k vs exact ground truth). On an + NVIDIA H200 at 1M x 768 vectors (nlist=4096, 8 CPU threads): ~13x faster build and, + at nprobe=32, ~6.5x lower single-query latency / ~75x higher batched throughput at + comparable recall (GPU latency stays ~flat while CPU grows linearly with nprobe). +- Docs: `docs/flashlib_backend_guide.md` gains a `flashlib_ivf` section. diff --git a/docs/COLQWEN_GUIDE.md b/docs/COLQWEN_GUIDE.md new file mode 100644 index 0000000..42772f6 --- /dev/null +++ b/docs/COLQWEN_GUIDE.md @@ -0,0 +1,200 @@ +# ColQwen Integration Guide + +Easy-to-use multimodal PDF retrieval with ColQwen2/ColPali models. + +## Quick Start + +> **🍎 Mac Users**: ColQwen is optimized for Apple Silicon with MPS acceleration for faster inference! + +### 1. Install Dependencies +```bash +uv pip install colpali_engine pdf2image pillow matplotlib qwen_vl_utils einops seaborn +brew install poppler # macOS only, for PDF processing +``` + +### 2. Basic Usage +```bash +# Build index from PDFs +python -m apps.colqwen_rag build --pdfs ./my_papers/ --index research_papers + +# Search with text queries +python -m apps.colqwen_rag search research_papers "How does attention mechanism work?" + +# Interactive Q&A +python -m apps.colqwen_rag ask research_papers --interactive +``` + +## Commands + +### Build Index +```bash +python -m apps.colqwen_rag build \ + --pdfs ./pdf_directory/ \ + --index my_index \ + --model colqwen2 \ + --pages-dir ./page_images/ # Optional: save page images +``` + +**Options:** +- `--pdfs`: Directory containing PDF files (or single PDF path) +- `--index`: Name for the index (required) +- `--model`: `colqwen2` (default) or `colpali` +- `--pages-dir`: Directory to save page images (optional) + +### Search Index +```bash +python -m apps.colqwen_rag search my_index "your question here" --top-k 5 +``` + +**Options:** +- `--top-k`: Number of results to return (default: 5) +- `--model`: Model used for search (should match build model) + +### Interactive Q&A +```bash +python -m apps.colqwen_rag ask my_index --interactive +``` + +**Commands in interactive mode:** +- Type your questions naturally +- `help`: Show available commands +- `quit`/`exit`/`q`: Exit interactive mode + +## πŸ§ͺ Test & Reproduce Results + +Run the reproduction test for issue #119: +```bash +python test_colqwen_reproduction.py +``` + +This will: +1. βœ… Check dependencies +2. πŸ“₯ Download sample PDF (Attention Is All You Need paper) +3. πŸ—οΈ Build test index +4. πŸ” Run sample queries +5. πŸ“Š Show how to generate similarity maps + +## 🎨 Advanced: Similarity Maps + +For visual similarity analysis, use the existing advanced script: +```bash +cd apps/multimodal/vision-based-pdf-multi-vector/ +python multi-vector-leann-similarity-map.py +``` + +Edit the script to customize: +- `QUERY`: Your question +- `MODEL`: "colqwen2" or "colpali" +- `USE_HF_DATASET`: Use HuggingFace dataset or local PDFs +- `SIMILARITY_MAP`: Generate heatmaps +- `ANSWER`: Enable Qwen-VL answer generation + +## πŸ”§ How It Works + +### ColQwen2 vs ColPali +- **ColQwen2** (`vidore/colqwen2-v1.0`): Latest vision-language model +- **ColPali** (`vidore/colpali-v1.2`): Proven multimodal retriever + +### Architecture +1. **PDF β†’ Images**: Convert PDF pages to images (150 DPI) +2. **Vision Encoding**: Process images with ColQwen2/ColPali +3. **Multi-Vector Index**: Build LEANN HNSW index with multiple embeddings per page +4. **Query Processing**: Encode text queries with same model +5. **Similarity Search**: Find most relevant pages/regions +6. **Visual Maps**: Generate attention heatmaps (optional) + +### Device Support +- **CUDA**: Best performance with GPU acceleration +- **MPS**: Apple Silicon Mac support +- **CPU**: Fallback for any system (slower) + +Auto-detection: CUDA > MPS > CPU + +## πŸ“Š Performance Tips + +### For Best Performance: +```bash +# Use ColQwen2 for latest features +--model colqwen2 + +# Save page images for reuse +--pages-dir ./cached_pages/ + +# Adjust batch size based on GPU memory +# (automatically handled) +``` + +### For Large Document Sets: +- Process PDFs in batches +- Use SSD storage for index files +- Consider using CUDA if available + +## πŸ”— Related Resources + +- **Fast-PLAID**: https://github.com/lightonai/fast-plaid +- **Pylate**: https://github.com/lightonai/pylate +- **ColBERT**: https://github.com/stanford-futuredata/ColBERT +- **ColPali Paper**: Vision-Language Models for Document Retrieval +- **Issue #119**: https://github.com/yichuan-w/LEANN/issues/119 + +## πŸ› Troubleshooting + +### PDF Conversion Issues (macOS) +```bash +# Install poppler +brew install poppler +which pdfinfo && pdfinfo -v +``` + +### Memory Issues +- Reduce batch size (automatically handled) +- Use CPU instead of GPU: `export CUDA_VISIBLE_DEVICES=""` +- Process fewer PDFs at once + +### Model Download Issues +- Ensure internet connection for first run +- Models are cached after first download +- Use HuggingFace mirrors if needed + +### Import Errors +```bash +# Ensure all dependencies installed +uv pip install colpali_engine pdf2image pillow matplotlib qwen_vl_utils einops seaborn + +# Check PyTorch installation +python -c "import torch; print(torch.__version__)" +``` + +## πŸ’‘ Examples + +### Research Paper Analysis +```bash +# Index your research papers +python -m apps.colqwen_rag build --pdfs ~/Papers/AI/ --index ai_papers + +# Ask research questions +python -m apps.colqwen_rag search ai_papers "What are the limitations of transformer models?" +python -m apps.colqwen_rag search ai_papers "How does BERT compare to GPT?" +``` + +### Document Q&A +```bash +# Index business documents +python -m apps.colqwen_rag build --pdfs ~/Documents/Reports/ --index reports + +# Interactive analysis +python -m apps.colqwen_rag ask reports --interactive +``` + +### Visual Analysis +```bash +# Generate similarity maps for specific queries +cd apps/multimodal/vision-based-pdf-multi-vector/ +# Edit multi-vector-leann-similarity-map.py with your query +python multi-vector-leann-similarity-map.py +# Check ./figures/ for generated heatmaps +``` + +--- + +**🎯 This integration makes ColQwen as easy to use as other LEANN features while maintaining the full power of multimodal document understanding!** diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..7fd0da5 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,224 @@ +# 🀝 Contributing + +We welcome contributions! Leann is built by the community, for the community. + +## Ways to Contribute + +- πŸ› **Bug Reports**: Found an issue? Let us know! +- πŸ’‘ **Feature Requests**: Have an idea? We'd love to hear it! +- πŸ”§ **Code Contributions**: PRs welcome for all skill levels +- πŸ“– **Documentation**: Help make Leann more accessible +- πŸ§ͺ **Benchmarks**: Share your performance results + +## πŸš€ Development Setup + +### Prerequisites + +1. **Install uv** (fast Python package installer): + ```bash + curl -LsSf https://astral.sh/uv/install.sh | sh + ``` + +2. **Clone the repository**: + ```bash + git clone https://github.com/yichuan-w/LEANN.git leann + git submodule update --init --recursive + cd leann + ``` + +3. **Install system dependencies**: + + **macOS:** + ```bash + brew install llvm libomp boost protobuf zeromq pkgconf + ``` + + **Ubuntu/Debian:** + ```bash + sudo apt-get install libomp-dev libboost-all-dev protobuf-compiler \ + libabsl-dev libmkl-full-dev libaio-dev libzmq3-dev + ``` + +4. **Build from source**: + ```bash + # macOS + CC=$(brew --prefix llvm)/bin/clang CXX=$(brew --prefix llvm)/bin/clang++ uv sync + + # Ubuntu/Debian + uv sync + ``` + +## πŸ”¨ Pre-commit Hooks + +We use pre-commit hooks to ensure code quality and consistency. This runs automatically before each commit. + +### Setup Pre-commit + +1. **Install pre-commit tools**: + ```bash + uv sync --group lint + ``` + +2. **Install the git hooks**: + ```bash + pre-commit install + ``` + +3. **Run pre-commit manually** (optional): + ```bash + uv run pre-commit run --all-files + ``` + +### Pre-commit Checks + +Our pre-commit configuration includes: +- **Trailing whitespace removal** +- **End-of-file fixing** +- **YAML validation** +- **Large file prevention** +- **Merge conflict detection** +- **Debug statement detection** +- **Code formatting with ruff** +- **Code linting with ruff** + +## πŸ§ͺ Testing + +### Running Tests + +```bash +# Install test tools only (no project runtime) +uv sync --group test + +# Run all tests +uv run pytest + +# Run specific test file +uv run pytest test/test_filename.py + +# Run with coverage +uv run pytest --cov=leann +``` + +### Writing Tests + +- Place tests in the `test/` directory +- Follow the naming convention `test_*.py` +- Use descriptive test names that explain what's being tested +- Include both positive and negative test cases + +## πŸ“ Code Style + +We use `ruff` for both linting and formatting to ensure consistent code style. + +### Format Your Code + +```bash +# Format all files +ruff format + +# Check formatting without changing files +ruff format --check +``` + +### Lint Your Code + +```bash +# Run linter with auto-fix +ruff check --fix + +# Just check without fixing +ruff check +``` + +### Style Guidelines + +- Follow PEP 8 conventions +- Use descriptive variable names +- Add type hints where appropriate +- Write docstrings for all public functions and classes +- Keep functions focused and single-purpose + +## 🚦 CI/CD + +Our CI pipeline runs automatically on all pull requests. It includes: + +1. **Linting and Formatting**: Ensures code follows our style guidelines +2. **Multi-platform builds**: Tests on Ubuntu and macOS +3. **Python version matrix**: Tests on Python 3.9-3.13 +4. **Wheel building**: Ensures packages can be built and distributed + +### CI Commands + +The CI uses the same commands as pre-commit to ensure consistency: +```bash +# Linting +ruff check . + +# Format checking +ruff format --check . +``` + +Make sure your code passes these checks locally before pushing! + +## πŸ”„ Pull Request Process + +1. **Fork the repository** and create your branch from `main`: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes**: + - Write clean, documented code + - Add tests for new functionality + - Update documentation as needed + +3. **Run pre-commit checks**: + ```bash + pre-commit run --all-files + ``` + +4. **Test your changes**: + ```bash + uv run pytest + ``` + +5. **Commit with descriptive messages**: + ```bash + git commit -m "feat: add new search algorithm" + ``` + + Follow [Conventional Commits](https://www.conventionalcommits.org/): + - `feat:` for new features + - `fix:` for bug fixes + - `docs:` for documentation changes + - `test:` for test additions/changes + - `refactor:` for code refactoring + - `perf:` for performance improvements + +6. **Push and create a pull request**: + - Provide a clear description of your changes + - Reference any related issues + - Include examples or screenshots if applicable + +## πŸ“š Documentation + +When adding new features or making significant changes: + +1. Update relevant documentation in `/docs` +2. Add docstrings to new functions/classes +3. Update README.md if needed +4. Include usage examples + +## πŸ€” Getting Help + +- **Discord**: Join our community for discussions +- **Issues**: Check existing issues or create a new one +- **Discussions**: For general questions and ideas + +## πŸ“„ License + +By contributing, you agree that your contributions will be licensed under the same license as the project (MIT). + +--- + +Thank you for contributing to LEANN! Every contribution, no matter how small, helps make the project better for everyone. 🌟 diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..8588f45 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,22 @@ +# Release Guide + +## Setup (One-time) + +Add `PYPI_API_TOKEN` to GitHub Secrets: +1. Get token: https://pypi.org/manage/account/token/ +2. Add to secrets: Settings β†’ Secrets β†’ Actions β†’ `PYPI_API_TOKEN` + +## Release (One-click) + +1. Go to: https://github.com/yichuan-w/LEANN/actions/workflows/release-manual.yml +2. Click "Run workflow" +3. Enter version: `0.1.2` +4. Click green "Run workflow" button + +That's it! The workflow will automatically: +- βœ… Update version in all packages +- βœ… Build all packages +- βœ… Publish to PyPI +- βœ… Create GitHub tag and release + +Check progress: https://github.com/yichuan-w/LEANN/actions diff --git a/docs/THINKING_BUDGET_FEATURE.md b/docs/THINKING_BUDGET_FEATURE.md new file mode 100644 index 0000000..ddd3071 --- /dev/null +++ b/docs/THINKING_BUDGET_FEATURE.md @@ -0,0 +1,123 @@ +# Thinking Budget Feature Implementation + +## Overview + +This document describes the implementation of the **thinking budget** feature for LEANN, which allows users to control the computational effort for reasoning models like GPT-Oss:20b. + +## Feature Description + +The thinking budget feature provides three levels of computational effort for reasoning models: +- **`low`**: Fast responses, basic reasoning (default for simple queries) +- **`medium`**: Balanced speed and reasoning depth +- **`high`**: Maximum reasoning effort, best for complex analytical questions + +## Implementation Details + +### 1. Command Line Interface + +Added `--thinking-budget` parameter to both CLI and RAG examples: + +```bash +# LEANN CLI +leann ask my-index --llm ollama --model gpt-oss:20b --thinking-budget high + +# RAG Examples +python apps/email_rag.py --llm ollama --llm-model gpt-oss:20b --thinking-budget high +python apps/document_rag.py --llm openai --llm-model o3 --thinking-budget medium +``` + +### 2. LLM Backend Support + +#### Ollama Backend (`packages/leann-core/src/leann/chat.py`) + +```python +def ask(self, prompt: str, **kwargs) -> str: + # Handle thinking budget for reasoning models + options = kwargs.copy() + thinking_budget = kwargs.get("thinking_budget") + if thinking_budget: + options.pop("thinking_budget", None) + if thinking_budget in ["low", "medium", "high"]: + options["reasoning"] = {"effort": thinking_budget, "exclude": False} +``` + +**API Format**: Uses Ollama's `reasoning` parameter with `effort` and `exclude` fields. + +#### OpenAI Backend (`packages/leann-core/src/leann/chat.py`) + +```python +def ask(self, prompt: str, **kwargs) -> str: + # Handle thinking budget for reasoning models + thinking_budget = kwargs.get("thinking_budget") + if thinking_budget and thinking_budget in ["low", "medium", "high"]: + # Check if this is an o-series model + o_series_models = ["o3", "o3-mini", "o4-mini", "o1", "o3-pro", "o3-deep-research"] + if any(model in self.model for model in o_series_models): + params["reasoning_effort"] = thinking_budget +``` + +**API Format**: Uses OpenAI's `reasoning_effort` parameter for o-series models. + +### 3. Parameter Propagation + +The thinking budget parameter is properly propagated through the LEANN architecture: + +1. **CLI** (`packages/leann-core/src/leann/cli.py`): Captures `--thinking-budget` argument +2. **Base RAG** (`apps/base_rag_example.py`): Adds parameter to argument parser +3. **LeannChat** (`packages/leann-core/src/leann/api.py`): Passes `llm_kwargs` to LLM +4. **LLM Interface**: Handles the parameter in backend-specific implementations + +## Files Modified + +### Core Implementation +- `packages/leann-core/src/leann/chat.py`: Added thinking budget support to OllamaChat and OpenAIChat +- `packages/leann-core/src/leann/cli.py`: Added `--thinking-budget` argument +- `apps/base_rag_example.py`: Added thinking budget parameter to RAG examples + +### Documentation +- `README.md`: Added thinking budget parameter to usage examples +- `docs/configuration-guide.md`: Added detailed documentation and usage guidelines + +### Examples +- `examples/thinking_budget_demo.py`: Comprehensive demo script with usage examples + +## Usage Examples + +### Basic Usage +```bash +# High reasoning effort for complex questions +leann ask my-index --llm ollama --model gpt-oss:20b --thinking-budget high + +# Medium reasoning for balanced performance +leann ask my-index --llm openai --model gpt-4o --thinking-budget medium + +# Low reasoning for fast responses +leann ask my-index --llm ollama --model gpt-oss:20b --thinking-budget low +``` + +### RAG Examples +```bash +# Email RAG with high reasoning +python apps/email_rag.py --llm ollama --llm-model gpt-oss:20b --thinking-budget high + +# Document RAG with medium reasoning +python apps/document_rag.py --llm openai --llm-model gpt-4o --thinking-budget medium +``` + +## Supported Models + +### Ollama Models +- **GPT-Oss:20b**: Primary target model with reasoning capabilities +- **Other reasoning models**: Any Ollama model that supports the `reasoning` parameter + +### OpenAI Models +- **o3, o3-mini, o4-mini, o1**: o-series reasoning models with `reasoning_effort` parameter +- **GPT-OSS models**: Models that support reasoning capabilities + +## Testing + +The implementation includes comprehensive testing: +- Parameter handling verification +- Backend-specific API format validation +- CLI argument parsing tests +- Integration with existing LEANN architecture diff --git a/docs/ast_chunking_guide.md b/docs/ast_chunking_guide.md new file mode 100644 index 0000000..34d7ccb --- /dev/null +++ b/docs/ast_chunking_guide.md @@ -0,0 +1,143 @@ +# AST-Aware Code chunking guide + +## Overview + +This guide covers best practices for using AST-aware code chunking in LEANN. AST chunking provides better semantic understanding of code structure compared to traditional text-based chunking. + +## Quick Start + +### Basic Usage + +```bash +# Enable AST chunking for mixed content (code + docs) +python -m apps.document_rag --enable-code-chunking --data-dir ./my_project + +# Specialized code repository indexing +python -m apps.code_rag --repo-dir ./my_codebase + +# Global CLI with AST support +leann build my-code-index --docs ./src --use-ast-chunking +``` + +### Installation + +```bash +# Install LEANN with AST chunking support +uv pip install -e "." +``` + +#### For normal users (PyPI install) +- Use `pip install leann` or `uv pip install leann`. +- `astchunk` is pulled automatically from PyPI as a dependency; no extra steps. + +#### For developers (from source, editable) +```bash +git clone https://github.com/yichuan-w/LEANN.git leann +cd leann +git submodule update --init --recursive +uv sync +``` +- This repo vendors `astchunk` as a git submodule at `packages/astchunk-leann` (our fork). +- `[tool.uv.sources]` maps the `astchunk` package to that path in editable mode. +- You can edit code under `packages/astchunk-leann` and Python will use your changes immediately (no separate `pip install astchunk` needed). + +## Best Practices + +### When to Use AST Chunking + +βœ… **Recommended for:** +- Code repositories with multiple languages +- Mixed documentation and code content +- Complex codebases with deep function/class hierarchies +- When working with Claude Code for code assistance + +❌ **Not recommended for:** +- Pure text documents +- Very large files (>1MB) +- Languages not supported by tree-sitter + +### Optimal Configuration + +```bash +# Recommended settings for most codebases +python -m apps.code_rag \ + --repo-dir ./src \ + --ast-chunk-size 768 \ + --ast-chunk-overlap 96 \ + --exclude-dirs .git __pycache__ node_modules build dist +``` + +### Supported Languages + +| Extension | Language | Status | +|-----------|----------|--------| +| `.py` | Python | βœ… Full support | +| `.java` | Java | βœ… Full support | +| `.cs` | C# | βœ… Full support | +| `.ts`, `.tsx` | TypeScript | βœ… Full support | +| `.js`, `.jsx` | JavaScript | βœ… Via TypeScript parser | + +## Integration Examples + +### Document RAG with Code Support + +```python +# Enable code chunking in document RAG +python -m apps.document_rag \ + --enable-code-chunking \ + --data-dir ./project \ + --query "How does authentication work in the codebase?" +``` + +### Claude Code Integration + +When using with Claude Code MCP server, AST chunking provides better context for: +- Code completion and suggestions +- Bug analysis and debugging +- Architecture understanding +- Refactoring assistance + +## Troubleshooting + +### Common Issues + +1. **Fallback to Traditional Chunking** + - Normal behavior for unsupported languages + - Check logs for specific language support + +2. **Performance with Large Files** + - Adjust `--max-file-size` parameter + - Use `--exclude-dirs` to skip unnecessary directories + +3. **Quality Issues** + - Try different `--ast-chunk-size` values (512, 768, 1024) + - Adjust overlap for better context preservation + +### Debug Mode + +```bash +export LEANN_LOG_LEVEL=DEBUG +python -m apps.code_rag --repo-dir ./my_code +``` + +## Migration from Traditional Chunking + +Existing workflows continue to work without changes. To enable AST chunking: + +```bash +# Before +python -m apps.document_rag --chunk-size 256 + +# After (maintains traditional chunking for non-code files) +python -m apps.document_rag --enable-code-chunking --chunk-size 256 --ast-chunk-size 768 +``` + +## References + +- [astchunk GitHub Repository](https://github.com/yilinjz/astchunk) +- [LEANN MCP Integration](../packages/leann-mcp/README.md) +- [Research Paper](https://arxiv.org/html/2506.15655v1) + +--- + +**Note**: AST chunking maintains full backward compatibility while enhancing code understanding capabilities. diff --git a/docs/code/embedding_model_compare.py b/docs/code/embedding_model_compare.py new file mode 100644 index 0000000..36c331a --- /dev/null +++ b/docs/code/embedding_model_compare.py @@ -0,0 +1,98 @@ +""" +Comparison between Sentence Transformers and OpenAI embeddings + +This example shows how different embedding models handle complex queries +and demonstrates the differences between local and API-based embeddings. +""" + +import numpy as np +from leann.embedding_compute import compute_embeddings + +# OpenAI API key should be set as environment variable +# export OPENAI_API_KEY="your-api-key-here" + +# Test data +conference_text = "[Title]: COLING 2025 Conference\n[URL]: https://coling2025.org/" +browser_text = "[Title]: Browser Use Tool\n[URL]: https://github.com/browser-use" + +# Two queries with same intent but different wording +query1 = "Tell me my browser history about some conference i often visit" +query2 = "browser history about conference I often visit" + +texts = [query1, query2, conference_text, browser_text] + + +def cosine_similarity(a, b): + return np.dot(a, b) # Already normalized + + +def analyze_embeddings(embeddings, model_name): + print(f"\n=== {model_name} Results ===") + + # Results for Query 1 + sim1_conf = cosine_similarity(embeddings[0], embeddings[2]) + sim1_browser = cosine_similarity(embeddings[0], embeddings[3]) + + print(f"Query 1: '{query1}'") + print(f" β†’ Conference similarity: {sim1_conf:.4f} {'βœ“' if sim1_conf > sim1_browser else ''}") + print( + f" β†’ Browser similarity: {sim1_browser:.4f} {'βœ“' if sim1_browser > sim1_conf else ''}" + ) + print(f" Winner: {'Conference' if sim1_conf > sim1_browser else 'Browser'}") + + # Results for Query 2 + sim2_conf = cosine_similarity(embeddings[1], embeddings[2]) + sim2_browser = cosine_similarity(embeddings[1], embeddings[3]) + + print(f"\nQuery 2: '{query2}'") + print(f" β†’ Conference similarity: {sim2_conf:.4f} {'βœ“' if sim2_conf > sim2_browser else ''}") + print( + f" β†’ Browser similarity: {sim2_browser:.4f} {'βœ“' if sim2_browser > sim2_conf else ''}" + ) + print(f" Winner: {'Conference' if sim2_conf > sim2_browser else 'Browser'}") + + # Show the impact + print("\n=== Impact Analysis ===") + print(f"Conference similarity change: {sim2_conf - sim1_conf:+.4f}") + print(f"Browser similarity change: {sim2_browser - sim1_browser:+.4f}") + + if sim1_conf > sim1_browser and sim2_browser > sim2_conf: + print("❌ FLIP: Adding 'browser history' flips winner from Conference to Browser!") + elif sim1_conf > sim1_browser and sim2_conf > sim2_browser: + print("βœ… STABLE: Conference remains winner in both queries") + elif sim1_browser > sim1_conf and sim2_browser > sim2_conf: + print("βœ… STABLE: Browser remains winner in both queries") + else: + print("πŸ”„ MIXED: Results vary between queries") + + return { + "query1_conf": sim1_conf, + "query1_browser": sim1_browser, + "query2_conf": sim2_conf, + "query2_browser": sim2_browser, + } + + +# Test Sentence Transformers +print("Testing Sentence Transformers (facebook/contriever)...") +try: + st_embeddings = compute_embeddings(texts, "facebook/contriever", mode="sentence-transformers") + st_results = analyze_embeddings(st_embeddings, "Sentence Transformers (facebook/contriever)") +except Exception as e: + print(f"❌ Sentence Transformers failed: {e}") + st_results = None + +# Test OpenAI +print("\n" + "=" * 60) +print("Testing OpenAI (text-embedding-3-small)...") +try: + openai_embeddings = compute_embeddings(texts, "text-embedding-3-small", mode="openai") + openai_results = analyze_embeddings(openai_embeddings, "OpenAI (text-embedding-3-small)") +except Exception as e: + print(f"❌ OpenAI failed: {e}") + openai_results = None + +# Compare results +if st_results and openai_results: + print("\n" + "=" * 60) + print("=== COMPARISON SUMMARY ===") diff --git a/docs/configuration-guide.md b/docs/configuration-guide.md new file mode 100644 index 0000000..66817d1 --- /dev/null +++ b/docs/configuration-guide.md @@ -0,0 +1,563 @@ +# LEANN Configuration Guide + +This guide helps you optimize LEANN for different use cases and understand the trade-offs between various configuration options. + +## Getting Started: Simple is Better + +When first trying LEANN, start with a small dataset to quickly validate your approach: + +**For document RAG**: The default `data/` directory works perfectly - includes 2 AI research papers, Pride and Prejudice literature, and a technical report +```bash +python -m apps.document_rag --query "What techniques does LEANN use?" +``` + +**For other data sources**: Limit the dataset size for quick testing +```bash +# WeChat: Test with recent messages only +python -m apps.wechat_rag --max-items 100 --query "What did we discuss about the project timeline?" + +# Browser history: Last few days +python -m apps.browser_rag --max-items 500 --query "Find documentation about vector databases" + +# Email: Recent inbox +python -m apps.email_rag --max-items 200 --query "Who sent updates about the deployment status?" +``` + +Once validated, scale up gradually: +- 100 documents β†’ 1,000 β†’ 10,000 β†’ full dataset (`--max-items -1`) +- This helps identify issues early before committing to long processing times + +## Embedding Model Selection: Understanding the Trade-offs + +Based on our experience developing LEANN, embedding models fall into three categories: + +### Small Models (< 100M parameters) +**Example**: `sentence-transformers/all-MiniLM-L6-v2` (22M params) +- **Pros**: Lightweight, fast for both indexing and inference +- **Cons**: Lower semantic understanding, may miss nuanced relationships +- **Use when**: Speed is critical, handling simple queries, interactive mode, or just experimenting with LEANN. If time is not a constraint, consider using a larger/better embedding model + +### Medium Models (100M-500M parameters) +**Example**: `facebook/contriever` (110M params), `BAAI/bge-base-en-v1.5` (110M params) +- **Pros**: Balanced performance, good multilingual support, reasonable speed +- **Cons**: Requires more compute than small models +- **Use when**: Need quality results without extreme compute requirements, general-purpose RAG applications + +### Large Models (500M+ parameters) +**Example**: `Qwen/Qwen3-Embedding-0.6B` (600M params), `intfloat/multilingual-e5-large` (560M params) +- **Pros**: Best semantic understanding, captures complex relationships, excellent multilingual support. **Qwen3-Embedding-0.6B achieves nearly OpenAI API performance!** +- **Cons**: Slower inference, longer index build times +- **Use when**: Quality is paramount and you have sufficient compute resources. **Highly recommended** for production use + +### Quick Start: Cloud and Local Embedding Options + +**OpenAI Embeddings (Fastest Setup)** +For immediate testing without local model downloads(also if you [do not have GPU](https://github.com/yichuan-w/LEANN/issues/43) and do not care that much about your document leak, you should use this, we compute the embedding and recompute using openai API): +```bash +# Set OpenAI embeddings (requires OPENAI_API_KEY) +--embedding-mode openai --embedding-model text-embedding-3-small +``` + +**Ollama Embeddings (Privacy-Focused)** +For local embeddings with complete privacy: +```bash +# First, pull an embedding model +ollama pull nomic-embed-text + +# Use Ollama embeddings +--embedding-mode ollama --embedding-model nomic-embed-text +``` + +
+Cloud vs Local Trade-offs + +**OpenAI Embeddings** (`text-embedding-3-small/large`) +- **Pros**: No local compute needed, consistently fast, high quality +- **Cons**: Requires API key, costs money, data leaves your system, [known limitations with certain languages](https://yichuan-w.github.io/blog/lessons_learned_in_dev_leann/) +- **When to use**: Prototyping, non-sensitive data, need immediate results + +**Local Embeddings** +- **Pros**: Complete privacy, no ongoing costs, full control, can sometimes outperform OpenAI embeddings +- **Cons**: Slower than cloud APIs, requires local compute resources +- **When to use**: Production systems, sensitive data, cost-sensitive applications + +
+ +## Local & Remote Inference Endpoints + +> Applies to both LLMs (`leann ask`) and embeddings (`leann build`). + +LEANN now treats Ollama, LM Studio, and other OpenAI-compatible runtimes as first-class providers. You can point LEANN at any compatible endpoint – either on the same machine or across the network – with a couple of flags or environment variables. + +### One-Time Environment Setup + +```bash +# Works for OpenAI-compatible runtimes such as LM Studio, vLLM, SGLang, llamafile, etc. +export OPENAI_API_KEY="your-key" # or leave unset for local servers that do not check keys +export OPENAI_BASE_URL="http://localhost:1234/v1" + +# Ollama-compatible runtimes (Ollama, Ollama on another host, llamacpp-server, etc.) +export LEANN_OLLAMA_HOST="http://localhost:11434" # falls back to OLLAMA_HOST or LOCAL_LLM_ENDPOINT +``` + +LEANN also recognises `LEANN_LOCAL_LLM_HOST` (highest priority), `LEANN_OPENAI_BASE_URL`, and `LOCAL_OPENAI_BASE_URL`, so existing scripts continue to work. + +### Passing Hosts Per Command + +```bash +# Build an index with a remote embedding server +leann build my-notes \ + --docs ./notes \ + --embedding-mode openai \ + --embedding-model text-embedding-qwen3-embedding-0.6b \ + --embedding-api-base http://192.168.1.50:1234/v1 \ + --embedding-api-key local-dev-key + +# Query using a local LM Studio instance via OpenAI-compatible API +leann ask my-notes \ + --llm openai \ + --llm-model qwen3-8b \ + --api-base http://localhost:1234/v1 \ + --api-key local-dev-key + +# Query an Ollama instance running on another box +leann ask my-notes \ + --llm ollama \ + --llm-model qwen3:14b \ + --host http://192.168.1.101:11434 +``` + +⚠️ **Make sure the endpoint is reachable**: when your inference server runs on a home/workstation and the index/search job runs in the cloud, the server must be able to reach the host you configured. Typical options include: + +- Expose a public IP (and open the relevant port) on the machine that hosts LM Studio/Ollama. +- Configure router or cloud provider port forwarding. +- Tunnel traffic through tools like `tailscale`, `cloudflared`, or `ssh -R`. + +When you set these options while building an index, LEANN stores them in `meta.json`. Any subsequent `leann ask` or searcher process automatically reuses the same provider settings – even when we spawn background embedding servers. This makes the β€œserver without GPU talking to my local workstation” workflow from [issue #80](https://github.com/yichuan-w/LEANN/issues/80#issuecomment-2287230548) work out-of-the-box. + +**Tip:** If your runtime does not require an API key (many local stacks don’t), leave `--api-key` unset. LEANN will skip injecting credentials. + +### Python API Usage + +You can pass the same configuration from Python: + +```python +from leann.api import LeannBuilder + +builder = LeannBuilder( + backend_name="hnsw", + embedding_mode="openai", + embedding_model="text-embedding-qwen3-embedding-0.6b", + embedding_options={ + "base_url": "http://192.168.1.50:1234/v1", + "api_key": "local-dev-key", + }, +) +builder.build_index("./indexes/my-notes", chunks) +``` + +`embedding_options` is persisted to the index `meta.json`, so subsequent `LeannSearcher` or `LeannChat` sessions automatically reuse the same provider settings (the embedding server manager forwards them to the provider for you). + +## Optional Embedding Features + +### Task-Specific Prompt Templates + +Some embedding models are trained with task-specific prompts to differentiate between documents and queries. The most notable example is **Google's EmbeddingGemma**, which requires different prompts depending on the use case: + +- **Indexing documents**: `"title: none | text: "` +- **Search queries**: `"task: search result | query: "` + +LEANN supports automatic prompt prepending via the `--embedding-prompt-template` flag: + +```bash +# Build index with EmbeddingGemma (via LM Studio or Ollama) +leann build my-docs \ + --docs ./documents \ + --embedding-mode openai \ + --embedding-model text-embedding-embeddinggemma-300m-qat \ + --embedding-api-base http://localhost:1234/v1 \ + --embedding-prompt-template "title: none | text: " \ + --force + +# Search with query-specific prompt +leann search my-docs \ + --query "What is quantum computing?" \ + --embedding-prompt-template "task: search result | query: " +``` + +A full example that is used for building the LEANN's repo during dev: +``` +source "$LEANN_PATH/.venv/bin/activate" && \ +leann build --docs $(git ls-files | grep -Ev '\.(png|jpg|jpeg|gif|yml|yaml|sh|pdf|JPG)$') --embedding-mode openai \ +--embedding-model text-embedding-embeddinggemma-300m-qat \ +--embedding-prompt-template "title: none | text: " \ +--query-prompt-template "task: search result | query: " \ +--embedding-api-key local-dev-key \ +--embedding-api-base http://localhost:1234/v1 \ +--doc-chunk-size 1024 --doc-chunk-overlap 100 \ +--code-chunk-size 1024 --code-chunk-overlap 100 \ +--ast-chunk-size 1024 --ast-chunk-overlap 100 \ +--force --use-ast-chunking --no-compact --no-recompute +``` + +**Important Notes:** +- **Only use with compatible models**: EmbeddingGemma and similar task-specific models +- **NOT for regular models**: Adding prompts to models like `nomic-embed-text`, `text-embedding-3-small`, or `bge-base-en-v1.5` will corrupt embeddings +- **Template is saved**: Build-time templates are saved to `.meta.json` for reference; you can add both `--embedding-prompt-template` and `--query-prompt-template` values during building phase, and this way the mcp query will automatically pick up the query template +- **Flexible prompts**: You can use any prompt string, or leave it empty (`""`) + +**Python API:** +```python +from leann.api import LeannBuilder + +builder = LeannBuilder( + embedding_mode="openai", + embedding_model="text-embedding-embeddinggemma-300m-qat", + embedding_options={ + "base_url": "http://localhost:1234/v1", + "api_key": "lm-studio", + "prompt_template": "title: none | text: ", + }, +) +builder.build_index("./indexes/my-docs", chunks) +``` + +**References:** +- [HuggingFace Blog: EmbeddingGemma](https://huggingface.co/blog/embeddinggemma) - Technical details + +### LM Studio Auto-Detection (Optional) + +When using LM Studio with the OpenAI-compatible API, LEANN can optionally auto-detect model context lengths via the LM Studio SDK. This eliminates manual configuration for token limits. + +**Prerequisites:** +```bash +# Install Node.js (if not already installed) +# Then install the LM Studio SDK globally +npm install -g @lmstudio/sdk +``` + +**How it works:** +1. LEANN detects LM Studio URLs (`:1234`, `lmstudio` in URL) +2. Queries model metadata via Node.js subprocess +3. Automatically unloads model after query (respects your JIT auto-evict settings) +4. Falls back to static registry if SDK unavailable + +**No configuration needed** - it works automatically when SDK is installed: + +```bash +leann build my-docs \ + --docs ./documents \ + --embedding-mode openai \ + --embedding-model text-embedding-nomic-embed-text-v1.5 \ + --embedding-api-base http://localhost:1234/v1 + # Context length auto-detected if SDK available + # Falls back to registry (2048) if not +``` + +**Benefits:** +- βœ… Automatic token limit detection +- βœ… Respects LM Studio JIT auto-evict settings +- βœ… No manual registry maintenance +- βœ… Graceful fallback if SDK unavailable + +**Note:** This is completely optional. LEANN works perfectly fine without the SDK using the built-in token limit registry. + +## Index Selection: Matching Your Scale + +### HNSW (Hierarchical Navigable Small World) +**Best for**: Small to medium datasets (< 10M vectors) - **Default and recommended for extreme low storage** +- Full recomputation required +- High memory usage during build phase +- Excellent recall (95%+) + +```bash +# Optimal for most use cases +--backend-name hnsw --graph-degree 32 --build-complexity 64 +``` + +### DiskANN +**Best for**: Large datasets, especially when you want `recompute=True`. + +**Key advantages:** +- **Faster search** on large datasets (3x+ speedup vs HNSW in many cases) +- **Smart storage**: `recompute=True` enables automatic graph partitioning for smaller indexes +- **Better scaling**: Designed for 100k+ documents + +**Recompute behavior:** +- `recompute=True` (recommended): Pure PQ traversal + final reranking - faster and enables partitioning +- `recompute=False`: PQ + partial real distances during traversal - slower but higher accuracy + +```bash +# Recommended for most use cases +--backend-name diskann --graph-degree 32 --build-complexity 64 +``` + +**Performance Benchmark**: Run `uv run benchmarks/diskann_vs_hnsw_speed_comparison.py` to compare DiskANN and HNSW on your system. + +## LLM Selection: Engine and Model Comparison + +### LLM Engines + +**OpenAI** (`--llm openai`) +- **Pros**: Best quality, consistent performance, no local resources needed +- **Cons**: Costs money ($0.15-2.5 per million tokens), requires internet, data privacy concerns +- **Models**: `gpt-4o-mini` (fast, cheap), `gpt-4o` (best quality), `o3` (reasoning), `o3-mini` (reasoning, cheaper) +- **Thinking Budget**: Use `--thinking-budget low/medium/high` for o-series reasoning models (o3, o3-mini, o4-mini) +- **Note**: Our current default, but we recommend switching to Ollama for most use cases + +**Ollama** (`--llm ollama`) +- **Pros**: Fully local, free, privacy-preserving, good model variety +- **Cons**: Requires local GPU/CPU resources, slower than cloud APIs, need to install extra [ollama app](https://github.com/ollama/ollama?tab=readme-ov-file#ollama) and pre-download models by `ollama pull` +- **Models**: `qwen3:0.6b` (ultra-fast), `qwen3:1.7b` (balanced), `qwen3:4b` (good quality), `qwen3:7b` (high quality), `deepseek-r1:1.5b` (reasoning) +- **Thinking Budget**: Use `--thinking-budget low/medium/high` for reasoning models like GPT-Oss:20b + +**HuggingFace** (`--llm hf`) +- **Pros**: Free tier available, huge model selection, direct model loading (vs Ollama's server-based approach) +- **Cons**: More complex initial setup +- **Models**: `Qwen/Qwen3-1.7B-FP8` + +## Parameter Tuning Guide + +### Search Complexity Parameters + +**`--build-complexity`** (index building) +- Controls thoroughness during index construction +- Higher = better recall but slower build +- Recommendations: + - 32: Quick prototyping + - 64: Balanced (default) + - 128: Production systems + - 256: Maximum quality + +**`--search-complexity`** (query time) +- Controls search thoroughness +- Higher = better results but slower +- Recommendations: + - 16: Fast/Interactive search + - 32: High quality with diversity + - 64+: Maximum accuracy + +### Top-K Selection + +**`--top-k`** (number of retrieved chunks) +- More chunks = better context but slower LLM processing +- Should be always smaller than `--search-complexity` +- Guidelines: + - 10-20: General questions (default: 20) + - 30+: Complex multi-hop reasoning requiring comprehensive context + +**Trade-off formula**: +- Retrieval time ∝ log(n) Γ— search_complexity +- LLM processing time ∝ top_k Γ— chunk_size +- Total context = top_k Γ— chunk_size tokens + +### Thinking Budget for Reasoning Models + +**`--thinking-budget`** (reasoning effort level) +- Controls the computational effort for reasoning models +- Options: `low`, `medium`, `high` +- Guidelines: + - `low`: Fast responses, basic reasoning (default for simple queries) + - `medium`: Balanced speed and reasoning depth + - `high`: Maximum reasoning effort, best for complex analytical questions +- **Supported Models**: + - **Ollama**: `gpt-oss:20b`, `gpt-oss:120b` + - **OpenAI**: `o3`, `o3-mini`, `o4-mini`, `o1` (o-series reasoning models) +- **Note**: Models without reasoning support will show a warning and proceed without reasoning parameters +- **Example**: `--thinking-budget high` for complex analytical questions + +**πŸ“– For detailed usage examples and implementation details, check out [Thinking Budget Documentation](THINKING_BUDGET_FEATURE.md)** + +**πŸ’‘ Quick Examples:** +```bash +# OpenAI o-series reasoning model +python apps/document_rag.py --query "What are the main techniques LEANN explores?" \ + --index-dir hnswbuild --backend hnsw \ + --llm openai --llm-model o3 --thinking-budget medium + +# Ollama reasoning model +python apps/document_rag.py --query "What are the main techniques LEANN explores?" \ + --index-dir hnswbuild --backend hnsw \ + --llm ollama --llm-model gpt-oss:20b --thinking-budget high +``` + +### Graph Degree (HNSW/DiskANN) + +**`--graph-degree`** +- Number of connections per node in the graph +- Higher = better recall but more memory +- HNSW: 16-32 (default: 32) +- DiskANN: 32-128 (default: 64) + + +## Performance Optimization Checklist + +### If Embedding is Too Slow + +1. **Switch to smaller model**: + ```bash + # From large model + --embedding-model Qwen/Qwen3-Embedding-0.6B + # To small model + --embedding-model sentence-transformers/all-MiniLM-L6-v2 + ``` + +2. **Limit dataset size for testing**: + ```bash + --max-items 1000 # Process first 1k items only + ``` + +3. **Use MLX on Apple Silicon** (optional optimization): + ```bash + --embedding-mode mlx --embedding-model mlx-community/Qwen3-Embedding-0.6B-8bit + ``` + MLX might not be the best choice, as we tested and found that it only offers 1.3x acceleration compared to HF, so maybe using ollama is a better choice for embedding generation + +4. **Use Ollama** + ```bash + --embedding-mode ollama --embedding-model nomic-embed-text + ``` + To discover additional embedding models in ollama, check out https://ollama.com/search?c=embedding or read more about embedding models at https://ollama.com/blog/embedding-models, please do check the model size that works best for you +### If Search Quality is Poor + +1. **Increase retrieval count**: + ```bash + --top-k 30 # Retrieve more candidates + ``` + +2. **Upgrade embedding model**: + ```bash + # For English + --embedding-model BAAI/bge-base-en-v1.5 + # For multilingual + --embedding-model intfloat/multilingual-e5-large + ``` + +## Understanding the Trade-offs + +Every configuration choice involves trade-offs: + +| Factor | Small/Fast | Large/Quality | +|--------|------------|---------------| +| Embedding Model | `all-MiniLM-L6-v2` | `Qwen/Qwen3-Embedding-0.6B` | +| Chunk Size | 512 tokens | 128 tokens | +| Index Type | HNSW | DiskANN | +| LLM | `qwen3:1.7b` | `gpt-4o` | + +The key is finding the right balance for your specific use case. Start small and simple, measure performance, then scale up only where needed. + +## Low-resource setups + +If you don’t have a local GPU or builds/searches are too slow, use one or more of the options below. + +### 1) Use OpenAI embeddings (no local compute) + +Fastest path with zero local GPU requirements. Set your API key and use OpenAI embeddings during build and search: + +```bash +export OPENAI_API_KEY=sk-... + +# Build with OpenAI embeddings +leann build my-index \ + --embedding-mode openai \ + --embedding-model text-embedding-3-small + +# Search with OpenAI embeddings (recompute at query time) +leann search my-index "your query" \ + --recompute +``` + +### 2) Run remote builds with SkyPilot (cloud GPU) + +Offload embedding generation and index building to a GPU VM using [SkyPilot](https://docs.skypilot.co/en/latest/docs/index.html). A template is provided at `sky/leann-build.yaml`. + +```bash +# One-time: install and configure SkyPilot +pip install skypilot + +# Launch with defaults (L4:1) and mount ./data to ~/leann-data; the build runs automatically +sky launch -c leann-gpu sky/leann-build.yaml + +# Override parameters via -e key=value (optional) +sky launch -c leann-gpu sky/leann-build.yaml \ + -e index_name=my-index \ + -e backend=hnsw \ + -e embedding_mode=sentence-transformers \ + -e embedding_model=Qwen/Qwen3-Embedding-0.6B + +# Copy the built index back to your local .leann (use rsync) +rsync -Pavz leann-gpu:~/.leann/indexes/my-index ./.leann/indexes/ +``` + +### 3) Disable recomputation to trade storage for speed + +If you need lower latency and have more storage/memory, disable recomputation. This stores full embeddings and avoids recomputing at search time. + +```bash +# Build without recomputation (HNSW requires non-compact in this mode) +leann build my-index --no-recompute --no-compact + +# Search without recomputation +leann search my-index "your query" --no-recompute +``` + +When to use: +- Extreme low latency requirements (high QPS, interactive assistants) +- Read-heavy workloads where storage is cheaper than latency +- No always-available GPU + +Constraints: +- HNSW: when `--no-recompute` is set, LEANN automatically disables compact mode during build +- DiskANN: supported; `--no-recompute` skips selective recompute during search + +Storage impact: +- Storing N embeddings of dimension D with float32 requires approximately N Γ— D Γ— 4 bytes +- Example: 1,000,000 chunks Γ— 768 dims Γ— 4 bytes β‰ˆ 2.86 GB (plus graph/metadata) + +Converting an existing index (rebuild required): +```bash +# Rebuild in-place (ensure you still have original docs or can regenerate chunks) +leann build my-index --force --no-recompute --no-compact +``` + +Python API usage: +```python +from leann import LeannSearcher + +searcher = LeannSearcher("/path/to/my-index.leann") +results = searcher.search("your query", top_k=10, recompute_embeddings=False) +``` + +Trade-offs: +- Lower latency and fewer network hops at query time +- Significantly higher storage (10–100Γ— vs selective recomputation) +- Slightly larger memory footprint during build and search + +Quick benchmark results (`benchmarks/benchmark_no_recompute.py` with 5k texts, complexity=32): + +- HNSW + + ```text + recompute=True: search_time=0.818s, size=1.1MB + recompute=False: search_time=0.012s, size=16.6MB + ``` + +- DiskANN + + ```text + recompute=True: search_time=0.041s, size=5.9MB + recompute=False: search_time=0.013s, size=24.6MB + ``` + +Conclusion: +- **HNSW**: `no-recompute` is significantly faster (no embedding recomputation) but requires much more storage (stores all embeddings) +- **DiskANN**: `no-recompute` uses PQ + partial real distances during traversal (slower but higher accuracy), while `recompute=True` uses pure PQ traversal + final reranking (faster traversal, enables build-time partitioning for smaller storage) + + + +## Further Reading + +- [Lessons Learned Developing LEANN](https://yichuan-w.github.io/blog/lessons_learned_in_dev_leann/) +- [LEANN Technical Paper](https://arxiv.org/abs/2506.08276) +- [DiskANN Original Paper](https://suhasjs.github.io/files/diskann_neurips19.pdf) +- [SSD-based Graph Partitioning](https://github.com/SonglinLife/SSD_BASED_PLAN) diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..c469a91 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,58 @@ +# FAQ + +## 1. My building time seems long + +You can speed up the process by using a lightweight embedding model. Add this to your arguments: + +```bash +--embedding-model sentence-transformers/all-MiniLM-L6-v2 +``` +**Model sizes:** `all-MiniLM-L6-v2` (30M parameters), `facebook/contriever` (~100M parameters), `Qwen3-0.6B` (600M parameters) + +## 2. When should I use prompt templates? + +**Use prompt templates ONLY with task-specific embedding models** like Google's EmbeddingGemma. These models are specially trained to use different prompts for documents vs queries. + +**DO NOT use with regular models** like `nomic-embed-text`, `text-embedding-3-small`, or `bge-base-en-v1.5` - adding prompts to these models will corrupt the embeddings. + +**Example usage with EmbeddingGemma:** +```bash +# Build with document prompt +leann build my-docs --embedding-prompt-template "title: none | text: " + +# Search with query prompt +leann search my-docs --query "your question" --embedding-prompt-template "task: search result | query: " +``` + +See the [Configuration Guide: Task-Specific Prompt Templates](configuration-guide.md#task-specific-prompt-templates) for detailed usage. + +## 3. Why is LM Studio loading multiple copies of my model? + +This was fixed in recent versions. LEANN now properly unloads models after querying metadata, respecting your LM Studio JIT auto-evict settings. + +**If you still see duplicates:** +- Update to the latest LEANN version +- Restart LM Studio to clear loaded models +- Check that you have JIT auto-evict enabled in LM Studio settings + +**How it works now:** +1. LEANN loads model temporarily to get context length +2. Immediately unloads after query +3. LM Studio JIT loads model on-demand for actual embeddings +4. Auto-evicts per your settings + +## 4. Do I need Node.js and @lmstudio/sdk? + +**No, it's completely optional.** LEANN works perfectly fine without them using a built-in token limit registry. + +**Benefits if you install it:** +- Automatic context length detection for LM Studio models +- No manual registry maintenance +- Always gets accurate token limits from the model itself + +**To install (optional):** +```bash +npm install -g @lmstudio/sdk +``` + +See [Configuration Guide: LM Studio Auto-Detection](configuration-guide.md#lm-studio-auto-detection-optional) for details. diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 0000000..0a7f9dc --- /dev/null +++ b/docs/features.md @@ -0,0 +1,23 @@ +# ✨ Detailed Features + +## πŸ”₯ Core Features + +- **πŸ”„ Real-time Embeddings** - Eliminate heavy embedding storage with dynamic computation using optimized ZMQ servers and highly optimized search paradigm (overlapping and batching) with highly optimized embedding engine +- **🧠 AST-Aware Code Chunking** - Intelligent code chunking that preserves semantic boundaries (functions, classes, methods) for Python, Java, C#, and TypeScript files +- **πŸ“ˆ Scalable Architecture** - Handles millions of documents on consumer hardware; the larger your dataset, the more LEANN can save +- **🎯 Graph Pruning** - Advanced techniques to minimize the storage overhead of vector search to a limited footprint +- **πŸ—οΈ Pluggable Backends** - HNSW/FAISS (default), with optional DiskANN for large-scale deployments + +## πŸ› οΈ Technical Highlights +- **πŸ”„ Recompute Mode** - Highest accuracy scenarios while eliminating vector storage overhead +- **⚑ Zero-copy Operations** - Minimize IPC overhead by transferring distances instead of embeddings +- **πŸš€ High-throughput Embedding Pipeline** - Optimized batched processing for maximum efficiency +- **🎯 Two-level Search** - Novel coarse-to-fine search overlap for accelerated query processing (optional) +- **πŸ’Ύ Memory-mapped Indices** - Fast startup with raw text mapping to reduce memory overhead +- **πŸš€ MLX Support** - Ultra-fast recompute/build with quantized embedding models, accelerating building and search ([minimal example](../examples/mlx_demo.py)) + +## 🎨 Developer Experience + +- **Simple Python API** - Get started in minutes +- **Extensible backend system** - Easy to add new algorithms +- **Comprehensive examples** - From basic usage to production deployment diff --git a/docs/flashlib_backend_guide.md b/docs/flashlib_backend_guide.md new file mode 100644 index 0000000..ed5a155 --- /dev/null +++ b/docs/flashlib_backend_guide.md @@ -0,0 +1,179 @@ +# FlashLib Backend Guide + +LEANN now ships an optional, GPU-accelerated search backend powered by +[**FlashLib**](https://github.com/FlashML-org/flashlib) β€” a library of classical +machine-learning operators built on Triton and CuteDSL. This guide covers what it +is, when to use it, how to install it, and how it plugs into LEANN. + +## What is FlashLib? + +FlashLib (`pip install flashlib`) is a GPU library of classical ML primitives +(k-means, DBSCAN, PCA, SVD, UMAP, t-SNE, IVF-Flat ANN, and more). LEANN uses its +`IVFFlat` index β€” an inverted-file flat approximate-nearest-neighbor (ANN) index +that runs entirely on CUDA tensors. At a fixed `(nlist, nprobe)` it probes the same +candidate set as a reference IVF-Flat (FAISS / cuVS), so recall is predictable, +while the search itself is accelerated on the GPU. + +```python +import torch +from flashlib import IVFFlat + +db = torch.randn(1_000_000, 128, device="cuda") +index = IVFFlat(nlist=1024, nprobe=16).fit(db) +distances, indices = index.kneighbors(torch.randn(10_000, 128, device="cuda"), n_neighbors=10) +``` + +## When to use it + +| Backend | Best for | Storage | Hardware | +|---------|----------|---------|----------| +| `hnsw` (default) | Laptop / CPU, max storage savings via recomputation | ~3% of raw (pruned graph) | CPU | +| `diskann` | Larger-than-memory datasets | On-disk graph | CPU | +| `ivf` | Incremental add/remove without rebuild | Full vectors (FAISS) | CPU | +| **`flashlib`** | **High-throughput search on a CUDA GPU** | Full vectors (`.npy`) | **CUDA GPU** | +| **`flashlib_ivf`** | **GPU IVF-Flat (approximate) β€” the GPU counterpart of `ivf`** | Full vectors (`.pt`) | **CUDA GPU** | + +Use FlashLib when you already have a GPU and want fast IVF-Flat search. It stores +the full float32 vectors rather than a pruned graph, so it trades LEANN's storage +savings for raw GPU search speed. + +## Requirements + +- A **CUDA GPU** (required at *search* time; building the index only needs numpy). +- `flashlib` and `torch` (installed automatically with the extra below). + +## Installation + +The backend is **optional** β€” it is not pulled in by a default LEANN install. + +```bash +# From a LEANN source checkout +uv sync --extra flashlib + +# Or as a standalone package +pip install leann-backend-flashlib +``` + +LEANN auto-discovers any installed `leann-backend-*` package, so once it is +installed the `flashlib` backend name is available with no further configuration. + +## Usage + +### Python API + +```python +from leann import LeannBuilder, LeannSearcher + +builder = LeannBuilder(backend_name="flashlib") # nlist=1024, distance_metric="mips" +builder.add_text("LEANN recomputes embeddings on the fly to cut storage by ~97%.") +builder.add_text("FlashLib runs IVF-Flat search on the GPU.") +builder.build_index("demo.leann") + +searcher = LeannSearcher("demo.leann") +results = searcher.search("How does LEANN save storage?", top_k=3) +for r in results: + print(r.score, r.text) +``` + +### Example apps / CLI + +```bash +source .venv/bin/activate +python -m apps.document_rag \ + --query "What are the main techniques LEANN explores?" \ + --backend-name flashlib +``` + +## How it works + +FlashLib's `IVFFlat` builds its index in GPU memory and has **no on-disk format**. +The LEANN backend bridges that gap: + +1. **Build** (`FlashlibBuilder`): persists the raw float32 vectors as + `.flashlib.npy` and an id map as `.flashlib_id_map.json`. +2. **Search** (`FlashlibSearcher`): loads the vectors into a CUDA tensor and + reconstructs the index once via `IVFFlat(nlist, nprobe).fit(db)` at start-up, + then answers every query with `index.kneighbors(...)`. + +FlashLib's only distance metric is **squared L2**. For `mips` / `cosine` the +backend L2-normalizes both the database and the query vectors; on unit vectors, +squared-L2 ranking is equivalent to inner-product / cosine ranking, so results +match the other backends. `nlist` is clamped to the corpus size (the k-means +constraint), and `nprobe` is derived from the search `complexity` knob. + +## Parameters + +| Parameter | Default | Meaning | +|-----------|---------|---------| +| `nlist` (build) | `1024` | Number of IVF partitions; clamped to the number of vectors. | +| `distance_metric` (build) | `"mips"` | `mips`, `cosine`, or `l2`. | +| `nprobe` (search) | derived from `complexity` | Partitions probed per query β€” the recall knob (higher = better recall, slower). | + +## FlashLib IVF backend (`flashlib_ivf`) + +The **`flashlib_ivf`** backend (`leann-backend-flashlib-ivf`) is the GPU counterpart +of LEANN's FAISS `ivf` backend: an IVF-Flat (inverted file) index that +coarse-quantizes the corpus into `nlist` cells with k-means and, at search time, +scans only the `nprobe` nearest cells β€” entirely on CUDA tensors via FlashLib's +`flash_ivf_flat`. At a fixed `(nlist, nprobe)` the GPU and CPU IVF probe nearly the +same candidate set, so recall is comparable; the only difference is GPU vs CPU +kernels. `nprobe` is the recall knob (defaults to `min(complexity, nlist)`). + +```bash +uv sync --extra flashlib-ivf # or: pip install leann-backend-flashlib-ivf +``` + +```python +from leann import LeannBuilder, LeannSearcher + +builder = LeannBuilder(backend_name="flashlib_ivf", nlist=4096, distance_metric="cosine") +builder.add_text("LEANN recomputes embeddings on the fly to cut storage by ~97%.") +builder.build_index("demo.leann") + +searcher = LeannSearcher("demo.leann") +results = searcher.search("How does LEANN save storage?", top_k=10, complexity=32) # nprobe=32 +``` + +**How it works:** the builder trains the coarse quantizer on the GPU and persists the +built index tensors with `torch.save` (`.flashlib_ivf.pt`) plus an id map +(`.flashlib_ivf_id_map.json`); the searcher reloads them onto the GPU once (no +k-means re-train). A CUDA GPU is required at **both** build (k-means) and search time. +FlashLib IVF ranks by squared L2, so `mips`/`cosine` L2-normalize the vectors (squared-L2 +ranking then matches inner-product/cosine). + +### Speed β€” IVF (GPU) vs IVF (CPU) + +```bash +python benchmarks/flashlib_ivf_vs_faiss_ivf.py \ + --sizes 100000 1000000 --nprobe-sweep 1 8 32 128 --cpu-threads 8 +``` + +`flashlib_ivf` (GPU) vs the FAISS `ivf` backend (CPU) at the **same `nlist`**, sweeping +`nprobe` (NVIDIA H200, `faiss-cpu` at 8 threads, 768-dim, top-k=10). GPU latency stays +~flat while CPU latency grows linearly with `nprobe`, so the GPU lead widens exactly as +you raise recall: + +| Corpus | nprobe | GPU lat | CPU lat | GPU q/s | CPU q/s | Recall (GPU/CPU) | Speedup (lat / tpt) | +|--------|--------|---------|---------|---------|---------|------------------|---------------------| +| 1M | 8 | 0.45 ms | 1.14 ms | 107k | 5.9k | 0.340 / 0.321 | 2.6Γ— / 18Γ— | +| 1M | 32 | 0.46 ms | 3.00 ms | 141k | 1.9k | 0.400 / 0.350 | 6.5Γ— / 75Γ— | +| 1M | 128 | 0.55 ms | 9.91 ms | 95k | 0.6k | 0.539 / 0.423 | 18Γ— / 159Γ— | + +Build (1M, `nlist=4096`): GPU **10.6 s** vs FAISS CPU **140.7 s** β€” a **13Γ— faster +build** (GPU k-means vs CPU k-means training). + +Honest caveats: at very low `nprobe` (e.g. 1) single-query GPU latency (~0.44 ms) is +*higher* than CPU, because the per-query work is tiny and GPU kernel-launch overhead +dominates; the GPU advantage grows with `nprobe` (higher recall), batch size, and corpus +size. The absolute recall above is low because the synthetic mixture-of-Gaussians corpus +has more clusters than `nlist`; on real embeddings recall is far higher β€” this benchmark +isolates the GPU-vs-CPU *relative* comparison at matched `(nlist, nprobe)`. + +## Notes & limitations + +- GPU-only at search time; building an index works on a CPU-only machine. +- Stores full vectors, so it does not benefit from LEANN's graph-pruning storage + savings β€” pick `hnsw` if minimizing disk footprint is the priority. +- Query embeddings are computed through the standard LEANN embedding path (the + HNSW ZMQ embedding server when available, otherwise direct model loading), so + any LEANN-supported embedding model works. diff --git a/docs/grep_search.md b/docs/grep_search.md new file mode 100644 index 0000000..4fe002f --- /dev/null +++ b/docs/grep_search.md @@ -0,0 +1,149 @@ +# LEANN Grep Search Usage Guide + +## Overview + +LEANN's grep search functionality provides exact text matching for finding specific code patterns, error messages, function names, or exact phrases in your indexed documents. + +## Basic Usage + +### Simple Grep Search + +```python +from leann.api import LeannSearcher + +searcher = LeannSearcher("your_index_path") + +# Exact text search +results = searcher.search("def authenticate_user", use_grep=True, top_k=5) + +for result in results: + print(f"Score: {result.score}") + print(f"Text: {result.text[:100]}...") + print("-" * 40) +``` + +### Comparison: Semantic vs Grep Search + +```python +# Semantic search - finds conceptually similar content +semantic_results = searcher.search("machine learning algorithms", top_k=3) + +# Grep search - finds exact text matches +grep_results = searcher.search("def train_model", use_grep=True, top_k=3) +``` + +## When to Use Grep Search + +### Use Cases + +- **Code Search**: Finding specific function definitions, class names, or variable references +- **Error Debugging**: Locating exact error messages or stack traces +- **Documentation**: Finding specific API endpoints or exact terminology + +### Examples + +```python +# Find function definitions +functions = searcher.search("def __init__", use_grep=True) + +# Find import statements +imports = searcher.search("from sklearn import", use_grep=True) + +# Find specific error types +errors = searcher.search("FileNotFoundError", use_grep=True) + +# Find TODO comments +todos = searcher.search("TODO:", use_grep=True) + +# Find configuration entries +configs = searcher.search("server_port=", use_grep=True) +``` + +## Technical Details + +### How It Works + +1. **File Location**: Grep search operates on the raw text stored in `.jsonl` files +2. **Command Execution**: Uses the system `grep` command with case-insensitive search +3. **Result Processing**: Parses JSON lines and extracts text and metadata +4. **Scoring**: Simple frequency-based scoring based on query term occurrences + +### Search Process + +``` +Query: "def train_model" + ↓ +grep -i -n "def train_model" documents.leann.passages.jsonl + ↓ +Parse matching JSON lines + ↓ +Calculate scores based on term frequency + ↓ +Return top_k results +``` + +### Scoring Algorithm + +```python +# Term frequency in document +score = text.lower().count(query.lower()) +``` + +Results are ranked by score (highest first), with higher scores indicating more occurrences of the search term. + +## Error Handling + +### Common Issues + +#### Grep Command Not Found +``` +RuntimeError: grep command not found. Please install grep or use semantic search. +``` + +**Solution**: Install grep on your system: +- **Ubuntu/Debian**: `sudo apt-get install grep` +- **macOS**: grep is pre-installed +- **Windows**: Use WSL or install grep via Git Bash/MSYS2 + +#### No Results Found +```python +# Check if your query exists in the raw data +results = searcher.search("your_query", use_grep=True) +if not results: + print("No exact matches found. Try:") + print("1. Check spelling and case") + print("2. Use partial terms") + print("3. Switch to semantic search") +``` + +## Complete Example + +```python +#!/usr/bin/env python3 +""" +Grep Search Example +Demonstrates grep search for exact text matching. +""" + +from leann.api import LeannSearcher + +def demonstrate_grep_search(): + # Initialize searcher + searcher = LeannSearcher("my_index") + + print("=== Function Search ===") + functions = searcher.search("def __init__", use_grep=True, top_k=5) + for i, result in enumerate(functions, 1): + print(f"{i}. Score: {result.score}") + print(f" Preview: {result.text[:60]}...") + print() + + print("=== Error Search ===") + errors = searcher.search("FileNotFoundError", use_grep=True, top_k=3) + for result in errors: + print(f"Content: {result.text.strip()}") + print("-" * 40) + +if __name__ == "__main__": + demonstrate_grep_search() +``` diff --git a/docs/issue-proposals/smart-embedding-default.md b/docs/issue-proposals/smart-embedding-default.md new file mode 100644 index 0000000..41dffa5 --- /dev/null +++ b/docs/issue-proposals/smart-embedding-default.md @@ -0,0 +1,41 @@ +# Smart default embedding model based on platform and corpus size + +## Summary + +Propose platform- and corpus-aware default embedding model selection for `leann build` when `--embedding-model` is not explicitly specified. This would improve out-of-the-box experience for different deployment scenarios (macOS CPU, NVIDIA GPU, etc.) without changing behavior when users pass an explicit model. + +## Motivation + +- **Current default**: `facebook/contriever` (~420MB, 768 dim) β€” heavy for CPU-only builds on large corpora +- **macOS users** often hit slow builds on 20K+ chunks; lighter models like `all-MiniLM-L6-v2` (~90MB) are much faster +- **NVIDIA GPU users** can leverage stronger models; smaller corpora benefit from quality (e.g. Qwen3-Embedding-0.6B), larger ones from balanced models (e.g. bge-base-en-v1.5) + +## Proposed logic + +| Platform | Chunk count | Default model | +|----------|-------------|---------------| +| **macOS** | β‰₯ 20,000 | `sentence-transformers/all-MiniLM-L6-v2` | +| **macOS** | < 20,000 | `intfloat/e5-small-v2` | +| **NVIDIA GPU** | < 5,000 | `Qwen/Qwen3-Embedding-0.6B` | +| **NVIDIA GPU** | β‰₯ 5,000 | `BAAI/bge-base-en-v1.5` | +| **Other** | any | `facebook/contriever` (unchanged) | + +## Implementation notes + +1. **Platform detection**: `torch.cuda.is_available()` for NVIDIA; `sys.platform == "darwin"` for macOS +2. **Chunk count**: Known only after loading/chunking; may need to either: + - Do a lightweight pre-scan (e.g. file count Γ— rough chunks per file), or + - Defer default choice until after first chunking pass (and cache for incremental) +3. **Explicit override**: If user passes `--embedding-model`, always use it; this logic applies only when the flag is omitted + +## Model references + +- `sentence-transformers/all-MiniLM-L6-v2`: ~90MB, 384 dim, fast on CPU +- `intfloat/e5-small-v2`: ~90MB, 384 dim +- `Qwen/Qwen3-Embedding-0.6B`: 0.6B params, 1024 dim, strong retrieval +- `BAAI/bge-base-en-v1.5`: ~110M params, 768 dim, good MTEB scores + +## Open questions + +- Should we add a `--embedding-model auto` to explicitly opt into this logic? +- Pre-scan vs post-chunk decision: trade-off between accuracy and implementation complexity diff --git a/docs/metadata_filtering.md b/docs/metadata_filtering.md new file mode 100644 index 0000000..1a267e1 --- /dev/null +++ b/docs/metadata_filtering.md @@ -0,0 +1,300 @@ +# LEANN Metadata Filtering Usage Guide + +## Overview + +Leann possesses metadata filtering capabilities that allow you to filter search results based on arbitrary metadata fields set during chunking. This feature enables use cases like spoiler-free book search, document filtering by date/type, code search by file type, and potentially much more. + +## Basic Usage + +### Adding Metadata to Your Documents + +When building your index, add metadata to each text chunk: + +```python +from leann.api import LeannBuilder + +builder = LeannBuilder("hnsw") + +# Add text with metadata +builder.add_text( + text="Chapter 1: Alice falls down the rabbit hole", + metadata={ + "chapter": 1, + "character": "Alice", + "themes": ["adventure", "curiosity"], + "word_count": 150 + } +) + +builder.build_index("alice_in_wonderland_index") +``` + +### Searching with Metadata Filters + +Use the `metadata_filters` parameter in search calls: + +```python +from leann.api import LeannSearcher + +searcher = LeannSearcher("alice_in_wonderland_index") + +# Search with filters +results = searcher.search( + query="What happens to Alice?", + top_k=10, + metadata_filters={ + "chapter": {"<=": 5}, # Only chapters 1-5 + "spoiler_level": {"!=": "high"} # No high spoilers + } +) +``` + +## Filter Syntax + +### Basic Structure + +```python +metadata_filters = { + "field_name": {"operator": value}, + "another_field": {"operator": value} +} +``` + +### Supported Operators + +#### Comparison Operators +- `"=="`: Equal to +- `"!="`: Not equal to +- `"<"`: Less than +- `"<="`: Less than or equal +- `">"`: Greater than +- `">="`: Greater than or equal + +```python +# Examples +{"chapter": {"==": 1}} # Exactly chapter 1 +{"page": {">": 100}} # Pages after 100 +{"rating": {">=": 4.0}} # Rating 4.0 or higher +{"word_count": {"<": 500}} # Short passages +``` + +#### Membership Operators +- `"in"`: Value is in list +- `"not_in"`: Value is not in list + +```python +# Examples +{"character": {"in": ["Alice", "Bob"]}} # Alice OR Bob +{"genre": {"not_in": ["horror", "thriller"]}} # Exclude genres +{"tags": {"in": ["fiction", "adventure"]}} # Any of these tags +``` + +#### String Operators +- `"contains"`: String contains substring +- `"starts_with"`: String starts with prefix +- `"ends_with"`: String ends with suffix + +```python +# Examples +{"title": {"contains": "alice"}} # Title contains "alice" +{"filename": {"ends_with": ".py"}} # Python files +{"author": {"starts_with": "Dr."}} # Authors with "Dr." prefix +``` + +#### Boolean Operators +- `"is_true"`: Field is truthy +- `"is_false"`: Field is falsy + +```python +# Examples +{"is_published": {"is_true": True}} # Published content +{"is_draft": {"is_false": False}} # Not drafts +``` + +### Multiple Operators on Same Field + +You can apply multiple operators to the same field (AND logic): + +```python +metadata_filters = { + "word_count": { + ">=": 100, # At least 100 words + "<=": 500 # At most 500 words + } +} +``` + +### Compound Filters + +Multiple fields are combined with AND logic: + +```python +metadata_filters = { + "chapter": {"<=": 10}, # Up to chapter 10 + "character": {"==": "Alice"}, # About Alice + "spoiler_level": {"!=": "high"} # No major spoilers +} +``` + +## Use Case Examples + +### 1. Spoiler-Free Book Search + +```python +# Reader has only read up to chapter 5 +def search_spoiler_free(query, max_chapter): + return searcher.search( + query=query, + metadata_filters={ + "chapter": {"<=": max_chapter}, + "spoiler_level": {"in": ["none", "low"]} + } + ) + +results = search_spoiler_free("What happens to Alice?", max_chapter=5) +``` + +### 2. Document Management by Date + +```python +# Find recent documents +recent_docs = searcher.search( + query="project updates", + metadata_filters={ + "date": {">=": "2024-01-01"}, + "document_type": {"==": "report"} + } +) +``` + +### 3. Code Search by File Type + +```python +# Search only Python files +python_code = searcher.search( + query="authentication function", + metadata_filters={ + "file_extension": {"==": ".py"}, + "lines_of_code": {"<": 100} + } +) +``` + +### 4. Content Filtering by Audience + +```python +# Age-appropriate content +family_content = searcher.search( + query="adventure stories", + metadata_filters={ + "age_rating": {"in": ["G", "PG"]}, + "content_warnings": {"not_in": ["violence", "adult_themes"]} + } +) +``` + +### 5. Multi-Book Series Management + +```python +# Search across first 3 books only +early_series = searcher.search( + query="character development", + metadata_filters={ + "series": {"==": "Harry Potter"}, + "book_number": {"<=": 3} + } +) +``` + +## Running the Example + +You can see metadata filtering in action with our spoiler-free book RAG example: + +```bash +# Don't forget to set up the environment +uv venv +source .venv/bin/activate + +# Set your OpenAI API key (required for embeddings, but you can update the example locally and use ollama instead) +export OPENAI_API_KEY="your-api-key-here" + +# Run the spoiler-free book RAG example +uv run examples/spoiler_free_book_rag.py +``` + +This example demonstrates: +- Building an index with metadata (chapter numbers, characters, themes, locations) +- Searching with filters to avoid spoilers (e.g., only show results up to chapter 5) +- Different scenarios for readers at various points in the book + +The example uses Alice's Adventures in Wonderland as sample data and shows how you can search for information without revealing plot points from later chapters. + +## Advanced Patterns + +### Custom Chunking with metadata + +```python +def chunk_book_with_metadata(book_text, book_info): + chunks = [] + + for chapter_num, chapter_text in parse_chapters(book_text): + # Extract entities, themes, etc. + characters = extract_characters(chapter_text) + themes = classify_themes(chapter_text) + spoiler_level = assess_spoiler_level(chapter_text, chapter_num) + + # Create chunks with rich metadata + for paragraph in split_paragraphs(chapter_text): + chunks.append({ + "text": paragraph, + "metadata": { + "book_title": book_info["title"], + "chapter": chapter_num, + "characters": characters, + "themes": themes, + "spoiler_level": spoiler_level, + "word_count": len(paragraph.split()), + "reading_level": calculate_reading_level(paragraph) + } + }) + + return chunks +``` + +## Performance Considerations + +### Efficient Filtering Strategies + +1. **Post-search filtering**: Applies filters after vector search, which should be efficient for typical result sets (10-100 results). + +2. **Metadata design**: Keep metadata fields simple and avoid deeply nested structures. + +### Best Practices + +1. **Consistent metadata schema**: Use consistent field names and value types across your documents. + +2. **Reasonable metadata size**: Keep metadata reasonably sized to avoid storage overhead. + +3. **Type consistency**: Use consistent data types for the same fields (e.g., always integers for chapter numbers). + +4. **Index multiple granularities**: Consider chunking at different levels (paragraph, section, chapter) with appropriate metadata. + +### Adding Metadata to Existing Indices + +To add metadata filtering to existing indices, you'll need to rebuild them with metadata: + +```python +# Read existing passages and add metadata +def add_metadata_to_existing_chunks(chunks): + for chunk in chunks: + # Extract or assign metadata based on content + chunk["metadata"] = extract_metadata(chunk["text"]) + return chunks + +# Rebuild index with metadata +enhanced_chunks = add_metadata_to_existing_chunks(existing_chunks) +builder = LeannBuilder("hnsw") +for chunk in enhanced_chunks: + builder.add_text(chunk["text"], chunk["metadata"]) +builder.build_index("enhanced_index") +``` diff --git a/docs/normalized_embeddings.md b/docs/normalized_embeddings.md new file mode 100644 index 0000000..e873489 --- /dev/null +++ b/docs/normalized_embeddings.md @@ -0,0 +1,75 @@ +# Normalized Embeddings Support in LEANN + +LEANN now automatically detects normalized embedding models and sets the appropriate distance metric for optimal performance. + +## What are Normalized Embeddings? + +Normalized embeddings are vectors with L2 norm = 1 (unit vectors). These embeddings are optimized for cosine similarity rather than Maximum Inner Product Search (MIPS). + +## Automatic Detection + +When you create a `LeannBuilder` instance with a normalized embedding model, LEANN will: + +1. **Automatically set `distance_metric="cosine"`** if not specified +2. **Show a warning** if you manually specify a different distance metric +3. **Provide optimal search performance** with the correct metric + +## Supported Normalized Embedding Models + +### OpenAI +All OpenAI text embedding models are normalized: +- `text-embedding-ada-002` +- `text-embedding-3-small` +- `text-embedding-3-large` + +### Voyage AI +All Voyage AI embedding models are normalized: +- `voyage-2` +- `voyage-3` +- `voyage-large-2` +- `voyage-multilingual-2` +- `voyage-code-2` + +### Cohere +All Cohere embedding models are normalized: +- `embed-english-v3.0` +- `embed-multilingual-v3.0` +- `embed-english-light-v3.0` +- `embed-multilingual-light-v3.0` + +## Example Usage + +```python +from leann.api import LeannBuilder + +# Automatic detection - will use cosine distance +builder = LeannBuilder( + backend_name="hnsw", + embedding_model="text-embedding-3-small", + embedding_mode="openai" +) +# Warning: Detected normalized embeddings model 'text-embedding-3-small'... +# Automatically setting distance_metric='cosine' + +# Manual override (not recommended) +builder = LeannBuilder( + backend_name="hnsw", + embedding_model="text-embedding-3-small", + embedding_mode="openai", + distance_metric="mips" # Will show warning +) +# Warning: Using 'mips' distance metric with normalized embeddings... +``` + +## Non-Normalized Embeddings + +Models like `facebook/contriever` and other sentence-transformers models that are not normalized will continue to use MIPS by default, which is optimal for them. + +## Why This Matters + +Using the wrong distance metric with normalized embeddings can lead to: +- **Poor search quality** due to HNSW's early termination with narrow score ranges +- **Incorrect ranking** of search results +- **Suboptimal performance** compared to using the correct metric + +For more details on why this happens, see our analysis in the [embedding detection code](../packages/leann-core/src/leann/api.py) which automatically handles normalized embeddings and MIPS distance metric issues. diff --git a/docs/openclaw-setup.md b/docs/openclaw-setup.md new file mode 100644 index 0000000..479089e --- /dev/null +++ b/docs/openclaw-setup.md @@ -0,0 +1,166 @@ +# OpenClaw + LEANN Setup Guide + +Two ways to connect LEANN to your OpenClaw agent: **MCP server** (recommended) +or **ClawHub skill**. + +--- + +## Option A: MCP Server (Recommended) + +OpenClaw natively supports MCP tools. LEANN ships an MCP server that exposes +`leann_search` and `leann_list` as tools your agent can call directly. + +### 1. Install LEANN + +```bash +pip install leann-core +# or +uv tool install leann-core --with leann +``` + +### 2. Build an index on your memory files + +Using Ollama embeddings (recommended if you already run Ollama): + +```bash +leann build openclaw-memory \ + --docs ~/.openclaw/workspace/MEMORY.md ~/.openclaw/workspace/memory/ \ + --embedding-mode ollama \ + --embedding-model nomic-embed-text +``` + +Or using local sentence-transformers (no Ollama required): + +```bash +leann build openclaw-memory \ + --docs ~/.openclaw/workspace/MEMORY.md ~/.openclaw/workspace/memory/ \ + --embedding-mode sentence-transformers \ + --embedding-model all-MiniLM-L6-v2 +``` + +Add extra directories if you have them: + +```bash +leann build openclaw-memory \ + --docs ~/.openclaw/workspace/MEMORY.md \ + ~/.openclaw/workspace/memory/ \ + ~/Documents/notes/ \ + --embedding-mode ollama \ + --embedding-model nomic-embed-text +``` + +### 3. Register the MCP server with OpenClaw + +Add to `~/.openclaw/openclaw.json`: + +```json5 +{ + // ... your existing config ... + "mcpServers": { + "leann": { + "command": "leann_mcp", + "args": [], + "env": {} + } + } +} +``` + +### 4. Use it + +Ask your agent: +- "Search my memories for database decisions" +- "What did we decide about the API design?" +- "Find my notes on deployment" + +The agent will call `leann_search` via MCP and return structured results. + +### 5. Keep the index fresh + +```bash +# Re-run build (idempotent β€” only processes changed files) +leann build openclaw-memory \ + --docs ~/.openclaw/workspace/MEMORY.md ~/.openclaw/workspace/memory/ + +# Or use watch mode for continuous auto-sync +leann watch openclaw-memory --interval 30 +``` + +--- + +## Option B: ClawHub Skill + +If you prefer the skill-based approach: + +```bash +clawhub install leann-team/leann-memory +``` + +Or copy `skills/leann-memory/` from this repo to +`~/.openclaw/workspace/skills/leann-memory/`. + +The skill tells your agent how to call `leann search` via shell commands. +Setup steps (install + build index) are the same as above. + +--- + +## Important: Ollama Configuration + +If you use Ollama as your OpenClaw model provider, make sure your +`~/.openclaw/openclaw.json` uses the **native Ollama API** β€” not the +OpenAI-compatible endpoint: + +```json5 +{ + "models": { + "providers": { + "ollama": { + "baseUrl": "http://127.0.0.1:11434", // no /v1 suffix + "apiKey": "ollama-local", + "api": "ollama" // NOT "openai-completions" or "openai-responses" + } + } + } +} +``` + +Using `"openai-completions"` or `"openai-responses"` silently breaks tool +calling β€” the model outputs tool calls as plain text instead of structured +`tool_calls`. See [astral-sh/ty#21243](https://github.com/openclaw/openclaw/issues/21243). + +--- + +## Storage Comparison + +| Scenario | Default memory-core | LEANN | +|---|---|---| +| 1 year daily logs (~12K chunks) | ~23 MB | **~0.7 MB** | +| + session transcripts (~100K chunks) | ~190 MB | **~6 MB** | +| + 10 GB indexed documents (~500K chunks) | ~950 MB | **~30 MB** | + +All numbers assume 384-dimensional embeddings (all-MiniLM-L6-v2 or +nomic-embed-text). + +--- + +## Troubleshooting + +**"leann: command not found"** +Ensure LEANN is on your PATH. If installed via `uv tool install`, run +`uv tool update-shell` and restart your terminal. + +**"Index not found"** +Run `leann list` to see available indexes. Build one first with `leann build`. + +**Slow first search** +The first query loads the embedding model (~90 MB). Subsequent queries reuse the +warm daemon and are fast (~0.5s). Use `leann warmup openclaw-memory` to +pre-warm. + +**Memory files changed but search results are stale** +Re-run `leann build openclaw-memory --docs ...` β€” it detects changes +automatically and only re-indexes what changed. + +**Agent doesn't use LEANN tools** +Make sure your Ollama model supports tool calling (e.g. `qwen3:8b` or larger). +Smaller models like `qwen3:4b` may not reliably invoke tools. diff --git a/docs/react_agent.md b/docs/react_agent.md new file mode 100644 index 0000000..671b362 --- /dev/null +++ b/docs/react_agent.md @@ -0,0 +1,235 @@ +# LEANN ReAct Agent Guide + +## Overview + +The LEANN ReAct (Reasoning + Acting) Agent enables **multiturn retrieval and reasoning** for complex queries that require multiple search iterations. Unlike the standard `leann ask` command which performs a single search and answer, the ReAct agent can: + +- **Reason** about what information is needed +- **Act** by performing targeted searches +- **Observe** the results and iterate +- **Answer** based on all gathered context + +This is particularly useful for questions that require: +- Multiple pieces of information from different parts of your index +- Iterative refinement of search queries +- Complex reasoning that builds on previous findings + +## How It Works + +The ReAct agent follows a **Thought β†’ Action β†’ Observation** loop: + +1. **Thought**: The agent analyzes the question and determines what information is needed +2. **Action**: The agent performs a search query based on its reasoning +3. **Observation**: The agent reviews the search results +4. **Iteration**: The process repeats until the agent has enough information or reaches the maximum iteration limit +5. **Final Answer**: The agent synthesizes all gathered information into a comprehensive answer + +## Basic Usage + +### Command Line + +```bash +# Basic usage +leann react "your question" + +# With custom LLM settings +leann react my-index "What are the main features discussed?" \ + --llm ollama \ + --model qwen3:8b \ + --max-iterations 5 \ + --top-k 5 +``` + +### Command Options + +- `index_name`: Name of the LEANN index to search +- `query`: The question to research +- `--llm`: LLM provider (`ollama`, `openai`, `anthropic`, `hf`, `simulated`) - default: `ollama` +- `--model`: Model name (default: `qwen3:8b`) +- `--host`: Override Ollama-compatible host (defaults to `LEANN_OLLAMA_HOST` or `OLLAMA_HOST`) +- `--top-k`: Number of results per search iteration (default: `5`) +- `--max-iterations`: Maximum number of search iterations (default: `5`) +- `--api-base`: Base URL for OpenAI-compatible APIs +- `--api-key`: API key for cloud LLM providers + +### Python API + +```python +from leann import create_react_agent, LeannSearcher + +# Create a searcher +searcher = LeannSearcher(index_path="path/to/index.leann") + +# Create the ReAct agent +agent = create_react_agent( + index_path="path/to/index.leann", + llm_config={ + "type": "ollama", + "model": "qwen3:8b", + "host": "http://localhost:11434" # optional + }, + max_iterations=5 +) + +# Run the agent +answer = agent.run("What are the main topics covered in the documentation?", top_k=5) +print(answer) + +# Access search history +if agent.search_history: + print(f"\nSearch History ({len(agent.search_history)} iterations):") + for entry in agent.search_history: + print(f" {entry['iteration']}. {entry['action']} ({entry['results_count']} results)") +``` + +## Example Use Cases + +### 1. Multi-faceted Questions + +```bash +# Questions that need information from multiple sources +leann react docs-index "What are the differences between HNSW and DiskANN backends, and when should I use each?" +``` + +The agent will: +- First search for "HNSW backend features" +- Then search for "DiskANN backend features" +- Compare the results +- Provide a comprehensive answer + +### 2. Iterative Research + +```bash +# Questions requiring multiple search iterations +leann react codebase-index "How does the embedding computation work and what optimizations are used?" +``` + +The agent will: +- Search for "embedding computation" +- Based on results, search for "embedding optimizations" +- Refine queries based on findings +- Synthesize the information + +### 3. Complex Reasoning + +```bash +# Questions that require building understanding +leann react research-index "What are the performance characteristics of different indexing strategies?" +``` + +## Comparison: `leann ask` vs `leann react` + +| Feature | `leann ask` | `leann react` | +|---------|-------------|---------------| +| **Search iterations** | Single search | Multiple iterations | +| **Query refinement** | No | Yes, based on observations | +| **Use case** | Simple Q&A | Complex, multi-faceted questions | +| **Speed** | Faster | Slower (multiple searches) | +| **Reasoning** | Direct answer | Iterative reasoning | + +### When to Use Each + +**Use `leann ask` when:** +- You have a straightforward question +- A single search should provide enough context +- You want a quick answer + +**Use `leann react` when:** +- Your question requires information from multiple sources +- You need the agent to explore and refine its understanding +- The answer requires synthesizing multiple pieces of information + +## Advanced Configuration + +### Custom LLM Providers + +```bash +# Using OpenAI +leann react my-index "question" \ + --llm openai \ + --model gpt-4 \ + --api-base https://api.openai.com/v1 \ + --api-key $OPENAI_API_KEY + +# Using Anthropic +leann react my-index "question" \ + --llm anthropic \ + --model claude-3-opus-20240229 \ + --api-key $ANTHROPIC_API_KEY +``` + +### Adjusting Search Parameters + +```bash +# More results per iteration +leann react my-index "question" --top-k 10 + +# More iterations for complex questions +leann react my-index "question" --max-iterations 10 +``` + +## Understanding the Output + +When you run `leann react`, you'll see: + +1. **Question**: The original question being researched +2. **Iteration logs**: Each search action and its results +3. **Final Answer**: The synthesized answer based on all iterations +4. **Search History**: Summary of all search iterations performed + +Example output: + +``` +πŸ€– Starting ReAct agent with index 'my-index'... +Using qwen3:8b (ollama) + +πŸ” Question: What are the main features of LEANN? + +πŸ” Action: search("LEANN features") +[Result 1] (Score: 0.923) +LEANN is a vector database that saves 97% storage... + +πŸ” Action: search("LEANN storage optimization") +[Result 1] (Score: 0.891) +LEANN uses compact storage and recomputation... + +βœ… Final Answer: +LEANN is a vector database with several key features: +1. 97% storage savings compared to traditional vector databases +2. Compact storage with recomputation capabilities +3. Support for multiple backends (HNSW and DiskANN) +... + +πŸ“Š Search History (2 iterations): + 1. search("LEANN features") (5 results) + 2. search("LEANN storage optimization") (5 results) +``` + +## Tips for Best Results + +1. **Be specific**: Clear, specific questions work better than vague ones +2. **Adjust iterations**: Complex questions may need more iterations (increase `--max-iterations`) +3. **Monitor history**: Check the search history to understand the agent's reasoning +4. **Use appropriate models**: Larger models generally provide better reasoning, but are slower +5. **Index quality**: Ensure your index is well-built with relevant content + +## Limitations + +- **Speed**: Multiple iterations make ReAct slower than single-search queries +- **Cost**: More LLM calls mean higher costs for cloud providers +- **Complexity**: Very complex questions may still require human review +- **Model dependency**: Reasoning quality depends on the LLM's capabilities + +## Future Enhancements + +This is the first implementation (1/N) of Deep-Research integration. Future enhancements may include: +- Web search integration for external information +- More sophisticated reasoning strategies +- Parallel search execution +- Better query optimization + +## Related Documentation + +- [Basic Usage Guide](../README.md) +- [CLI Reference](configuration-guide.md) +- [Embedding Models](normalized_embeddings.md) diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..2f87114 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,67 @@ +# LEANN Roadmap + +LEANN aims to be a **personal knowledge layer** β€” not just a storage-efficient vector database, but a unified, always-up-to-date knowledge base that runs entirely on your own machine. It connects your code, images, and personal data (documents, emails, browser history, chats) into a single multimodal search interface. + +Contributions and feedback are welcome. Join our [Slack](https://join.slack.com/t/leann-e2u9779/shared_invite/zt-3ol2ww9ic-Eg_kB8omwe6xmYVd0epr4Q) to discuss. + +--- + +## Completed + +- [x] HNSW backend integration +- [x] DiskANN backend with MIPS/L2/Cosine support +- [x] Real-time embedding pipeline +- [x] Memory-efficient graph pruning +- [x] IVF backend with incremental add/remove (#231, #89, #141) +- [x] Merkle tree file-change detection β€” `leann watch` (#41) + +--- + +## P0 β€” Core (Q1 2026) + +### LEANN MCP β€” The Best Code Retrieval MCP + +The primary near-term goal: make LEANN the go-to MCP server for code-aware AI assistants. This means **dynamic updates** (your index stays current as you edit code), **rich code context** (AST-aware chunking that understands functions, classes, and modules β€” not just raw text), and a **dead-simple interface** (one command to build, automatic incremental updates, zero configuration for common setups). + +- [x] IVF backend β€” incremental add/remove without full rebuild (#231, #89, #141) +- [x] Merkle tree file-change detection β€” `leann watch` for automatic re-indexing on file changes (#41) +- [ ] Cold start optimization β€” faster first-build experience for new users (#166, #177) +- [ ] Live index updates β€” push index changes as files are saved, not just on rebuild +- [ ] Smarter code context β€” cross-file symbol resolution, call graph awareness, import tracking + +### Search Quality + +- [ ] Hybrid search β€” combine dense vector retrieval with sparse keyword matching (BM25) for better recall on exact identifiers and variable names (#233, #90) + +### Documentation + +- [ ] ReadTheDocs β€” hosted documentation site (#234) +- [ ] Benchmarks β€” recall@k, latency, and storage comparisons across backends + +--- + +## P1 (Q2 2026) + +### Multimodal + +- [ ] Video retrieval (#160) +- [ ] CLIP support β€” image-text cross-modal search (#94) +- [ ] OCR β€” extract text from images/scanned documents (#158) + +### Platform & Distribution + +- [ ] Windows support (#14) +- [ ] Web UI (#229) + +### Applications & Integrations + +- [ ] Agent + Deep research (#104) +- [ ] Local Cursor β€” local model + local retrieval for code assistance (#47) +- [ ] LlamaIndex integration (#217) +- [ ] Obsidian support (#96) + +--- + +## Contributing + +If you're interested in working on any of the items above, please reach out to [@yichuan-w](https://github.com/yichuan-w) or [@andylizf](https://github.com/andylizf). See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contributor workflow. diff --git a/docs/slack-setup-guide.md b/docs/slack-setup-guide.md new file mode 100644 index 0000000..ee83243 --- /dev/null +++ b/docs/slack-setup-guide.md @@ -0,0 +1,395 @@ +# Slack Integration Setup Guide + +This guide provides step-by-step instructions for setting up Slack integration with LEANN. + +## Overview + +LEANN's Slack integration uses MCP (Model Context Protocol) servers to fetch and index your Slack messages for RAG (Retrieval-Augmented Generation). This allows you to search through your Slack conversations using natural language queries. + +## Prerequisites + +1. **Slack Workspace Access**: You need admin or owner permissions in your Slack workspace to create apps and configure OAuth tokens. + +2. **Slack MCP Server**: Install a Slack MCP server (e.g., `slack-mcp-server` via npm) + +3. **LEANN**: Ensure you have LEANN installed and working + +## Step 1: Create a Slack App + +### 1.1 Go to Slack API Dashboard + +1. Visit [https://api.slack.com/apps](https://api.slack.com/apps) +2. Click **"Create New App"** +3. Choose **"From scratch"** +4. Enter your app name (e.g., "LEANN Slack Integration") +5. Select your workspace +6. Click **"Create App"** + +### 1.2 Configure App Permissions + +#### Token Scopes + +1. In your app dashboard, go to **"OAuth & Permissions"** in the left sidebar +2. Scroll down to **"Scopes"** section +3. Under **"Bot Token Scopes & OAuth Scope"**, click **"Add an OAuth Scope"** +4. Add the following scopes: + - `channels:read` - Read public channel information + - `channels:history` - Read messages in public channels + - `groups:read` - Read private channel information + - `groups:history` - Read messages in private channels + - `im:read` - Read direct message information + - `im:history` - Read direct messages + - `mpim:read` - Read group direct message information + - `mpim:history` - Read group direct messages + - `users:read` - Read user information + - `team:read` - Read workspace information + +#### App-Level Tokens (Optional) + +Some MCP servers may require app-level tokens: + +1. Go to **"Basic Information"** in the left sidebar +2. Scroll down to **"App-Level Tokens"** +3. Click **"Generate Token and Scopes"** +4. Enter a name (e.g., "LEANN Integration") +5. Add the `connections:write` scope +6. Click **"Generate"** +7. Copy the token (starts with `xapp-`) + +### 1.3 Install App to Workspace + +1. Go to **"OAuth & Permissions"** in the left sidebar +2. Click **"Install to Workspace"** +3. Review the permissions and click **"Allow"** +4. Copy the **"Bot User OAuth Token"** (starts with `xoxb-`) +5. Copy the **"User OAuth Token"** (starts with `xoxp-`) + +## Step 2: Install Slack MCP Server + +### Option A: Using npm (Recommended) + +```bash +# Install globally +npm install -g slack-mcp-server + +# Or install locally +npm install slack-mcp-server +``` + +### Option B: Using npx (No installation required) + +```bash +# Use directly without installation +npx slack-mcp-server +``` + +## Step 3: Install and Configure Ollama (for Real LLM Responses) + +### 3.1 Install Ollama + +```bash +# Install Ollama using Homebrew (macOS) +brew install ollama + +# Or download from https://ollama.ai/ +``` + +### 3.2 Start Ollama Service + +```bash +# Start Ollama as a service +brew services start ollama + +# Or start manually +ollama serve +``` + +### 3.3 Pull a Model + +```bash +# Pull a lightweight model for testing +ollama pull llama3.2:1b + +# Verify the model is available +ollama list +``` + +## Step 4: Configure Environment Variables + +Create a `.env` file or set environment variables: + +```bash +# Required: User OAuth Token +SLACK_OAUTH_TOKEN=xoxp-your-user-oauth-token-here + +# Optional: App-Level Token (if your MCP server requires it) +SLACK_APP_TOKEN=xapp-your-app-token-here + +# Optional: Workspace-specific settings +SLACK_WORKSPACE_ID=T1234567890 # Your workspace ID (optional) +``` + +## Step 5: Test the Setup + +### 5.1 Test MCP Server Connection + +```bash +python -m apps.slack_rag \ + --mcp-server "slack-mcp-server" \ + --test-connection \ + --workspace-name "Your Workspace Name" +``` + +This will test the connection and list available tools without indexing any data. + +### 5.2 Index a Specific Channel + +```bash +python -m apps.slack_rag \ + --mcp-server "slack-mcp-server" \ + --workspace-name "Your Workspace Name" \ + --channels general \ + --query "What did we discuss about the project?" +``` + +### 5.3 Real RAG Query Examples + +This section demonstrates successful Slack RAG integration queries against the Sky Lab Computing workspace's "random" channel. The system successfully retrieves actual conversation messages and performs semantic search with high relevance scores, including finding specific research paper announcements and technical discussions. + +### Example 1: Advisor Models Query + +**Query:** "train black-box models to adopt to your personal data" + +This query demonstrates the system's ability to find specific research announcements about training black-box models for personal data adaptation. + +![Advisor Models Query - Command Setup](videos/slack_integration_1.1.png) + +![Advisor Models Query - Search Results](videos/slack_integration_1.2.png) + +![Advisor Models Query - LLM Response](videos/slack_integration_1.3.png) + +### Example 2: Barbarians at the Gate Query + +**Query:** "AI-driven research systems ADRS" + +This query demonstrates the system's ability to find specific research announcements about AI-driven research systems and algorithm discovery. + +![Barbarians Query - Command Setup](videos/slack_integration_2.1.png) + +![Barbarians Query - Search Results](videos/slack_integration_2.2.png) + +![Barbarians Query - LLM Response](videos/slack_integration_2.3.png) + +### Prerequisites + +- Bot is installed in the Sky Lab Computing workspace and invited to the target channel (run `/invite @YourBotName` in the channel if needed) +- Bot token available and exported in the same terminal session + +### Commands + +1) Set the workspace token for this shell + +```bash +export SLACK_MCP_XOXP_TOKEN="xoxp-***-redacted-***" +``` + +2) Run queries against the "random" channel by channel ID (C0GN5BX0F) + +**Advisor Models Query:** +```bash +python -m apps.slack_rag \ + --mcp-server "slack-mcp-server" \ + --workspace-name "Sky Lab Computing" \ + --channels C0GN5BX0F \ + --max-messages-per-channel 100000 \ + --query "train black-box models to adopt to your personal data" \ + --llm ollama \ + --llm-model "llama3.2:1b" \ + --llm-host "http://localhost:11434" \ + --no-concatenate-conversations +``` + +**Barbarians at the Gate Query:** +```bash +python -m apps.slack_rag \ + --mcp-server "slack-mcp-server" \ + --workspace-name "Sky Lab Computing" \ + --channels C0GN5BX0F \ + --max-messages-per-channel 100000 \ + --query "AI-driven research systems ADRS" \ + --llm ollama \ + --llm-model "llama3.2:1b" \ + --llm-host "http://localhost:11434" \ + --no-concatenate-conversations +``` + +These examples demonstrate the system's ability to find and retrieve specific research announcements and technical discussions from the conversation history, showcasing the power of semantic search in Slack data. + +3) Optional: Ask a broader question + +```bash +python test_channel_by_id_or_name.py \ + --channel-id C0GN5BX0F \ + --workspace-name "Sky Lab Computing" \ + --query "What is LEANN about?" +``` + +Notes: +- If you see `not_in_channel`, invite the bot to the channel and re-run. +- If you see `channel_not_found`, confirm the channel ID and workspace. +- Deep search via server-side β€œsearch” tools may require additional Slack scopes; the example above performs client-side filtering over retrieved history. + +## Common Issues and Solutions + +### Issue 1: "users cache is not ready yet" Error + +**Problem**: You see this warning: +``` +WARNING - Failed to fetch messages from channel random: Failed to fetch messages: {'code': -32603, 'message': 'users cache is not ready yet, sync process is still running... please wait'} +``` + +**Solution**: This is a common timing issue. The LEANN integration now includes automatic retry logic: + +1. **Wait and Retry**: The system will automatically retry with exponential backoff (2s, 4s, 8s, etc.) +2. **Increase Retry Parameters**: If needed, you can customize retry behavior: + ```bash + python -m apps.slack_rag \ + --mcp-server "slack-mcp-server" \ + --max-retries 10 \ + --retry-delay 3.0 \ + --channels general \ + --query "Your query here" + ``` +3. **Keep MCP Server Running**: Start the MCP server separately and keep it running: + ```bash + # Terminal 1: Start MCP server + slack-mcp-server + + # Terminal 2: Run LEANN (it will connect to the running server) + python -m apps.slack_rag --mcp-server "slack-mcp-server" --channels general --query "test" + ``` + +### Issue 2: "No message fetching tool found" + +**Problem**: The MCP server doesn't have the expected tools. + +**Solution**: +1. Check if your MCP server is properly installed and configured +2. Verify your Slack tokens are correct +3. Try a different MCP server implementation +4. Check the MCP server documentation for required configuration + +### Issue 3: Permission Denied Errors + +**Problem**: You get permission errors when trying to access channels. + +**Solutions**: +1. **Check Bot Permissions**: Ensure your bot has been added to the channels you want to access +2. **Verify Token Scopes**: Make sure you have all required scopes configured +3. **Channel Access**: For private channels, the bot needs to be explicitly invited +4. **Workspace Permissions**: Ensure your Slack app has the necessary workspace permissions + +### Issue 4: Empty Results + +**Problem**: No messages are returned even though the channel has messages. + +**Solutions**: +1. **Check Channel Names**: Ensure channel names are correct (without the # symbol) +2. **Verify Bot Access**: Make sure the bot can access the channels +3. **Check Date Ranges**: Some MCP servers have limitations on message history +4. **Increase Message Limits**: Try increasing the message limit: + ```bash + python -m apps.slack_rag \ + --mcp-server "slack-mcp-server" \ + --channels general \ + --max-messages-per-channel 1000 \ + --query "test" + ``` + +## Advanced Configuration + +### Custom MCP Server Commands + +If you need to pass additional parameters to your MCP server: + +```bash +python -m apps.slack_rag \ + --mcp-server "slack-mcp-server --token-file /path/to/tokens.json" \ + --workspace-name "Your Workspace" \ + --channels general \ + --query "Your query" +``` + +### Multiple Workspaces + +To work with multiple Slack workspaces, you can: + +1. Create separate apps for each workspace +2. Use different environment variables +3. Run separate instances with different configurations + +### Performance Optimization + +For better performance with large workspaces: + +```bash +python -m apps.slack_rag \ + --mcp-server "slack-mcp-server" \ + --workspace-name "Your Workspace" \ + --max-messages-per-channel 500 \ + --no-concatenate-conversations \ + --query "Your query" +``` +--- + +## Troubleshooting Checklist + +- [ ] Slack app created with proper permissions +- [ ] Bot token (xoxb-) copied correctly +- [ ] App-level token (xapp-) created if needed +- [ ] MCP server installed and accessible +- [ ] Ollama installed and running (`brew services start ollama`) +- [ ] Ollama model pulled (`ollama pull llama3.2:1b`) +- [ ] Environment variables set correctly +- [ ] Bot invited to relevant channels +- [ ] Channel names specified without # symbol +- [ ] Sufficient retry attempts configured +- [ ] Network connectivity to Slack APIs + +## Getting Help + +If you continue to have issues: + +1. **Check Logs**: Look for detailed error messages in the console output +2. **Test MCP Server**: Use `--test-connection` to verify the MCP server is working +3. **Verify Tokens**: Double-check that your Slack tokens are valid and have the right scopes +4. **Check Ollama**: Ensure Ollama is running (`ollama serve`) and the model is available (`ollama list`) +5. **Community Support**: Reach out to the LEANN community for help + +## Example Commands + +### Basic Usage +```bash +# Test connection +python -m apps.slack_rag --mcp-server "slack-mcp-server" --test-connection + +# Index specific channels +python -m apps.slack_rag \ + --mcp-server "slack-mcp-server" \ + --workspace-name "My Company" \ + --channels general random \ + --query "What did we decide about the project timeline?" +``` + +### Advanced Usage +```bash +# With custom retry settings +python -m apps.slack_rag \ + --mcp-server "slack-mcp-server" \ + --workspace-name "My Company" \ + --channels general \ + --max-retries 10 \ + --retry-delay 5.0 \ + --max-messages-per-channel 2000 \ + --query "Show me all decisions made in the last month" +``` diff --git a/docs/ultimate_goal.md b/docs/ultimate_goal.md new file mode 100644 index 0000000..8d2d7bd --- /dev/null +++ b/docs/ultimate_goal.md @@ -0,0 +1,41 @@ +# LEANN β€” Long-Term Vision + +## The Best Personal Data Management Platform + +LEANN's ultimate goal is to be the **unified personal knowledge layer** that lives on your machine. Not a cloud service. Not a SaaS product. A local-first system that understands everything you've ever worked on β€” code, documents, emails, chats, browser history, images β€” and makes it all instantly searchable and usable. + +### 1. Continuous Learning from Your Context + +LEANN should get smarter over time by continuously ingesting and indexing your data as you produce it: + +- **Always-on indexing**: Watch your filesystem, email, browser, and chat sources. Incrementally update indexes as new data arrives β€” no manual rebuilds. +- **Cross-source connections**: Surface relationships across data sources. A Slack conversation about a bug should link to the relevant code change, the related email thread, and the document that describes the feature. +- **Temporal awareness**: Understand when things happened. "What was I working on last Tuesday?" should be a searchable query. +- **Personalized ranking**: Learn from your search patterns and usage to rank results by relevance to *you*, not just semantic similarity. + +### 2. The Best MCP for Code Retrieval + +AI coding assistants are only as good as the context they receive. LEANN aims to be the best context provider: + +- **AST-aware chunking**: Understand code structure β€” functions, classes, modules β€” not just raw text blocks. Retrieve semantically meaningful units. +- **Dynamic index updates**: The index stays current as you edit. No stale results. No manual rebuilds. Save a file, and the index reflects the change within seconds. +- **Cross-file understanding**: Resolve symbols across files. When you search for a function, also surface its callers, its tests, and the types it depends on. +- **Dead-simple interface**: One command to build (`leann build`), automatic incremental updates, MCP server that any AI assistant can plug into with zero configuration. +- **Repository-scale search**: Handle monorepos and large codebases without breaking a sweat. The storage efficiency (97% reduction vs. traditional vector DBs) makes this feasible on a laptop. + +### 3. Multimodal Knowledge Base + +Text is just the beginning: + +- **Image search**: CLIP-based retrieval for screenshots, diagrams, photos. "Find the architecture diagram from last month." +- **Video retrieval**: Index video content β€” lectures, meetings, screen recordings β€” and search by what was said or shown. +- **OCR pipeline**: Extract and index text from scanned documents, handwritten notes, whiteboard photos. +- **Unified search**: One query searches across all modalities. The answer might be in a PDF, a screenshot, a code comment, or a Slack message. + +### 4. Platform for Personal AI + +Looking further ahead, LEANN becomes the memory and retrieval layer for personal AI agents: + +- **Agent memory**: AI agents that remember your preferences, past decisions, and project context across sessions. +- **Deep research**: Agents that can search your entire personal knowledge base to answer complex questions, synthesize information, and generate insights. +- **Proactive suggestions**: Surface relevant context before you ask for it β€” "You discussed this exact problem with a colleague 3 months ago, here's what you decided." diff --git a/docs/user-scripts.md b/docs/user-scripts.md new file mode 100644 index 0000000..d9b5fc8 --- /dev/null +++ b/docs/user-scripts.md @@ -0,0 +1,95 @@ +# Data Source Indexing Commands + +LEANN provides `index-*` CLI commands for indexing common personal data sources. Each command reads from a specific data source and builds a searchable LEANN index. + +## Available Commands + +| Command | Description | Platform | +|---------|-------------|----------| +| `leann index-browser [chrome\|brave]` | Browser history | macOS | +| `leann index-email` | Apple Mail | macOS | +| `leann index-calendar` | Apple Calendar | macOS | +| `leann index-imessage` | iMessage conversations | macOS | +| `leann index-wechat --export-dir ` | WeChat chat history | Any | +| `leann index-chatgpt --export-path ` | ChatGPT export | Any | +| `leann index-claude --export-path ` | Claude export | Any | + +## Common Options + +All `index-*` commands accept these options: + +```bash +--index-name NAME # Custom index name (each command has a sensible default) +--embedding-model MODEL # Embedding model (default: facebook/contriever) +--embedding-mode MODE # Backend: sentence-transformers, openai, mlx, ollama +--max-count N # Max items to index (default: 1000) +--no-recompute # Store full embeddings instead of using recomputation +``` + +## Examples + +### Index Chrome browser history + +```bash +leann index-browser chrome +leann index-browser brave --index-name brave_history +``` + +### Index Apple Mail + +```bash +leann index-email +``` + +### Index iMessage + +```bash +leann index-imessage +``` + +### Index Apple Calendar + +```bash +leann index-calendar +``` + +### Index ChatGPT or Claude exports + +```bash +# ChatGPT: export from https://chat.openai.com β†’ Settings β†’ Export data +leann index-chatgpt --export-path ~/Downloads/chatgpt-export.zip + +# Claude: export from https://claude.ai β†’ Settings β†’ Export data +leann index-claude --export-path ~/Downloads/claude-export.json +``` + +### Index WeChat + +```bash +# Requires exported JSON files from wechat-exporter +leann index-wechat --export-dir ~/wechat-export/ +``` + +## Daily Automation + +You can schedule indexing with cron for automatic daily updates: + +```bash +# Edit crontab +crontab -e + +# Add entries (runs at 2 AM daily): +0 2 * * * cd /path/to/LEANN && leann index-browser chrome +5 2 * * * cd /path/to/LEANN && leann index-email +10 2 * * * cd /path/to/LEANN && leann index-imessage +``` + +## Searching Indexed Data + +After indexing, search with the standard `leann search` command: + +```bash +leann search browser_history "github pull request review" +leann search email "meeting notes from last week" +leann search imessage "dinner plans" +``` diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/basic_demo.py b/examples/basic_demo.py new file mode 100644 index 0000000..05b2651 --- /dev/null +++ b/examples/basic_demo.py @@ -0,0 +1,88 @@ +""" +Simple demo showing basic leann usage +Run: uv run python examples/basic_demo.py +""" + +import argparse + +from leann import LeannBuilder, LeannChat, LeannSearcher + + +def main(): + parser = argparse.ArgumentParser( + description="Simple demo of Leann with selectable embedding models." + ) + parser.add_argument( + "--embedding_model", + type=str, + default="sentence-transformers/all-mpnet-base-v2", + help="The embedding model to use, e.g., 'sentence-transformers/all-mpnet-base-v2' or 'text-embedding-ada-002'.", + ) + args = parser.parse_args() + + print(f"=== Leann Simple Demo with {args.embedding_model} ===") + print() + + # Sample knowledge base + chunks = [ + "Machine learning is a subset of artificial intelligence that enables computers to learn without being explicitly programmed.", + "Deep learning uses neural networks with multiple layers to process data and make decisions.", + "Natural language processing helps computers understand and generate human language.", + "Computer vision enables machines to interpret and understand visual information from images and videos.", + "Reinforcement learning teaches agents to make decisions by receiving rewards or penalties for their actions.", + "Data science combines statistics, programming, and domain expertise to extract insights from data.", + "Big data refers to extremely large datasets that require special tools and techniques to process.", + "Cloud computing provides on-demand access to computing resources over the internet.", + ] + + print("1. Building index (no embeddings stored)...") + builder = LeannBuilder( + embedding_model=args.embedding_model, + backend_name="hnsw", + ) + for chunk in chunks: + builder.add_text(chunk) + builder.build_index("demo_knowledge.leann") + print() + + print("2. Searching with real-time embeddings...") + searcher = LeannSearcher("demo_knowledge.leann") + + queries = [ + "What is machine learning?", + "How does neural network work?", + "Tell me about data processing", + ] + + for query in queries: + print(f"Query: {query}") + results = searcher.search(query, top_k=2) + + for i, result in enumerate(results, 1): + print(f" {i}. Score: {result.score:.3f}") + print(f" Text: {result.text[:100]}...") + print() + + print("3. Interactive chat demo:") + print(" (Note: Requires OpenAI API key for real responses)") + + chat = LeannChat("demo_knowledge.leann") + + # Demo questions + demo_questions: list[str] = [ + "What is the difference between machine learning and deep learning?", + "How is data science related to big data?", + ] + + for question in demo_questions: + print(f" Q: {question}") + response = chat.ask(question) + print(f" A: {response}") + print() + + print("Demo completed! Try running:") + print(" uv run python apps/document_rag.py") + + +if __name__ == "__main__": + main() diff --git a/examples/dynamic_update_no_recompute.py b/examples/dynamic_update_no_recompute.py new file mode 100644 index 0000000..761375d --- /dev/null +++ b/examples/dynamic_update_no_recompute.py @@ -0,0 +1,429 @@ +"""Dynamic HNSW update demo without compact storage. + +This script reproduces the minimal scenario we used while debugging on-the-fly +recompute: + +1. Build a non-compact HNSW index from the first few paragraphs of a text file. +2. Print the top results with `recompute_embeddings=True`. +3. Append additional paragraphs with :meth:`LeannBuilder.update_index`. +4. Run the same query again to show the newly inserted passages. + +Run it with ``uv`` (optionally pointing LEANN_HNSW_LOG_PATH at a file to inspect +ZMQ activity):: + + LEANN_HNSW_LOG_PATH=embedding_fetch.log \ + uv run -m examples.dynamic_update_no_recompute \ + --index-path .leann/examples/leann-demo.leann + +By default the script builds an index from ``data/2501.14312v1 (1).pdf`` and +then updates it with LEANN-related material from ``data/2506.08276v1.pdf``. +It issues the query "What's LEANN?" before and after the update to show how the +new passages become immediately searchable. The script uses the +``sentence-transformers/all-MiniLM-L6-v2`` model with ``is_recompute=True`` so +Faiss pulls existing vectors on demand via the ZMQ embedding server, while +freshly added passages are embedded locally just like the initial build. + +To make storage comparisons easy, the script can also build a matching +``is_recompute=False`` baseline (enabled by default) and report the index size +delta after the update. Disable the baseline run with +``--skip-compare-no-recompute`` if you only need the recompute flow. +""" + +import argparse +import json +from collections.abc import Iterable +from pathlib import Path +from typing import Any + +from leann.api import LeannBuilder, LeannSearcher +from leann.registry import register_project_directory + +from apps.chunking import create_text_chunks + +REPO_ROOT = Path(__file__).resolve().parents[1] + +DEFAULT_QUERY = "What's LEANN?" +DEFAULT_INITIAL_FILES = [ + REPO_ROOT / "data" / "2501.14312v1 (1).pdf", + REPO_ROOT / "data" / "huawei_pangu.md", + REPO_ROOT / "data" / "PrideandPrejudice.txt", +] +DEFAULT_UPDATE_FILES = [REPO_ROOT / "data" / "2506.08276v1.pdf"] + + +def load_chunks_from_files(paths: list[Path]) -> list[str]: + from llama_index.core import SimpleDirectoryReader + + documents = [] + for path in paths: + p = path.expanduser().resolve() + if not p.exists(): + raise FileNotFoundError(f"Input path not found: {p}") + if p.is_dir(): + reader = SimpleDirectoryReader(str(p), recursive=False) + documents.extend(reader.load_data(show_progress=True)) + else: + reader = SimpleDirectoryReader(input_files=[str(p)]) + documents.extend(reader.load_data(show_progress=True)) + + if not documents: + return [] + + chunks = create_text_chunks( + documents, + chunk_size=512, + chunk_overlap=128, + use_ast_chunking=False, + ) + return [c for c in chunks if isinstance(c, str) and c.strip()] + + +def run_search(index_path: Path, query: str, top_k: int, *, recompute_embeddings: bool) -> list: + searcher = LeannSearcher(str(index_path)) + try: + return searcher.search( + query=query, + top_k=top_k, + recompute_embeddings=recompute_embeddings, + batch_size=16, + ) + finally: + searcher.cleanup() + + +def print_results(title: str, results: Iterable) -> None: + print(f"\n=== {title} ===") + res_list = list(results) + print(f"results count: {len(res_list)}") + print("passages:") + if not res_list: + print(" (no passages returned)") + for res in res_list: + snippet = res.text.replace("\n", " ")[:120] + print(f" - {res.id}: {snippet}... (score={res.score:.4f})") + + +def build_initial_index( + index_path: Path, + paragraphs: list[str], + model_name: str, + embedding_mode: str, + is_recompute: bool, +) -> None: + builder = LeannBuilder( + backend_name="hnsw", + embedding_model=model_name, + embedding_mode=embedding_mode, + is_compact=False, + is_recompute=is_recompute, + ) + for idx, passage in enumerate(paragraphs): + builder.add_text(passage, metadata={"id": str(idx)}) + builder.build_index(str(index_path)) + + +def update_index( + index_path: Path, + start_id: int, + paragraphs: list[str], + model_name: str, + embedding_mode: str, + is_recompute: bool, +) -> None: + updater = LeannBuilder( + backend_name="hnsw", + embedding_model=model_name, + embedding_mode=embedding_mode, + is_compact=False, + is_recompute=is_recompute, + ) + for offset, passage in enumerate(paragraphs, start=start_id): + updater.add_text(passage, metadata={"id": str(offset)}) + updater.update_index(str(index_path)) + + +def ensure_index_dir(index_path: Path) -> None: + index_path.parent.mkdir(parents=True, exist_ok=True) + + +def cleanup_index_files(index_path: Path) -> None: + """Remove leftover index artifacts for a clean rebuild.""" + + parent = index_path.parent + if not parent.exists(): + return + stem = index_path.stem + for file in parent.glob(f"{stem}*"): + if file.is_file(): + file.unlink() + + +def index_file_size(index_path: Path) -> int: + """Return the size of the primary .index file for the given index path.""" + + index_file = index_path.parent / f"{index_path.stem}.index" + return index_file.stat().st_size if index_file.exists() else 0 + + +def load_metadata_snapshot(index_path: Path) -> dict[str, Any] | None: + meta_path = index_path.parent / f"{index_path.name}.meta.json" + if not meta_path.exists(): + return None + try: + return json.loads(meta_path.read_text()) + except json.JSONDecodeError: + return None + + +def run_workflow( + *, + label: str, + index_path: Path, + initial_paragraphs: list[str], + update_paragraphs: list[str], + model_name: str, + embedding_mode: str, + is_recompute: bool, + query: str, + top_k: int, + skip_search: bool, +) -> dict[str, Any]: + prefix = f"[{label}] " if label else "" + + ensure_index_dir(index_path) + cleanup_index_files(index_path) + + print(f"{prefix}Building initial index...") + build_initial_index( + index_path, + initial_paragraphs, + model_name, + embedding_mode, + is_recompute=is_recompute, + ) + + initial_size = index_file_size(index_path) + if not skip_search: + before_results = run_search( + index_path, + query, + top_k, + recompute_embeddings=is_recompute, + ) + else: + before_results = None + + print(f"\n{prefix}Updating index with additional passages...") + update_index( + index_path, + start_id=len(initial_paragraphs), + paragraphs=update_paragraphs, + model_name=model_name, + embedding_mode=embedding_mode, + is_recompute=is_recompute, + ) + + if not skip_search: + after_results = run_search( + index_path, + query, + top_k, + recompute_embeddings=is_recompute, + ) + else: + after_results = None + updated_size = index_file_size(index_path) + + return { + "initial_size": initial_size, + "updated_size": updated_size, + "delta": updated_size - initial_size, + "before_results": before_results if not skip_search else None, + "after_results": after_results if not skip_search else None, + "metadata": load_metadata_snapshot(index_path), + } + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--initial-files", + type=Path, + nargs="+", + default=DEFAULT_INITIAL_FILES, + help="Initial document files (PDF/TXT) used to build the base index", + ) + parser.add_argument( + "--index-path", + type=Path, + default=Path(".leann/examples/leann-demo.leann"), + help="Destination index path (default: .leann/examples/leann-demo.leann)", + ) + parser.add_argument( + "--initial-count", + type=int, + default=8, + help="Number of chunks to use from the initial documents (default: 8)", + ) + parser.add_argument( + "--update-files", + type=Path, + nargs="*", + default=DEFAULT_UPDATE_FILES, + help="Additional documents to add during update (PDF/TXT)", + ) + parser.add_argument( + "--update-count", + type=int, + default=4, + help="Number of chunks to append from update documents (default: 4)", + ) + parser.add_argument( + "--update-text", + type=str, + default=( + "LEANN (Lightweight Embedding ANN) is an indexing toolkit focused on " + "recompute-aware HNSW graphs, allowing embeddings to be regenerated " + "on demand to keep disk usage minimal." + ), + help="Fallback text to append if --update-files is omitted", + ) + parser.add_argument( + "--top-k", + type=int, + default=4, + help="Number of results to show for each search (default: 4)", + ) + parser.add_argument( + "--query", + type=str, + default=DEFAULT_QUERY, + help="Query to run before/after the update", + ) + parser.add_argument( + "--embedding-model", + type=str, + default="sentence-transformers/all-MiniLM-L6-v2", + help="Embedding model name", + ) + parser.add_argument( + "--embedding-mode", + type=str, + default="sentence-transformers", + choices=["sentence-transformers", "openai", "mlx", "ollama"], + help="Embedding backend mode", + ) + parser.add_argument( + "--compare-no-recompute", + dest="compare_no_recompute", + action="store_true", + help="Also run a baseline with is_recompute=False and report its index growth.", + ) + parser.add_argument( + "--skip-compare-no-recompute", + dest="compare_no_recompute", + action="store_false", + help="Skip building the no-recompute baseline.", + ) + parser.add_argument( + "--skip-search", + dest="skip_search", + action="store_true", + help="Skip the search step.", + ) + parser.set_defaults(compare_no_recompute=True) + args = parser.parse_args() + + ensure_index_dir(args.index_path) + register_project_directory(REPO_ROOT) + + initial_chunks = load_chunks_from_files(list(args.initial_files)) + if not initial_chunks: + raise ValueError("No text chunks extracted from the initial files.") + + initial = initial_chunks[: args.initial_count] + if not initial: + raise ValueError("Initial chunk set is empty after applying --initial-count.") + + if args.update_files: + update_chunks = load_chunks_from_files(list(args.update_files)) + if not update_chunks: + raise ValueError("No text chunks extracted from the update files.") + to_add = update_chunks[: args.update_count] + else: + if not args.update_text: + raise ValueError("Provide --update-files or --update-text for the update step.") + to_add = [args.update_text] + if not to_add: + raise ValueError("Update chunk set is empty after applying --update-count.") + + recompute_stats = run_workflow( + label="recompute", + index_path=args.index_path, + initial_paragraphs=initial, + update_paragraphs=to_add, + model_name=args.embedding_model, + embedding_mode=args.embedding_mode, + is_recompute=True, + query=args.query, + top_k=args.top_k, + skip_search=args.skip_search, + ) + + if not args.skip_search: + print_results("initial search", recompute_stats["before_results"]) + if not args.skip_search: + print_results("after update", recompute_stats["after_results"]) + print( + f"\n[recompute] Index file size change: {recompute_stats['initial_size']} -> {recompute_stats['updated_size']} bytes" + f" (Ξ” {recompute_stats['delta']})" + ) + + if recompute_stats["metadata"]: + meta_view = {k: recompute_stats["metadata"].get(k) for k in ("is_compact", "is_pruned")} + print("[recompute] metadata snapshot:") + print(json.dumps(meta_view, indent=2)) + + if args.compare_no_recompute: + baseline_path = ( + args.index_path.parent / f"{args.index_path.stem}-norecompute{args.index_path.suffix}" + ) + baseline_stats = run_workflow( + label="no-recompute", + index_path=baseline_path, + initial_paragraphs=initial, + update_paragraphs=to_add, + model_name=args.embedding_model, + embedding_mode=args.embedding_mode, + is_recompute=False, + query=args.query, + top_k=args.top_k, + skip_search=args.skip_search, + ) + + print( + f"\n[no-recompute] Index file size change: {baseline_stats['initial_size']} -> {baseline_stats['updated_size']} bytes" + f" (Ξ” {baseline_stats['delta']})" + ) + + after_texts = ( + [res.text for res in recompute_stats["after_results"]] if not args.skip_search else None + ) + baseline_after_texts = ( + [res.text for res in baseline_stats["after_results"]] if not args.skip_search else None + ) + if after_texts == baseline_after_texts: + print( + "[no-recompute] Search results match recompute baseline; see above for the shared output." + ) + else: + print("[no-recompute] WARNING: search results differ from recompute baseline.") + + if baseline_stats["metadata"]: + meta_view = {k: baseline_stats["metadata"].get(k) for k in ("is_compact", "is_pruned")} + print("[no-recompute] metadata snapshot:") + print(json.dumps(meta_view, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/grep_search_example.py b/examples/grep_search_example.py new file mode 100644 index 0000000..71723ab --- /dev/null +++ b/examples/grep_search_example.py @@ -0,0 +1,35 @@ +""" +Grep Search Example + +Shows how to use grep-based text search instead of semantic search. +Useful when you need exact text matches rather than meaning-based results. +""" + +from leann import LeannSearcher + +# Load your index +searcher = LeannSearcher("my-documents.leann") + +# Regular semantic search +print("=== Semantic Search ===") +results = searcher.search("machine learning algorithms", top_k=3) +for result in results: + print(f"Score: {result.score:.3f}") + print(f"Text: {result.text[:80]}...") + print() + +# Grep-based search for exact text matches +print("=== Grep Search ===") +results = searcher.search("def train_model", top_k=3, use_grep=True) +for result in results: + print(f"Score: {result.score}") + print(f"Text: {result.text[:80]}...") + print() + +# Find specific error messages +error_results = searcher.search("FileNotFoundError", use_grep=True) +print(f"Found {len(error_results)} files mentioning FileNotFoundError") + +# Search for function definitions +func_results = searcher.search("class SearchResult", use_grep=True, top_k=5) +print(f"Found {len(func_results)} class definitions") diff --git a/examples/mcp_integration_demo.py b/examples/mcp_integration_demo.py new file mode 100644 index 0000000..6fc4cfc --- /dev/null +++ b/examples/mcp_integration_demo.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +MCP Integration Examples for LEANN + +This script demonstrates how to use LEANN with different MCP servers for +RAG on various platforms like Slack and Twitter. + +Examples: +1. Slack message RAG via MCP +2. Twitter bookmark RAG via MCP +3. Testing MCP server connections +""" + +import asyncio +import sys +from pathlib import Path + +# Add the parent directory to the path so we can import from apps +sys.path.append(str(Path(__file__).parent.parent)) + + +async def demo_slack_mcp(): + """Demonstrate Slack MCP integration.""" + print("=" * 60) + print("πŸ”₯ Slack MCP RAG Demo") + print("=" * 60) + + print("\n1. Testing Slack MCP server connection...") + + # This would typically use a real MCP server command + # For demo purposes, we show what the command would look like + # slack_app = SlackMCPRAG() # Would be used for actual testing + + # Simulate command line arguments for testing + class MockArgs: + mcp_server = "slack-mcp-server" # This would be the actual MCP server command + workspace_name = "my-workspace" + channels = ["general", "random", "dev-team"] + no_concatenate_conversations = False + max_messages_per_channel = 50 + test_connection = True + + print(f"MCP Server Command: {MockArgs.mcp_server}") + print(f"Workspace: {MockArgs.workspace_name}") + print(f"Channels: {', '.join(MockArgs.channels)}") + + # In a real scenario, you would run: + # success = await slack_app.test_mcp_connection(MockArgs) + + print("\nπŸ“ Example usage:") + print("python -m apps.slack_rag \\") + print(" --mcp-server 'slack-mcp-server' \\") + print(" --workspace-name 'my-team' \\") + print(" --channels general dev-team \\") + print(" --test-connection") + + print("\nπŸ” After indexing, you could query:") + print("- 'What did the team discuss about the project deadline?'") + print("- 'Find messages about the new feature launch'") + print("- 'Show me conversations about budget planning'") + + +async def demo_twitter_mcp(): + """Demonstrate Twitter MCP integration.""" + print("\n" + "=" * 60) + print("🐦 Twitter MCP RAG Demo") + print("=" * 60) + + print("\n1. Testing Twitter MCP server connection...") + + # twitter_app = TwitterMCPRAG() # Would be used for actual testing + + class MockArgs: + mcp_server = "twitter-mcp-server" + username = None # Fetch all bookmarks + max_bookmarks = 500 + no_tweet_content = False + no_metadata = False + test_connection = True + + print(f"MCP Server Command: {MockArgs.mcp_server}") + print(f"Max Bookmarks: {MockArgs.max_bookmarks}") + print(f"Include Content: {not MockArgs.no_tweet_content}") + print(f"Include Metadata: {not MockArgs.no_metadata}") + + print("\nπŸ“ Example usage:") + print("python -m apps.twitter_rag \\") + print(" --mcp-server 'twitter-mcp-server' \\") + print(" --max-bookmarks 1000 \\") + print(" --test-connection") + + print("\nπŸ” After indexing, you could query:") + print("- 'What AI articles did I bookmark last month?'") + print("- 'Find tweets about machine learning techniques'") + print("- 'Show me bookmarked threads about startup advice'") + + +async def show_mcp_server_setup(): + """Show how to set up MCP servers.""" + print("\n" + "=" * 60) + print("βš™οΈ MCP Server Setup Guide") + print("=" * 60) + + print("\nπŸ”§ Setting up Slack MCP Server:") + print("1. Install a Slack MCP server (example commands):") + print(" npm install -g slack-mcp-server") + print(" # OR") + print(" pip install slack-mcp-server") + + print("\n2. Configure Slack credentials:") + print(" export SLACK_BOT_TOKEN='xoxb-your-bot-token'") + print(" export SLACK_APP_TOKEN='xapp-your-app-token'") + + print("\n3. Test the server:") + print(" slack-mcp-server --help") + + print("\nπŸ”§ Setting up Twitter MCP Server:") + print("1. Install a Twitter MCP server:") + print(" npm install -g twitter-mcp-server") + print(" # OR") + print(" pip install twitter-mcp-server") + + print("\n2. Configure Twitter API credentials:") + print(" export TWITTER_API_KEY='your-api-key'") + print(" export TWITTER_API_SECRET='your-api-secret'") + print(" export TWITTER_ACCESS_TOKEN='your-access-token'") + print(" export TWITTER_ACCESS_TOKEN_SECRET='your-access-token-secret'") + + print("\n3. Test the server:") + print(" twitter-mcp-server --help") + + +async def show_integration_benefits(): + """Show the benefits of MCP integration.""" + print("\n" + "=" * 60) + print("🌟 Benefits of MCP Integration") + print("=" * 60) + + benefits = [ + ("πŸ”„ Live Data Access", "Fetch real-time data from platforms without manual exports"), + ("πŸ”Œ Standardized Protocol", "Use any MCP-compatible server with minimal code changes"), + ("πŸš€ Easy Extension", "Add new platforms by implementing MCP readers"), + ("πŸ”’ Secure Access", "MCP servers handle authentication and API management"), + ("πŸ“Š Rich Metadata", "Access full platform metadata (timestamps, engagement, etc.)"), + ("⚑ Efficient Processing", "Stream data directly into LEANN without intermediate files"), + ] + + for title, description in benefits: + print(f"\n{title}") + print(f" {description}") + + +async def main(): + """Main demo function.""" + print("🎯 LEANN MCP Integration Examples") + print("This demo shows how to integrate LEANN with MCP servers for various platforms.") + + await demo_slack_mcp() + await demo_twitter_mcp() + await show_mcp_server_setup() + await show_integration_benefits() + + print("\n" + "=" * 60) + print("✨ Next Steps") + print("=" * 60) + print("1. Install and configure MCP servers for your platforms") + print("2. Test connections using --test-connection flag") + print("3. Run indexing to build your RAG knowledge base") + print("4. Start querying your personal data!") + + print("\nπŸ“š For more information:") + print("- Check the README for detailed setup instructions") + print("- Look at the apps/slack_rag.py and apps/twitter_rag.py for implementation details") + print("- Explore other MCP servers for additional platforms") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/mlx_demo.py b/examples/mlx_demo.py new file mode 100644 index 0000000..c844534 --- /dev/null +++ b/examples/mlx_demo.py @@ -0,0 +1,43 @@ +import os + +from leann.api import LeannBuilder, LeannChat + +# Define the path for our new MLX-based index +INDEX_PATH = "./mlx_diskann_index/leann" + +if os.path.exists(INDEX_PATH + ".meta.json"): + print(f"Index already exists at {INDEX_PATH}. Skipping build.") +else: + print("Initializing LeannBuilder with MLX support...") + # 1. Configure LeannBuilder to use MLX + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ", + embedding_mode="mlx", + ) + + # 2. Add documents + print("Adding documents...") + docs = [ + "MLX is an array framework for machine learning on Apple silicon.", + "It was designed by Apple's machine learning research team.", + "The mlx-community organization provides pre-trained models in MLX format.", + "It supports operations on multi-dimensional arrays.", + "Leann can now use MLX for its embedding models.", + ] + for doc in docs: + builder.add_text(doc) + + # 3. Build the index + print(f"Building the MLX-based index at: {INDEX_PATH}") + builder.build_index(INDEX_PATH) + print("\nSuccessfully built the index with MLX embeddings!") + print(f"Check the metadata file: {INDEX_PATH}.meta.json") + + +chat = LeannChat(index_path=INDEX_PATH) +# add query +query = "MLX is an array framework for machine learning on Apple silicon." +print(f"Query: {query}") +response = chat.ask(query, top_k=3, recompute_beighbor_embeddings=True, complexity=3, beam_width=1) +print(f"Response: {response}") diff --git a/examples/spoiler_free_book_rag.py b/examples/spoiler_free_book_rag.py new file mode 100644 index 0000000..7f02fd4 --- /dev/null +++ b/examples/spoiler_free_book_rag.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +""" +Spoiler-Free Book RAG Example using LEANN Metadata Filtering + +This example demonstrates how to use LEANN's metadata filtering to create +a spoiler-free book RAG system where users can search for information +up to a specific chapter they've read. + +Usage: + python spoiler_free_book_rag.py +""" + +import os +import sys +from typing import Any, Optional + +# Add LEANN to path (adjust path as needed) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../packages/leann-core/src")) + +from leann.api import LeannBuilder, LeannSearcher + + +def chunk_book_with_metadata(book_title: str = "Sample Book") -> list[dict[str, Any]]: + """ + Create sample book chunks with metadata for demonstration. + + In a real implementation, this would parse actual book files (epub, txt, etc.) + and extract chapter boundaries, character mentions, etc. + + Args: + book_title: Title of the book + + Returns: + List of chunk dictionaries with text and metadata + """ + # Sample book chunks with metadata + # In practice, you'd use proper text processing libraries + + sample_chunks = [ + { + "text": "Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do.", + "metadata": { + "book": book_title, + "chapter": 1, + "page": 1, + "characters": ["Alice", "Sister"], + "themes": ["boredom", "curiosity"], + "location": "riverbank", + }, + }, + { + "text": "So she was considering in her own mind (as well as she could, for the hot day made her feel very sleepy and stupid), whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her.", + "metadata": { + "book": book_title, + "chapter": 1, + "page": 2, + "characters": ["Alice", "White Rabbit"], + "themes": ["decision", "surprise", "magic"], + "location": "riverbank", + }, + }, + { + "text": "Alice found herself falling down a very deep well. Either the well was very deep, or she fell very slowly, for she had plenty of time as she fell to look about her and to wonder what was going to happen next.", + "metadata": { + "book": book_title, + "chapter": 2, + "page": 15, + "characters": ["Alice"], + "themes": ["falling", "wonder", "transformation"], + "location": "rabbit hole", + }, + }, + { + "text": "Alice meets the Cheshire Cat, who tells her that everyone in Wonderland is mad, including Alice herself.", + "metadata": { + "book": book_title, + "chapter": 6, + "page": 85, + "characters": ["Alice", "Cheshire Cat"], + "themes": ["madness", "philosophy", "identity"], + "location": "Duchess's house", + }, + }, + { + "text": "At the Queen's croquet ground, Alice witnesses the absurd trial that reveals the arbitrary nature of Wonderland's justice system.", + "metadata": { + "book": book_title, + "chapter": 8, + "page": 120, + "characters": ["Alice", "Queen of Hearts", "King of Hearts"], + "themes": ["justice", "absurdity", "authority"], + "location": "Queen's court", + }, + }, + { + "text": "Alice realizes that Wonderland was all a dream, even the Rabbit, as she wakes up on the riverbank next to her sister.", + "metadata": { + "book": book_title, + "chapter": 12, + "page": 180, + "characters": ["Alice", "Sister", "Rabbit"], + "themes": ["revelation", "reality", "growth"], + "location": "riverbank", + }, + }, + ] + + return sample_chunks + + +def build_spoiler_free_index(book_chunks: list[dict[str, Any]], index_name: str) -> str: + """ + Build a LEANN index with book chunks that include spoiler metadata. + + Args: + book_chunks: List of book chunks with metadata + index_name: Name for the index + + Returns: + Path to the built index + """ + print(f"πŸ“š Building spoiler-free book index: {index_name}") + + # Initialize LEANN builder + builder = LeannBuilder( + backend_name="hnsw", embedding_model="text-embedding-3-small", embedding_mode="openai" + ) + + # Add each chunk with its metadata + for chunk in book_chunks: + builder.add_text(text=chunk["text"], metadata=chunk["metadata"]) + + # Build the index + index_path = f"{index_name}_book_index" + builder.build_index(index_path) + + print(f"βœ… Index built successfully: {index_path}") + return index_path + + +def spoiler_free_search( + index_path: str, + query: str, + max_chapter: int, + character_filter: Optional[list[str]] = None, +) -> list[dict[str, Any]]: + """ + Perform a spoiler-free search on the book index. + + Args: + index_path: Path to the LEANN index + query: Search query + max_chapter: Maximum chapter number to include + character_filter: Optional list of characters to focus on + + Returns: + List of search results safe for the reader + """ + print(f"πŸ” Searching: '{query}' (up to chapter {max_chapter})") + + searcher = LeannSearcher(index_path) + + metadata_filters = {"chapter": {"<=": max_chapter}} + + if character_filter: + metadata_filters["characters"] = {"contains": character_filter[0]} + + results = searcher.search(query=query, top_k=10, metadata_filters=metadata_filters) + + return results + + +def demo_spoiler_free_rag(): + """ + Demonstrate the spoiler-free book RAG system. + """ + print("🎭 Spoiler-Free Book RAG Demo") + print("=" * 40) + + # Step 1: Prepare book data + book_title = "Alice's Adventures in Wonderland" + book_chunks = chunk_book_with_metadata(book_title) + + print(f"πŸ“– Loaded {len(book_chunks)} chunks from '{book_title}'") + + # Step 2: Build the index (in practice, this would be done once) + try: + index_path = build_spoiler_free_index(book_chunks, "alice_wonderland") + except Exception as e: + print(f"❌ Failed to build index (likely missing dependencies): {e}") + print( + "πŸ’‘ This demo shows the filtering logic - actual indexing requires LEANN dependencies" + ) + return + + # Step 3: Demonstrate various spoiler-free searches + search_scenarios = [ + { + "description": "Reader who has only read Chapter 1", + "query": "What can you tell me about the rabbit?", + "max_chapter": 1, + }, + { + "description": "Reader who has read up to Chapter 5", + "query": "Tell me about Alice's adventures", + "max_chapter": 5, + }, + { + "description": "Reader who has read most of the book", + "query": "What does the Cheshire Cat represent?", + "max_chapter": 10, + }, + { + "description": "Reader who has read the whole book", + "query": "What can you tell me about the rabbit?", + "max_chapter": 12, + }, + ] + + for scenario in search_scenarios: + print(f"\nπŸ“š Scenario: {scenario['description']}") + print(f" Query: {scenario['query']}") + + try: + results = spoiler_free_search( + index_path=index_path, + query=scenario["query"], + max_chapter=scenario["max_chapter"], + ) + + print(f" πŸ“„ Found {len(results)} results:") + for i, result in enumerate(results[:3], 1): # Show top 3 + chapter = result.metadata.get("chapter", "?") + location = result.metadata.get("location", "?") + print(f" {i}. Chapter {chapter} ({location}): {result.text[:80]}...") + + except Exception as e: + print(f" ❌ Search failed: {e}") + + +if __name__ == "__main__": + print("πŸ“š LEANN Spoiler-Free Book RAG Example") + print("=====================================") + + try: + demo_spoiler_free_rag() + except ImportError as e: + print(f"❌ Cannot run demo due to missing dependencies: {e}") + except Exception as e: + print(f"❌ Error running demo: {e}") diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..e470008 --- /dev/null +++ b/llms.txt @@ -0,0 +1,28 @@ +# llms.txt β€” LEANN MCP and Agent Integration +product: LEANN +homepage: https://github.com/yichuan-w/LEANN +contact: https://github.com/yichuan-w/LEANN/issues + +# Installation +install: uv tool install leann-core --with leann + +# MCP Server Entry Point +mcp.server: leann_mcp +mcp.protocol_version: 2024-11-05 + +# Tools +mcp.tools: leann_list, leann_search + +mcp.tool.leann_list.description: List available LEANN indexes +mcp.tool.leann_list.input: {} + +mcp.tool.leann_search.description: Semantic search across a named LEANN index +mcp.tool.leann_search.input.index_name: string, required +mcp.tool.leann_search.input.query: string, required +mcp.tool.leann_search.input.top_k: integer, optional, default=5, min=1, max=20 +mcp.tool.leann_search.input.complexity: integer, optional, default=32, min=16, max=128 + +# Notes +note: Build indexes with `leann build --docs ` before searching. +example.add: claude mcp add --scope user leann-server -- leann_mcp +example.verify: claude mcp list | cat diff --git a/packages/__init__.py b/packages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/leann-backend-diskann/CMakeLists.txt b/packages/leann-backend-diskann/CMakeLists.txt new file mode 100644 index 0000000..a944399 --- /dev/null +++ b/packages/leann-backend-diskann/CMakeLists.txt @@ -0,0 +1,9 @@ +# Wrapper so USE_TCMALLOC is OFF before DiskANN's option() runs. DiskANN's +# FetchContent(tcmalloc) pulls protobuf/re2/fuzztest and fails on Linux CI when +# Abseil CMake targets are missing (e.g. ubuntu arm64 with apt protobuf only). +cmake_minimum_required(VERSION 3.24) +project(leann_backend_diskann_wrapper NONE) + +set(USE_TCMALLOC OFF CACHE BOOL "Use tcmalloc from gperftools" FORCE) + +add_subdirectory(third_party/DiskANN) diff --git a/packages/leann-backend-diskann/__init__.py b/packages/leann-backend-diskann/__init__.py new file mode 100644 index 0000000..3596aa4 --- /dev/null +++ b/packages/leann-backend-diskann/__init__.py @@ -0,0 +1 @@ +# This file makes the directory a Python package diff --git a/packages/leann-backend-diskann/leann_backend_diskann/__init__.py b/packages/leann-backend-diskann/leann_backend_diskann/__init__.py new file mode 100644 index 0000000..e085346 --- /dev/null +++ b/packages/leann-backend-diskann/leann_backend_diskann/__init__.py @@ -0,0 +1,64 @@ +import os +from pathlib import Path + +_DLL_DIR_HANDLES: list[object] = [] + + +def _configure_windows_dll_search_path() -> None: + """Register vcpkg DLL directories so Windows can resolve native dependencies. + + Python 3.8+ no longer searches PATH for DLL dependencies of native + extensions (see https://github.com/numpy/numpy/wiki/windows-dll-notes). + The standard workaround is ``os.add_dll_directory``. + + This only matters for **CI builds and source installs** where C++ deps + (OpenBLAS, ZeroMQ, protobuf, …) live in a vcpkg tree. Pre-built wheels + shipped via PyPI bundle all required DLLs inside the wheel (via + delvewheel), so end-users installing with ``pip install`` are unaffected. + """ + if os.name != "nt" or not hasattr(os, "add_dll_directory"): + return + + candidate_dirs: list[Path] = [] + env_roots = [os.getenv("VCPKG_INSTALLATION_ROOT"), os.getenv("VCPKG_ROOT")] + for root in env_roots: + if not root: + continue + root_path = Path(root) + candidate_dirs.extend( + [ + root_path / "installed" / "x64-windows" / "bin", + root_path / "installed" / "x64-windows" / "debug" / "bin", + root_path / "installed" / "x64-windows" / "tools" / "protobuf", + ] + ) + + for path_entry in os.environ.get("PATH", "").split(";"): + entry = path_entry.strip() + if not entry: + continue + if "vcpkg" in entry.lower(): + candidate_dirs.append(Path(entry)) + + seen: set[str] = set() + for dll_dir in candidate_dirs: + resolved = str(dll_dir).lower() + if resolved in seen or not dll_dir.exists(): + continue + seen.add(resolved) + try: + _DLL_DIR_HANDLES.append(os.add_dll_directory(str(dll_dir))) + os.environ["PATH"] = f"{dll_dir};{os.environ.get('PATH', '')}" + except OSError: + continue + + +_configure_windows_dll_search_path() + +from . import diskann_backend as diskann_backend # noqa: E402 +from . import graph_partition # noqa: E402 + +# Export main classes and functions +from .graph_partition import GraphPartitioner, partition_graph # noqa: E402 + +__all__ = ["GraphPartitioner", "diskann_backend", "graph_partition", "partition_graph"] diff --git a/packages/leann-backend-diskann/leann_backend_diskann/diskann_backend.py b/packages/leann-backend-diskann/leann_backend_diskann/diskann_backend.py new file mode 100644 index 0000000..8e97609 --- /dev/null +++ b/packages/leann-backend-diskann/leann_backend_diskann/diskann_backend.py @@ -0,0 +1,563 @@ +import contextlib +import logging +import multiprocessing as mp +import os +import struct +import sys +from pathlib import Path +from typing import Any, Literal, Optional + +import numpy as np +import psutil +from leann.interface import ( + LeannBackendBuilderInterface, + LeannBackendFactoryInterface, + LeannBackendSearcherInterface, +) +from leann.registry import register_backend +from leann.searcher_base import BaseSearcher + +logger = logging.getLogger(__name__) + + +@contextlib.contextmanager +def suppress_cpp_output_if_needed(): + """Suppress C++ stdout/stderr based on LEANN_LOG_LEVEL""" + # In CI we avoid fiddling with low-level file descriptors to prevent aborts + if os.getenv("CI") == "true": + yield + return + + log_level = os.getenv("LEANN_LOG_LEVEL", "WARNING").upper() + + # Only suppress if log level is WARNING or higher (ERROR, CRITICAL) + should_suppress = log_level in ["WARNING", "ERROR", "CRITICAL"] + + if not should_suppress: + # Don't suppress, just yield + yield + return + + # Save original file descriptors + stdout_fd = sys.stdout.fileno() + stderr_fd = sys.stderr.fileno() + + # Save original stdout/stderr + stdout_dup = os.dup(stdout_fd) + stderr_dup = os.dup(stderr_fd) + + try: + # Redirect to /dev/null + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, stdout_fd) + os.dup2(devnull, stderr_fd) + os.close(devnull) + + yield + + finally: + # Restore original file descriptors + os.dup2(stdout_dup, stdout_fd) + os.dup2(stderr_dup, stderr_fd) + os.close(stdout_dup) + os.close(stderr_dup) + + +def _get_diskann_metrics(): + from . import _diskannpy as diskannpy # type: ignore + + return { + "mips": diskannpy.Metric.INNER_PRODUCT, + "l2": diskannpy.Metric.L2, + "cosine": diskannpy.Metric.COSINE, + } + + +@contextlib.contextmanager +def chdir(path): + original_dir = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(original_dir) + + +def _write_vectors_to_bin(data: np.ndarray, file_path: Path): + num_vectors, dim = data.shape + with open(file_path, "wb") as f: + f.write(struct.pack("I", num_vectors)) + f.write(struct.pack("I", dim)) + f.write(data.tobytes()) + + +def _calculate_smart_memory_config(data: np.ndarray) -> tuple[float, float]: + """ + Calculate smart memory configuration for DiskANN based on data size and system specs. + + Args: + data: The embedding data array + + Returns: + tuple: (search_memory_maximum, build_memory_maximum) in GB + """ + num_vectors, dim = data.shape + + # Calculate embedding storage size + embedding_size_bytes = num_vectors * dim * 4 # float32 = 4 bytes + embedding_size_gb = embedding_size_bytes / (1024**3) + + # search_memory_maximum: 1/10 of embedding size for optimal PQ compression + # This controls Product Quantization size - smaller means more compression + search_memory_gb = max(0.1, embedding_size_gb / 10) # At least 100MB + + # build_memory_maximum: Based on available system RAM for sharding control + # This controls how much memory DiskANN uses during index construction + available_memory_gb = psutil.virtual_memory().available / (1024**3) + total_memory_gb = psutil.virtual_memory().total / (1024**3) + + # Use 50% of available memory, but at least 2GB and at most 75% of total + build_memory_gb = max(2.0, min(available_memory_gb * 0.5, total_memory_gb * 0.75)) + + logger.info( + f"Smart memory config - Data: {embedding_size_gb:.2f}GB, " + f"Search mem: {search_memory_gb:.2f}GB (PQ control), " + f"Build mem: {build_memory_gb:.2f}GB (sharding control)" + ) + + return search_memory_gb, build_memory_gb + + +@register_backend("diskann") +class DiskannBackend(LeannBackendFactoryInterface): + @staticmethod + def builder(**kwargs) -> LeannBackendBuilderInterface: + return DiskannBuilder(**kwargs) + + @staticmethod + def searcher(index_path: str, **kwargs) -> LeannBackendSearcherInterface: + return DiskannSearcher(index_path, **kwargs) + + +class DiskannBuilder(LeannBackendBuilderInterface): + def __init__(self, **kwargs): + self.build_params = kwargs + + def _safe_cleanup_after_partition(self, index_dir: Path, index_prefix: str): + """ + Safely cleanup files after partition. + In partition mode, C++ doesn't read _disk.index content, + so we can delete it if all derived files exist. + """ + disk_index_file = index_dir / f"{index_prefix}_disk.index" + beam_search_file = index_dir / f"{index_prefix}_disk_beam_search.index" + + # Required files that C++ partition mode needs + # Note: C++ generates these with _disk.index suffix + disk_suffix = "_disk.index" + required_files = [ + f"{index_prefix}{disk_suffix}_medoids.bin", # Critical: assert fails if missing + # Note: _centroids.bin is not created in single-shot build - C++ handles this automatically + f"{index_prefix}_pq_pivots.bin", # PQ table + f"{index_prefix}_pq_compressed.bin", # PQ compressed vectors + ] + + # Check if all required files exist + missing_files = [] + for filename in required_files: + file_path = index_dir / filename + if not file_path.exists(): + missing_files.append(filename) + + if missing_files: + logger.warning( + f"Cannot safely delete _disk.index - missing required files: {missing_files}" + ) + logger.info("Keeping all original files for safety") + return + + # Calculate space savings + space_saved = 0 + files_to_delete = [] + + if disk_index_file.exists(): + space_saved += disk_index_file.stat().st_size + files_to_delete.append(disk_index_file) + + if beam_search_file.exists(): + space_saved += beam_search_file.stat().st_size + files_to_delete.append(beam_search_file) + + # Safe to delete! + for file_to_delete in files_to_delete: + try: + os.remove(file_to_delete) + logger.info(f"βœ… Safely deleted: {file_to_delete.name}") + except Exception as e: + logger.warning(f"Failed to delete {file_to_delete.name}: {e}") + + if space_saved > 0: + space_saved_mb = space_saved / (1024 * 1024) + logger.info(f"πŸ’Ύ Space saved: {space_saved_mb:.1f} MB") + + # Show what files are kept + logger.info("πŸ“ Kept essential files for partition mode:") + for filename in required_files: + file_path = index_dir / filename + if file_path.exists(): + size_mb = file_path.stat().st_size / (1024 * 1024) + logger.info(f" - {filename} ({size_mb:.1f} MB)") + + def build(self, data: np.ndarray, ids: list[str], index_path: str, **kwargs): + path = Path(index_path) + index_dir = path.parent + index_prefix = path.stem + index_dir.mkdir(parents=True, exist_ok=True) + + if data.dtype != np.float32: + logger.warning(f"Converting data to float32, shape: {data.shape}") + data = data.astype(np.float32) + + # DiskANN uses 256 PQ centroids by default for product quantization. + # When num_vectors < 256, MKL's cblas_sgemm receives degenerate matrix dimensions + # and emits non-fatal "Parameter N was incorrect" errors to stderr on Windows. + # The index still builds correctly, but the noise can be confusing. + # See: https://github.com/yichuan-w/LEANN/issues/280 + _NUM_PQ_CENTROIDS = 256 + if data.shape[0] < _NUM_PQ_CENTROIDS: + logger.warning( + f"DiskANN dataset has only {data.shape[0]} vector(s), which is fewer than " + f"the default number of PQ centroids ({_NUM_PQ_CENTROIDS}). " + f"On Windows with Intel MKL this produces non-fatal 'cblas_sgemm parameter' " + f"errors in the output β€” the index will still build correctly. " + f"Consider using '--backend-name hnsw' for small datasets to avoid these messages." + ) + + data_filename = f"{index_prefix}_data.bin" + _write_vectors_to_bin(data, index_dir / data_filename) + + build_kwargs = {**self.build_params, **kwargs} + + # Extract is_recompute from nested backend_kwargs if needed + is_recompute = build_kwargs.get("is_recompute", False) + if not is_recompute and "backend_kwargs" in build_kwargs: + is_recompute = build_kwargs["backend_kwargs"].get("is_recompute", False) + + # Flatten all backend_kwargs parameters to top level for compatibility + if "backend_kwargs" in build_kwargs: + nested_params = build_kwargs.pop("backend_kwargs") + build_kwargs.update(nested_params) + + metric_enum = _get_diskann_metrics().get( + build_kwargs.get("distance_metric", "mips").lower() + ) + if metric_enum is None: + raise ValueError( + f"Unsupported distance_metric '{build_kwargs.get('distance_metric', 'unknown')}'." + ) + + # Calculate smart memory configuration if not explicitly provided + if ( + "search_memory_maximum" not in build_kwargs + or "build_memory_maximum" not in build_kwargs + ): + smart_search_mem, smart_build_mem = _calculate_smart_memory_config(data) + else: + smart_search_mem = build_kwargs.get("search_memory_maximum", 4.0) + smart_build_mem = build_kwargs.get("build_memory_maximum", 8.0) + + try: + # Build in a child process so native crashes/exits in diskannpy + # do not terminate the main CLI process silently. + def _run_build( + work_dir: str, + metric: Any, + data_file: str, + prefix: str, + complexity: int, + graph_degree: int, + search_mem: float, + build_mem: float, + num_threads: int, + pq_disk_bytes: int, + out_q: "mp.Queue[tuple[bool, str]]", + ) -> None: + try: + from . import _diskannpy as diskannpy # type: ignore + + with chdir(work_dir): + diskannpy.build_disk_float_index( + metric, + data_file, + prefix, + complexity, + graph_degree, + search_mem, + build_mem, + num_threads, + pq_disk_bytes, + "", + ) + out_q.put((True, "ok")) + except Exception as e: + out_q.put((False, str(e))) + + queue: mp.Queue[tuple[bool, str]] = mp.Queue() + proc = mp.Process( + target=_run_build, + args=( + str(index_dir), + metric_enum, + data_filename, + index_prefix, + int(build_kwargs.get("complexity", 64)), + int(build_kwargs.get("graph_degree", 32)), + float(build_kwargs.get("search_memory_maximum", smart_search_mem)), + float(build_kwargs.get("build_memory_maximum", smart_build_mem)), + int(build_kwargs.get("num_threads", 8)), + int(build_kwargs.get("pq_disk_bytes", 0)), + queue, + ), + ) + proc.start() + proc.join() + + if proc.exitcode != 0: + raise RuntimeError( + f"DiskANN native build process exited unexpectedly with code {proc.exitcode}. " + "Re-run with LEANN_LOG_LEVEL=DEBUG and check system logs." + ) + if queue.empty(): + raise RuntimeError( + "DiskANN native build exited without status message. " + "This usually indicates a native termination inside diskannpy." + ) + ok, msg = queue.get_nowait() + if not ok: + raise RuntimeError(f"DiskANN build failed: {msg}") + + # Validate expected output to catch silent native failures. + expected_disk_index = index_dir / f"{index_prefix}_disk.index" + if not expected_disk_index.exists(): + produced = sorted(str(p.name) for p in index_dir.glob(f"{index_prefix}*")) + raise RuntimeError( + "DiskANN build finished but expected index file is missing: " + f"{expected_disk_index.name}. Produced files: {produced}" + ) + + # Auto-partition if is_recompute is enabled + if build_kwargs.get("is_recompute", False): + logger.info("is_recompute=True, starting automatic graph partitioning...") + from .graph_partition import partition_graph + + # Partition the index using absolute paths + # Convert to absolute paths to avoid issues with working directory changes + absolute_index_dir = Path(index_dir).resolve() + absolute_index_prefix_path = str(absolute_index_dir / index_prefix) + disk_graph_path, partition_bin_path = partition_graph( + index_prefix_path=absolute_index_prefix_path, + output_dir=str(absolute_index_dir), + partition_prefix=index_prefix, + ) + + # Safe cleanup: In partition mode, C++ doesn't read _disk.index content + # but still needs the derived files (_medoids.bin, _centroids.bin, etc.) + self._safe_cleanup_after_partition(index_dir, index_prefix) + + logger.info("βœ… Graph partitioning completed successfully!") + logger.info(f" - Disk graph: {disk_graph_path}") + logger.info(f" - Partition file: {partition_bin_path}") + + finally: + temp_data_file = index_dir / data_filename + if temp_data_file.exists(): + os.remove(temp_data_file) + logger.debug(f"Cleaned up temporary data file: {temp_data_file}") + + +class DiskannSearcher(BaseSearcher): + def __init__(self, index_path: str, **kwargs): + super().__init__( + index_path, + backend_module_name="leann_backend_diskann.diskann_embedding_server", + **kwargs, + ) + + # Initialize DiskANN index with suppressed C++ output based on log level + with suppress_cpp_output_if_needed(): + from . import _diskannpy as diskannpy # type: ignore + + distance_metric = kwargs.get("distance_metric", "mips").lower() + metric_enum = _get_diskann_metrics().get(distance_metric) + if metric_enum is None: + raise ValueError(f"Unsupported distance_metric '{distance_metric}'.") + + self.num_threads = kwargs.get("num_threads", 8) + + # For DiskANN, we need to reinitialize the index when zmq_port changes + # Store the initialization parameters for later use + # Note: C++ load method expects the BASE path (without _disk.index suffix) + # C++ internally constructs: index_prefix + "_disk.index" + index_name = self.index_path.stem # "simple_test.leann" -> "simple_test" + diskann_index_prefix = str(self.index_dir / index_name) # /path/to/simple_test + full_index_prefix = diskann_index_prefix # /path/to/simple_test (base path) + + # Auto-detect partition files and set partition_prefix + partition_graph_file = self.index_dir / f"{index_name}_disk_graph.index" + partition_bin_file = self.index_dir / f"{index_name}_partition.bin" + + partition_prefix = "" + if partition_graph_file.exists() and partition_bin_file.exists(): + # C++ expects full path prefix, not just filename + partition_prefix = str(self.index_dir / index_name) # /path/to/simple_test + logger.info( + f"βœ… Detected partition files, using partition_prefix='{partition_prefix}'" + ) + else: + logger.debug("No partition files detected, using standard index files") + + self._init_params = { + "metric_enum": metric_enum, + "full_index_prefix": full_index_prefix, + "num_threads": self.num_threads, + "num_nodes_to_cache": kwargs.get("num_nodes_to_cache", 0), + # 1 -> initialize cache using sample_data; 2 -> ready cache without init; others disable cache + "cache_mechanism": kwargs.get("cache_mechanism", 1), + "pq_prefix": "", + "partition_prefix": partition_prefix, + } + + # Log partition configuration for debugging + if partition_prefix: + logger.info( + f"βœ… Detected partition files, using partition_prefix='{partition_prefix}'" + ) + self._diskannpy = diskannpy + self._current_zmq_port = None + self._index = None + logger.debug("DiskANN searcher initialized (index will be loaded on first search)") + + def close(self): + """Release the C++ index object and its file handles. + + On Windows, open memory-mapped files prevent temp directory cleanup. + Call this (or use LeannSearcher as a context manager) before deleting + the index directory. + """ + self._index = None + self._current_zmq_port = None + import gc + + gc.collect() + + def _ensure_index_loaded(self, zmq_port: int): + """Ensure the index is loaded with the correct zmq_port.""" + if self._index is None or self._current_zmq_port != zmq_port: + # Need to (re)load the index with the correct zmq_port + with suppress_cpp_output_if_needed(): + if self._index is not None: + logger.debug(f"Reloading DiskANN index with new zmq_port: {zmq_port}") + else: + logger.debug(f"Loading DiskANN index with zmq_port: {zmq_port}") + + self._index = self._diskannpy.StaticDiskFloatIndex( + self._init_params["metric_enum"], + self._init_params["full_index_prefix"], + self._init_params["num_threads"], + self._init_params["num_nodes_to_cache"], + self._init_params["cache_mechanism"], + zmq_port, + self._init_params["pq_prefix"], + self._init_params["partition_prefix"], + ) + self._current_zmq_port = zmq_port + + def search( + self, + query: np.ndarray, + top_k: int, + complexity: int = 64, + beam_width: int = 1, + prune_ratio: float = 0.0, + recompute_embeddings: bool = False, + pruning_strategy: Literal["global", "local", "proportional"] = "global", + zmq_port: Optional[int] = None, + batch_recompute: bool = False, + dedup_node_dis: bool = False, + **kwargs, + ) -> dict[str, Any]: + """ + Search for nearest neighbors using DiskANN index. + + Args: + query: Query vectors (B, D) where B is batch size, D is dimension + top_k: Number of nearest neighbors to return + complexity: Search complexity/candidate list size, higher = more accurate but slower + beam_width: Number of parallel IO requests per iteration + prune_ratio: Ratio of neighbors to prune via approximate distance (0.0-1.0) + recompute_embeddings: Whether to fetch fresh embeddings from server + pruning_strategy: PQ candidate selection strategy: + - "global": Use global pruning strategy (default) + - "local": Use local pruning strategy + - "proportional": Not supported in DiskANN, falls back to global + zmq_port: ZMQ port for embedding server communication. Must be provided if recompute_embeddings is True. + batch_recompute: Whether to batch neighbor recomputation (DiskANN-specific) + dedup_node_dis: Whether to cache and reuse distance computations (DiskANN-specific) + **kwargs: Additional DiskANN-specific parameters (for legacy compatibility) + + Returns: + Dict with 'labels' (list of lists) and 'distances' (ndarray) + """ + # Handle zmq_port compatibility: Ensure index is loaded with correct port + if recompute_embeddings: + if zmq_port is None: + raise ValueError("zmq_port must be provided if recompute_embeddings is True") + self._ensure_index_loaded(zmq_port) + else: + # If not recomputing, we still need an index, use a default port + if self._index is None: + self._ensure_index_loaded(6666) # Default port when not recomputing + + # DiskANN doesn't support "proportional" strategy + if pruning_strategy == "proportional": + raise NotImplementedError( + "DiskANN backend does not support 'proportional' pruning strategy. Use 'global' or 'local' instead." + ) + + if query.dtype != np.float32: + query = query.astype(np.float32) + + # Map pruning_strategy to DiskANN's global_pruning parameter + if pruning_strategy == "local": + use_global_pruning = False + else: # "global" + use_global_pruning = True + + # Strategy: + # - Traversal always uses PQ distances + # - If recompute_embeddings=True, do a single final rerank via deferred fetch + # (fetch embeddings for the final candidate set only) + # - Do not recompute neighbor distances along the path + use_deferred_fetch = True if recompute_embeddings else False + recompute_neighors = False # Expected typo. For backward compatibility. + + with suppress_cpp_output_if_needed(): + labels, distances = self._index.batch_search( + query, + query.shape[0], + top_k, + complexity, + beam_width, + self.num_threads, + use_deferred_fetch, + kwargs.get("skip_search_reorder", False), + recompute_neighors, + dedup_node_dis, + prune_ratio, + batch_recompute, + use_global_pruning, + ) + + string_labels = [[str(int_label) for int_label in batch_labels] for batch_labels in labels] + + return {"labels": string_labels, "distances": distances} diff --git a/packages/leann-backend-diskann/leann_backend_diskann/diskann_embedding_server.py b/packages/leann-backend-diskann/leann_backend_diskann/diskann_embedding_server.py new file mode 100644 index 0000000..c354675 --- /dev/null +++ b/packages/leann-backend-diskann/leann_backend_diskann/diskann_embedding_server.py @@ -0,0 +1,533 @@ +""" +DiskANN-specific embedding server +""" + +import argparse +import json +import logging +import os +import sys +import threading +import time +from pathlib import Path +from typing import Any, Optional + +import numpy as np +import zmq + +# Set up logging based on environment variable +LOG_LEVEL = os.getenv("LEANN_LOG_LEVEL", "WARNING").upper() +logger = logging.getLogger(__name__) + +# Force set logger level (don't rely on basicConfig in subprocess) +log_level = getattr(logging, LOG_LEVEL, logging.WARNING) +logger.setLevel(log_level) + +# Ensure we have a handler if none exists +if not logger.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.propagate = False + + +_RAW_PROVIDER_OPTIONS = os.getenv("LEANN_EMBEDDING_OPTIONS") +try: + PROVIDER_OPTIONS: dict[str, Any] = ( + json.loads(_RAW_PROVIDER_OPTIONS) if _RAW_PROVIDER_OPTIONS else {} + ) +except json.JSONDecodeError: + logger.warning("Failed to parse LEANN_EMBEDDING_OPTIONS; ignoring provider options") + PROVIDER_OPTIONS = {} + + +def create_diskann_embedding_server( + passages_file: Optional[str] = None, + zmq_port: int = 5555, + model_name: str = "sentence-transformers/all-mpnet-base-v2", + embedding_mode: str = "sentence-transformers", + distance_metric: str = "l2", + enable_warmup: bool = False, + daemon_ttl: int = 0, +): + """ + Create and start a ZMQ-based embedding server for DiskANN backend. + Uses ROUTER socket and protobuf communication as required by DiskANN C++ implementation. + """ + logger.info(f"Starting DiskANN server on port {zmq_port} with model {model_name}") + logger.info(f"Using embedding mode: {embedding_mode}") + + # Add leann-core to path for unified embedding computation + current_dir = Path(__file__).parent + leann_core_path = current_dir.parent.parent / "leann-core" / "src" + sys.path.insert(0, str(leann_core_path)) + + try: + from leann.api import PassageManager + from leann.embedding_compute import compute_embeddings + + logger.info("Successfully imported unified embedding computation module") + except ImportError as e: + logger.error(f"Failed to import embedding computation module: {e}") + return + finally: + sys.path.pop(0) + + if enable_warmup: + try: + logger.info("Starting warmup embedding request...") + _ = compute_embeddings( + ["__LEANN_WARMUP__"], + model_name, + mode=embedding_mode, + provider_options=PROVIDER_OPTIONS, + ) + logger.info("Warmup complete.") + except Exception as exc: + logger.warning(f"Warmup failed (continuing): {exc}") + + # Check port availability + import socket + + def check_port(port): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(("localhost", port)) == 0 + + if check_port(zmq_port): + logger.error(f"Port {zmq_port} is already in use") + return + + # Only support metadata file, fail fast for everything else + if not passages_file or not passages_file.endswith(".meta.json"): + raise ValueError("Only metadata files (.meta.json) are supported") + + # Load metadata to get passage sources + with open(passages_file) as f: + meta = json.load(f) + + logger.info(f"Loading PassageManager with metadata_file_path: {passages_file}") + passages = PassageManager(meta["passage_sources"], metadata_file_path=passages_file) + logger.info(f"Loaded PassageManager with {len(passages)} passages from metadata") + + # Import protobuf after ensuring the path is correct + try: + from . import embedding_pb2 + except ImportError as e: + logger.error(f"Failed to import protobuf module: {e}") + return + + def zmq_server_thread(): + """ZMQ server thread using REP socket for universal compatibility""" + context = zmq.Context() + socket = context.socket( + zmq.REP + ) # REP socket for both BaseSearcher and DiskANN C++ REQ clients + socket.bind(f"tcp://*:{zmq_port}") + logger.info(f"DiskANN ZMQ REP server listening on port {zmq_port}") + + socket.setsockopt(zmq.RCVTIMEO, 1000) + socket.setsockopt(zmq.SNDTIMEO, 1000) + socket.setsockopt(zmq.LINGER, 0) + + while True: + try: + # REP socket receives single-part messages + message = socket.recv() + + # Check for empty messages - REP socket requires response to every request + if len(message) == 0: + logger.debug("Received empty message, sending empty response") + socket.send(b"") # REP socket must respond to every request + continue + + logger.debug(f"Received ZMQ request of size {len(message)} bytes") + logger.debug(f"Message preview: {message[:50]}") # Show first 50 bytes + + e2e_start = time.time() + + # Try protobuf first (for DiskANN C++ node_ids requests - primary use case) + texts = [] + node_ids = [] + is_text_request = False + + try: + req_proto = embedding_pb2.NodeEmbeddingRequest() + req_proto.ParseFromString(message) + node_ids = list(req_proto.node_ids) + + if not node_ids: + raise RuntimeError( + f"PROTOBUF: Received empty node_ids! Message size: {len(message)}" + ) + + logger.info( + f"βœ… PROTOBUF: Node ID request for {len(node_ids)} node embeddings: {node_ids[:10]}" + ) + except Exception as protobuf_error: + logger.debug(f"Protobuf parsing failed: {protobuf_error}") + # Fallback to msgpack (for BaseSearcher direct text requests) + try: + import msgpack + + request = msgpack.unpackb(message) + # For BaseSearcher compatibility, request is a list of texts directly + if isinstance(request, list) and all( + isinstance(item, str) for item in request + ): + texts = request + is_text_request = True + logger.info(f"βœ… MSGPACK: Direct text request for {len(texts)} texts") + else: + raise ValueError("Not a valid msgpack text request") + except Exception as msgpack_error: + raise RuntimeError( + f"Both protobuf and msgpack parsing failed! Protobuf: {protobuf_error}, Msgpack: {msgpack_error}" + ) + + # Look up texts by node IDs (only if not direct text request) + if not is_text_request: + for nid in node_ids: + try: + passage_data = passages.get_passage(str(nid)) + txt = passage_data["text"] + if not txt: + raise RuntimeError(f"FATAL: Empty text for passage ID {nid}") + texts.append(txt) + except KeyError as e: + logger.error(f"Passage ID {nid} not found: {e}") + raise e + except Exception as e: + logger.error(f"Exception looking up passage ID {nid}: {e}") + raise + + # Debug logging + logger.debug(f"Processing {len(texts)} texts") + logger.debug(f"Text lengths: {[len(t) for t in texts[:5]]}") # Show first 5 + + # Process embeddings using unified computation + embeddings = compute_embeddings( + texts, + model_name, + mode=embedding_mode, + provider_options=PROVIDER_OPTIONS, + ) + logger.info( + f"Computed embeddings for {len(texts)} texts, shape: {embeddings.shape}" + ) + + # Prepare response based on request type + if is_text_request: + # For BaseSearcher compatibility: return msgpack format + import msgpack + + response_data = msgpack.packb(embeddings.tolist()) + else: + # For DiskANN C++ compatibility: return protobuf format + resp_proto = embedding_pb2.NodeEmbeddingResponse() + hidden_contiguous = np.ascontiguousarray(embeddings, dtype=np.float32) + + # Serialize embeddings data + resp_proto.embeddings_data = hidden_contiguous.tobytes() + resp_proto.dimensions.append(hidden_contiguous.shape[0]) + resp_proto.dimensions.append(hidden_contiguous.shape[1]) + + response_data = resp_proto.SerializeToString() + + # Send response back to the client + socket.send(response_data) + + e2e_end = time.time() + logger.info(f"⏱️ ZMQ E2E time: {e2e_end - e2e_start:.6f}s") + + except zmq.Again: + logger.debug("ZMQ socket timeout, continuing to listen") + continue + except Exception as e: + logger.error(f"Error in ZMQ server loop: {e}") + import traceback + + traceback.print_exc() + raise + + def zmq_server_thread_with_shutdown(shutdown_event): + """ZMQ server thread that respects shutdown signal. + + This creates its own REP socket, binds to zmq_port, and periodically + checks shutdown_event using recv timeouts to exit cleanly. + """ + logger.info("DiskANN ZMQ server thread started with shutdown support") + + context = zmq.Context() + rep_socket = context.socket(zmq.REP) + rep_socket.bind(f"tcp://*:{zmq_port}") + logger.info(f"DiskANN ZMQ REP server listening on port {zmq_port}") + + # Set receive timeout so we can check shutdown_event periodically + rep_socket.setsockopt(zmq.RCVTIMEO, 1000) # 1 second timeout + rep_socket.setsockopt(zmq.SNDTIMEO, 1000) + rep_socket.setsockopt(zmq.LINGER, 0) + + try: + while not shutdown_event.is_set(): + try: + e2e_start = time.time() + # REP socket receives single-part messages + message = rep_socket.recv() + last_activity[0] = time.time() + + # Check for empty messages - REP socket requires response to every request + if not message: + logger.warning("Received empty message, sending empty response") + rep_socket.send(b"") + continue + + # Try protobuf first (same logic as original) + texts = [] + is_text_request = False + + try: + req_proto = embedding_pb2.NodeEmbeddingRequest() + req_proto.ParseFromString(message) + node_ids = list(req_proto.node_ids) + + # Look up texts by node IDs + for nid in node_ids: + try: + passage_data = passages.get_passage(str(nid)) + txt = passage_data["text"] + if not txt: + raise RuntimeError(f"FATAL: Empty text for passage ID {nid}") + texts.append(txt) + except KeyError: + raise RuntimeError(f"FATAL: Passage with ID {nid} not found") + + logger.info(f"ZMQ received protobuf request for {len(node_ids)} node IDs") + except Exception: + # Fallback to msgpack for text requests + try: + import msgpack + + request = msgpack.unpackb(message) + if isinstance(request, list) and all( + isinstance(item, str) for item in request + ): + texts = request + is_text_request = True + logger.info( + f"ZMQ received msgpack text request for {len(texts)} texts" + ) + else: + raise ValueError("Not a valid msgpack text request") + except Exception: + logger.error("Both protobuf and msgpack parsing failed!") + # Send error response + resp_proto = embedding_pb2.NodeEmbeddingResponse() + rep_socket.send(resp_proto.SerializeToString()) + continue + + # Process the request + embeddings = compute_embeddings( + texts, + model_name, + mode=embedding_mode, + provider_options=PROVIDER_OPTIONS, + ) + logger.info(f"Computed embeddings shape: {embeddings.shape}") + + # Validation + if np.isnan(embeddings).any() or np.isinf(embeddings).any(): + logger.error("NaN or Inf detected in embeddings!") + # Send error response + if is_text_request: + import msgpack + + response_data = msgpack.packb([]) + else: + resp_proto = embedding_pb2.NodeEmbeddingResponse() + response_data = resp_proto.SerializeToString() + rep_socket.send(response_data) + continue + + # Prepare response based on request type + if is_text_request: + # For direct text requests, return msgpack + import msgpack + + response_data = msgpack.packb(embeddings.tolist()) + else: + # For protobuf requests, return protobuf + resp_proto = embedding_pb2.NodeEmbeddingResponse() + hidden_contiguous = np.ascontiguousarray(embeddings, dtype=np.float32) + + resp_proto.embeddings_data = hidden_contiguous.tobytes() + resp_proto.dimensions.append(hidden_contiguous.shape[0]) + resp_proto.dimensions.append(hidden_contiguous.shape[1]) + + response_data = resp_proto.SerializeToString() + + # Send response back to the client + rep_socket.send(response_data) + + e2e_end = time.time() + logger.info(f"⏱️ ZMQ E2E time: {e2e_end - e2e_start:.6f}s") + + except zmq.Again: + # Timeout - check shutdown_event and continue + continue + except Exception as e: + if not shutdown_event.is_set(): + logger.error(f"Error in ZMQ server loop: {e}") + try: + # Send error response for REP socket + resp_proto = embedding_pb2.NodeEmbeddingResponse() + rep_socket.send(resp_proto.SerializeToString()) + except Exception: + pass + else: + logger.info("Shutdown in progress, ignoring ZMQ error") + break + finally: + try: + rep_socket.close(0) + except Exception: + pass + try: + context.term() + except Exception: + pass + + logger.info("DiskANN ZMQ server thread exiting gracefully") + + # Add shutdown coordination + shutdown_event = threading.Event() + last_activity = [time.time()] + + def shutdown_zmq_server(): + """Gracefully shutdown ZMQ server.""" + logger.info("Initiating graceful shutdown...") + shutdown_event.set() + + if zmq_thread.is_alive(): + logger.info("Waiting for ZMQ thread to finish...") + zmq_thread.join(timeout=5) + if zmq_thread.is_alive(): + logger.warning("ZMQ thread did not finish in time") + + # Clean up ZMQ resources + try: + # Note: socket and context are cleaned up by thread exit + logger.info("ZMQ resources cleaned up") + except Exception as e: + logger.warning(f"Error cleaning ZMQ resources: {e}") + + # Clean up other resources + try: + import gc + + gc.collect() + logger.info("Additional resources cleaned up") + except Exception as e: + logger.warning(f"Error cleaning additional resources: {e}") + + logger.info("Graceful shutdown completed") + sys.exit(0) + + # Register signal handlers within this function scope + import signal + + def signal_handler(sig, frame): + logger.info(f"Received signal {sig}, shutting down gracefully...") + shutdown_zmq_server() + + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + # Start ZMQ thread (NOT daemon!) + zmq_thread = threading.Thread( + target=lambda: zmq_server_thread_with_shutdown(shutdown_event), + daemon=False, # Not daemon - we want to wait for it + ) + zmq_thread.start() + logger.info(f"Started DiskANN ZMQ server thread on port {zmq_port}") + + # Keep the main thread alive + try: + while not shutdown_event.is_set(): + if daemon_ttl > 0 and (time.time() - last_activity[0]) >= daemon_ttl: + logger.info( + f"No requests for {daemon_ttl} seconds, shutting down daemon on port {zmq_port}" + ) + shutdown_zmq_server() + return + time.sleep(0.1) # Check shutdown more frequently + except KeyboardInterrupt: + logger.info("DiskANN Server shutting down...") + shutdown_zmq_server() + return + + # If we reach here, shutdown was triggered by signal + logger.info("Main loop exited, process should be shutting down") + + +if __name__ == "__main__": + import sys + + # Signal handlers are now registered within create_diskann_embedding_server + + parser = argparse.ArgumentParser(description="DiskANN Embedding service") + parser.add_argument("--zmq-port", type=int, default=5555, help="ZMQ port to run on") + parser.add_argument( + "--passages-file", + type=str, + help="Metadata JSON file containing passage sources", + ) + parser.add_argument( + "--model-name", + type=str, + default="sentence-transformers/all-mpnet-base-v2", + help="Embedding model name", + ) + parser.add_argument( + "--embedding-mode", + type=str, + default="sentence-transformers", + choices=["sentence-transformers", "openai", "mlx", "ollama"], + help="Embedding backend mode", + ) + parser.add_argument( + "--distance-metric", + type=str, + default="l2", + choices=["l2", "mips", "cosine"], + help="Distance metric for similarity computation", + ) + parser.add_argument( + "--enable-warmup", + action="store_true", + help="Preload model by running one warmup embedding at startup", + ) + parser.add_argument( + "--daemon-mode", + action="store_true", + help="Run as daemon mode (enables idle TTL checks)", + ) + parser.add_argument( + "--daemon-ttl", + type=int, + default=0, + help="Idle TTL in seconds for daemon mode; 0 disables auto-exit", + ) + + args = parser.parse_args() + + # Create and start the DiskANN embedding server + create_diskann_embedding_server( + passages_file=args.passages_file, + zmq_port=args.zmq_port, + model_name=args.model_name, + embedding_mode=args.embedding_mode, + distance_metric=args.distance_metric, + enable_warmup=args.enable_warmup, + daemon_ttl=args.daemon_ttl if args.daemon_mode else 0, + ) diff --git a/packages/leann-backend-diskann/leann_backend_diskann/embedding_pb2.py b/packages/leann-backend-diskann/leann_backend_diskann/embedding_pb2.py new file mode 100644 index 0000000..9bedccd --- /dev/null +++ b/packages/leann-backend-diskann/leann_backend_diskann/embedding_pb2.py @@ -0,0 +1,28 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: embedding.proto +# ruff: noqa +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0f\x65mbedding.proto\x12\x0eprotoembedding"(\n\x14NodeEmbeddingRequest\x12\x10\n\x08node_ids\x18\x01 \x03(\r"Y\n\x15NodeEmbeddingResponse\x12\x17\n\x0f\x65mbeddings_data\x18\x01 \x01(\x0c\x12\x12\n\ndimensions\x18\x02 \x03(\x05\x12\x13\n\x0bmissing_ids\x18\x03 \x03(\rb\x06proto3' +) + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "embedding_pb2", globals()) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._options = None + _NODEEMBEDDINGREQUEST._serialized_start = 35 + _NODEEMBEDDINGREQUEST._serialized_end = 75 + _NODEEMBEDDINGRESPONSE._serialized_start = 77 + _NODEEMBEDDINGRESPONSE._serialized_end = 166 +# @@protoc_insertion_point(module_scope) diff --git a/packages/leann-backend-diskann/leann_backend_diskann/graph_partition.py b/packages/leann-backend-diskann/leann_backend_diskann/graph_partition.py new file mode 100644 index 0000000..4c85e28 --- /dev/null +++ b/packages/leann-backend-diskann/leann_backend_diskann/graph_partition.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +""" +Graph Partition Module for LEANN DiskANN Backend + +This module provides Python bindings for the graph partition functionality +of DiskANN, allowing users to partition disk-based indices for better +performance. +""" + +import os +import shutil +import subprocess +import tempfile +from pathlib import Path +from typing import Optional + + +class GraphPartitioner: + """ + A Python interface for DiskANN's graph partition functionality. + + This class provides methods to partition disk-based indices for improved + search performance and memory efficiency. + """ + + def __init__(self, build_type: str = "release"): + """ + Initialize the GraphPartitioner. + + Args: + build_type: Build type for the executables ("debug" or "release") + """ + self.build_type = build_type + self._ensure_executables() + + def _get_executable_path(self, name: str) -> str: + """Get the path to a graph partition executable.""" + # Get the directory where this Python module is located + module_dir = Path(__file__).parent + # Navigate to the graph_partition directory + graph_partition_dir = module_dir.parent / "third_party" / "DiskANN" / "graph_partition" + executable_path = graph_partition_dir / "build" / self.build_type / "graph_partition" / name + + if not executable_path.exists(): + raise FileNotFoundError(f"Executable {name} not found at {executable_path}") + + return str(executable_path) + + def _ensure_executables(self): + """Ensure that the required executables are built.""" + try: + self._get_executable_path("partitioner") + self._get_executable_path("index_relayout") + except FileNotFoundError: + # Try to build the executables automatically + print("Executables not found, attempting to build them...") + self._build_executables() + + def _build_executables(self): + """Build the required executables.""" + graph_partition_dir = ( + Path(__file__).parent.parent / "third_party" / "DiskANN" / "graph_partition" + ) + original_dir = os.getcwd() + + try: + os.chdir(graph_partition_dir) + + # Clean any existing build + if (graph_partition_dir / "build").exists(): + shutil.rmtree(graph_partition_dir / "build") + + # Run the build script + cmd = ["./build.sh", self.build_type, "split_graph", "/tmp/dummy"] + subprocess.run(cmd, capture_output=True, text=True, cwd=graph_partition_dir) + + # Check if executables were created + partitioner_path = self._get_executable_path("partitioner") + relayout_path = self._get_executable_path("index_relayout") + + print(f"βœ… Built partitioner: {partitioner_path}") + print(f"βœ… Built index_relayout: {relayout_path}") + + except Exception as e: + raise RuntimeError(f"Failed to build executables: {e}") + finally: + os.chdir(original_dir) + + def partition_graph( + self, + index_prefix_path: str, + output_dir: Optional[str] = None, + partition_prefix: Optional[str] = None, + **kwargs, + ) -> tuple[str, str]: + """ + Partition a disk-based index for improved performance. + + Args: + index_prefix_path: Path to the index prefix (e.g., "/path/to/index") + output_dir: Output directory for results (defaults to parent of index_prefix_path) + partition_prefix: Prefix for output files (defaults to basename of index_prefix_path) + **kwargs: Additional parameters for graph partitioning: + - gp_times: Number of LDG partition iterations (default: 10) + - lock_nums: Number of lock nodes (default: 10) + - cut: Cut adjacency list degree (default: 100) + - scale_factor: Scale factor (default: 1) + - data_type: Data type (default: "float") + - thread_nums: Number of threads (default: 10) + + Returns: + Tuple of (disk_graph_index_path, partition_bin_path) + + Raises: + RuntimeError: If the partitioning process fails + """ + # Set default parameters + params = { + "gp_times": 10, + "lock_nums": 10, + "cut": 100, + "scale_factor": 1, + "data_type": "float", + "thread_nums": 10, + **kwargs, + } + + # Determine output directory + if output_dir is None: + output_dir = str(Path(index_prefix_path).parent) + + # Create output directory if it doesn't exist + Path(output_dir).mkdir(parents=True, exist_ok=True) + + # Determine partition prefix + if partition_prefix is None: + partition_prefix = Path(index_prefix_path).name + + # Get executable paths + partitioner_path = self._get_executable_path("partitioner") + relayout_path = self._get_executable_path("index_relayout") + + # Create temporary directory for processing + with tempfile.TemporaryDirectory() as temp_dir: + # Change to the graph_partition directory for temporary files + graph_partition_dir = ( + Path(__file__).parent.parent / "third_party" / "DiskANN" / "graph_partition" + ) + original_dir = os.getcwd() + + try: + os.chdir(graph_partition_dir) + + # Create temporary data directory + temp_data_dir = Path(temp_dir) / "data" + temp_data_dir.mkdir(parents=True, exist_ok=True) + + # Set up paths for temporary files + graph_path = temp_data_dir / "starling" / "_M_R_L_B" / "GRAPH" + graph_gp_path = ( + graph_path + / f"GP_TIMES_{params['gp_times']}_LOCK_{params['lock_nums']}_GP_USE_FREQ0_CUT{params['cut']}_SCALE{params['scale_factor']}" + ) + graph_gp_path.mkdir(parents=True, exist_ok=True) + + # Find input index file + old_index_file = f"{index_prefix_path}_disk_beam_search.index" + if not os.path.exists(old_index_file): + old_index_file = f"{index_prefix_path}_disk.index" + + if not os.path.exists(old_index_file): + raise RuntimeError(f"Index file not found: {old_index_file}") + + # Run partitioner + gp_file_path = graph_gp_path / "_part.bin" + partitioner_cmd = [ + partitioner_path, + "--index_file", + old_index_file, + "--data_type", + params["data_type"], + "--gp_file", + str(gp_file_path), + "-T", + str(params["thread_nums"]), + "--ldg_times", + str(params["gp_times"]), + "--scale", + str(params["scale_factor"]), + "--mode", + "1", + ] + + print(f"Running partitioner: {' '.join(partitioner_cmd)}") + result = subprocess.run( + partitioner_cmd, capture_output=True, text=True, cwd=graph_partition_dir + ) + + if result.returncode != 0: + raise RuntimeError( + f"Partitioner failed with return code {result.returncode}.\n" + f"stdout: {result.stdout}\n" + f"stderr: {result.stderr}" + ) + + # Run relayout + part_tmp_index = graph_gp_path / "_part_tmp.index" + relayout_cmd = [ + relayout_path, + old_index_file, + str(gp_file_path), + params["data_type"], + "1", + ] + + print(f"Running relayout: {' '.join(relayout_cmd)}") + result = subprocess.run( + relayout_cmd, capture_output=True, text=True, cwd=graph_partition_dir + ) + + if result.returncode != 0: + raise RuntimeError( + f"Relayout failed with return code {result.returncode}.\n" + f"stdout: {result.stdout}\n" + f"stderr: {result.stderr}" + ) + + # Copy results to output directory + disk_graph_path = Path(output_dir) / f"{partition_prefix}_disk_graph.index" + partition_bin_path = Path(output_dir) / f"{partition_prefix}_partition.bin" + + shutil.copy2(part_tmp_index, disk_graph_path) + shutil.copy2(gp_file_path, partition_bin_path) + + print(f"Results copied to: {output_dir}") + return str(disk_graph_path), str(partition_bin_path) + + finally: + os.chdir(original_dir) + + def get_partition_info(self, partition_bin_path: str) -> dict: + """ + Get information about a partition file. + + Args: + partition_bin_path: Path to the partition binary file + + Returns: + Dictionary containing partition information + """ + if not os.path.exists(partition_bin_path): + raise FileNotFoundError(f"Partition file not found: {partition_bin_path}") + + # For now, return basic file information + # In the future, this could parse the binary file for detailed info + stat = os.stat(partition_bin_path) + return { + "file_size": stat.st_size, + "file_path": partition_bin_path, + "modified_time": stat.st_mtime, + } + + +def partition_graph( + index_prefix_path: str, + output_dir: Optional[str] = None, + partition_prefix: Optional[str] = None, + build_type: str = "release", + **kwargs, +) -> tuple[str, str]: + """ + Convenience function to partition a graph index. + + Args: + index_prefix_path: Path to the index prefix + output_dir: Output directory (defaults to parent of index_prefix_path) + partition_prefix: Prefix for output files (defaults to basename of index_prefix_path) + build_type: Build type for executables ("debug" or "release") + **kwargs: Additional parameters for graph partitioning + + Returns: + Tuple of (disk_graph_index_path, partition_bin_path) + """ + partitioner = GraphPartitioner(build_type=build_type) + return partitioner.partition_graph(index_prefix_path, output_dir, partition_prefix, **kwargs) + + +# Example usage: +if __name__ == "__main__": + # Example: partition an index + try: + disk_graph_path, partition_bin_path = partition_graph( + "/path/to/your/index_prefix", gp_times=10, lock_nums=10, cut=100 + ) + print("Partitioning completed successfully!") + print(f"Disk graph index: {disk_graph_path}") + print(f"Partition binary: {partition_bin_path}") + except Exception as e: + print(f"Partitioning failed: {e}") diff --git a/packages/leann-backend-diskann/pyproject.toml b/packages/leann-backend-diskann/pyproject.toml new file mode 100644 index 0000000..3dfa801 --- /dev/null +++ b/packages/leann-backend-diskann/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +# Like leann-backend-hnsw: rely on system CMake on PATH (CI images + brew/apt ship it). +# Listing cmake here forces a PyPI fetch during isolated builds and has flaked on +# transient network timeouts (e.g. macOS runners failing to reach pypi.org/simple/cmake/). +requires = ["scikit-build-core>=0.10", "pybind11>=2.12.0", "numpy"] +build-backend = "scikit_build_core.build" + +[project] +name = "leann-backend-diskann" +version = "0.3.7" +dependencies = ["leann-core==0.3.7", "numpy", "protobuf>=3.19.0"] + +[tool.scikit-build] +# Root CMake forces USE_TCMALLOC=OFF before DiskANN options (see CMakeLists.txt). +cmake.source-dir = "." +# Key: Python package in root directory, paths match exactly +wheel.packages = ["leann_backend_diskann"] +# Use default redirect mode +editable.mode = "redirect" +cmake.build-type = "Release" +build.verbose = true +# Let CMake find packages via Homebrew prefix +cmake.define = {CMAKE_PREFIX_PATH = {env = "CMAKE_PREFIX_PATH"}, OpenMP_ROOT = {env = "OpenMP_ROOT"}, USE_TCMALLOC = "OFF"} diff --git a/packages/leann-backend-diskann/third_party/embedding.pb.cc b/packages/leann-backend-diskann/third_party/embedding.pb.cc new file mode 100644 index 0000000..2b10e23 --- /dev/null +++ b/packages/leann-backend-diskann/third_party/embedding.pb.cc @@ -0,0 +1,613 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: embedding.proto + +#include "embedding.pb.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +// @@protoc_insertion_point(includes) +#include +namespace protoembedding { +class NodeEmbeddingRequestDefaultTypeInternal { + public: + ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed _instance; +} _NodeEmbeddingRequest_default_instance_; +class NodeEmbeddingResponseDefaultTypeInternal { + public: + ::PROTOBUF_NAMESPACE_ID::internal::ExplicitlyConstructed _instance; +} _NodeEmbeddingResponse_default_instance_; +} // namespace protoembedding +static void InitDefaultsscc_info_NodeEmbeddingRequest_embedding_2eproto() { + GOOGLE_PROTOBUF_VERIFY_VERSION; + + { + void* ptr = &::protoembedding::_NodeEmbeddingRequest_default_instance_; + new (ptr) ::protoembedding::NodeEmbeddingRequest(); + ::PROTOBUF_NAMESPACE_ID::internal::OnShutdownDestroyMessage(ptr); + } + ::protoembedding::NodeEmbeddingRequest::InitAsDefaultInstance(); +} + +::PROTOBUF_NAMESPACE_ID::internal::SCCInfo<0> scc_info_NodeEmbeddingRequest_embedding_2eproto = + {{ATOMIC_VAR_INIT(::PROTOBUF_NAMESPACE_ID::internal::SCCInfoBase::kUninitialized), 0, 0, InitDefaultsscc_info_NodeEmbeddingRequest_embedding_2eproto}, {}}; + +static void InitDefaultsscc_info_NodeEmbeddingResponse_embedding_2eproto() { + GOOGLE_PROTOBUF_VERIFY_VERSION; + + { + void* ptr = &::protoembedding::_NodeEmbeddingResponse_default_instance_; + new (ptr) ::protoembedding::NodeEmbeddingResponse(); + ::PROTOBUF_NAMESPACE_ID::internal::OnShutdownDestroyMessage(ptr); + } + ::protoembedding::NodeEmbeddingResponse::InitAsDefaultInstance(); +} + +::PROTOBUF_NAMESPACE_ID::internal::SCCInfo<0> scc_info_NodeEmbeddingResponse_embedding_2eproto = + {{ATOMIC_VAR_INIT(::PROTOBUF_NAMESPACE_ID::internal::SCCInfoBase::kUninitialized), 0, 0, InitDefaultsscc_info_NodeEmbeddingResponse_embedding_2eproto}, {}}; + +static ::PROTOBUF_NAMESPACE_ID::Metadata file_level_metadata_embedding_2eproto[2]; +static constexpr ::PROTOBUF_NAMESPACE_ID::EnumDescriptor const** file_level_enum_descriptors_embedding_2eproto = nullptr; +static constexpr ::PROTOBUF_NAMESPACE_ID::ServiceDescriptor const** file_level_service_descriptors_embedding_2eproto = nullptr; + +const ::PROTOBUF_NAMESPACE_ID::uint32 TableStruct_embedding_2eproto::offsets[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) = { + ~0u, // no _has_bits_ + PROTOBUF_FIELD_OFFSET(::protoembedding::NodeEmbeddingRequest, _internal_metadata_), + ~0u, // no _extensions_ + ~0u, // no _oneof_case_ + ~0u, // no _weak_field_map_ + PROTOBUF_FIELD_OFFSET(::protoembedding::NodeEmbeddingRequest, node_ids_), + ~0u, // no _has_bits_ + PROTOBUF_FIELD_OFFSET(::protoembedding::NodeEmbeddingResponse, _internal_metadata_), + ~0u, // no _extensions_ + ~0u, // no _oneof_case_ + ~0u, // no _weak_field_map_ + PROTOBUF_FIELD_OFFSET(::protoembedding::NodeEmbeddingResponse, embeddings_data_), + PROTOBUF_FIELD_OFFSET(::protoembedding::NodeEmbeddingResponse, dimensions_), + PROTOBUF_FIELD_OFFSET(::protoembedding::NodeEmbeddingResponse, missing_ids_), +}; +static const ::PROTOBUF_NAMESPACE_ID::internal::MigrationSchema schemas[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) = { + { 0, -1, sizeof(::protoembedding::NodeEmbeddingRequest)}, + { 6, -1, sizeof(::protoembedding::NodeEmbeddingResponse)}, +}; + +static ::PROTOBUF_NAMESPACE_ID::Message const * const file_default_instances[] = { + reinterpret_cast(&::protoembedding::_NodeEmbeddingRequest_default_instance_), + reinterpret_cast(&::protoembedding::_NodeEmbeddingResponse_default_instance_), +}; + +const char descriptor_table_protodef_embedding_2eproto[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) = + "\n\017embedding.proto\022\016protoembedding\"(\n\024Nod" + "eEmbeddingRequest\022\020\n\010node_ids\030\001 \003(\r\"Y\n\025N" + "odeEmbeddingResponse\022\027\n\017embeddings_data\030" + "\001 \001(\014\022\022\n\ndimensions\030\002 \003(\005\022\023\n\013missing_ids" + "\030\003 \003(\rb\006proto3" + ; +static const ::PROTOBUF_NAMESPACE_ID::internal::DescriptorTable*const descriptor_table_embedding_2eproto_deps[1] = { +}; +static ::PROTOBUF_NAMESPACE_ID::internal::SCCInfoBase*const descriptor_table_embedding_2eproto_sccs[2] = { + &scc_info_NodeEmbeddingRequest_embedding_2eproto.base, + &scc_info_NodeEmbeddingResponse_embedding_2eproto.base, +}; +static ::PROTOBUF_NAMESPACE_ID::internal::once_flag descriptor_table_embedding_2eproto_once; +const ::PROTOBUF_NAMESPACE_ID::internal::DescriptorTable descriptor_table_embedding_2eproto = { + false, false, descriptor_table_protodef_embedding_2eproto, "embedding.proto", 174, + &descriptor_table_embedding_2eproto_once, descriptor_table_embedding_2eproto_sccs, descriptor_table_embedding_2eproto_deps, 2, 0, + schemas, file_default_instances, TableStruct_embedding_2eproto::offsets, + file_level_metadata_embedding_2eproto, 2, file_level_enum_descriptors_embedding_2eproto, file_level_service_descriptors_embedding_2eproto, +}; + +// Force running AddDescriptors() at dynamic initialization time. +static bool dynamic_init_dummy_embedding_2eproto = (static_cast(::PROTOBUF_NAMESPACE_ID::internal::AddDescriptors(&descriptor_table_embedding_2eproto)), true); +namespace protoembedding { + +// =================================================================== + +void NodeEmbeddingRequest::InitAsDefaultInstance() { +} +class NodeEmbeddingRequest::_Internal { + public: +}; + +NodeEmbeddingRequest::NodeEmbeddingRequest(::PROTOBUF_NAMESPACE_ID::Arena* arena) + : ::PROTOBUF_NAMESPACE_ID::Message(arena), + node_ids_(arena) { + SharedCtor(); + RegisterArenaDtor(arena); + // @@protoc_insertion_point(arena_constructor:protoembedding.NodeEmbeddingRequest) +} +NodeEmbeddingRequest::NodeEmbeddingRequest(const NodeEmbeddingRequest& from) + : ::PROTOBUF_NAMESPACE_ID::Message(), + node_ids_(from.node_ids_) { + _internal_metadata_.MergeFrom<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(from._internal_metadata_); + // @@protoc_insertion_point(copy_constructor:protoembedding.NodeEmbeddingRequest) +} + +void NodeEmbeddingRequest::SharedCtor() { +} + +NodeEmbeddingRequest::~NodeEmbeddingRequest() { + // @@protoc_insertion_point(destructor:protoembedding.NodeEmbeddingRequest) + SharedDtor(); + _internal_metadata_.Delete<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(); +} + +void NodeEmbeddingRequest::SharedDtor() { + GOOGLE_DCHECK(GetArena() == nullptr); +} + +void NodeEmbeddingRequest::ArenaDtor(void* object) { + NodeEmbeddingRequest* _this = reinterpret_cast< NodeEmbeddingRequest* >(object); + (void)_this; +} +void NodeEmbeddingRequest::RegisterArenaDtor(::PROTOBUF_NAMESPACE_ID::Arena*) { +} +void NodeEmbeddingRequest::SetCachedSize(int size) const { + _cached_size_.Set(size); +} +const NodeEmbeddingRequest& NodeEmbeddingRequest::default_instance() { + ::PROTOBUF_NAMESPACE_ID::internal::InitSCC(&::scc_info_NodeEmbeddingRequest_embedding_2eproto.base); + return *internal_default_instance(); +} + + +void NodeEmbeddingRequest::Clear() { +// @@protoc_insertion_point(message_clear_start:protoembedding.NodeEmbeddingRequest) + ::PROTOBUF_NAMESPACE_ID::uint32 cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + node_ids_.Clear(); + _internal_metadata_.Clear<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(); +} + +const char* NodeEmbeddingRequest::_InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + ::PROTOBUF_NAMESPACE_ID::Arena* arena = GetArena(); (void)arena; + while (!ctx->Done(&ptr)) { + ::PROTOBUF_NAMESPACE_ID::uint32 tag; + ptr = ::PROTOBUF_NAMESPACE_ID::internal::ReadTag(ptr, &tag); + CHK_(ptr); + switch (tag >> 3) { + // repeated uint32 node_ids = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast<::PROTOBUF_NAMESPACE_ID::uint8>(tag) == 10)) { + ptr = ::PROTOBUF_NAMESPACE_ID::internal::PackedUInt32Parser(_internal_mutable_node_ids(), ptr, ctx); + CHK_(ptr); + } else if (static_cast<::PROTOBUF_NAMESPACE_ID::uint8>(tag) == 8) { + _internal_add_node_ids(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr)); + CHK_(ptr); + } else goto handle_unusual; + continue; + default: { + handle_unusual: + if ((tag & 7) == 4 || tag == 0) { + ctx->SetLastTag(tag); + goto success; + } + ptr = UnknownFieldParse(tag, + _internal_metadata_.mutable_unknown_fields<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(), + ptr, ctx); + CHK_(ptr != nullptr); + continue; + } + } // switch + } // while +success: + return ptr; +failure: + ptr = nullptr; + goto success; +#undef CHK_ +} + +::PROTOBUF_NAMESPACE_ID::uint8* NodeEmbeddingRequest::_InternalSerialize( + ::PROTOBUF_NAMESPACE_ID::uint8* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:protoembedding.NodeEmbeddingRequest) + ::PROTOBUF_NAMESPACE_ID::uint32 cached_has_bits = 0; + (void) cached_has_bits; + + // repeated uint32 node_ids = 1; + { + int byte_size = _node_ids_cached_byte_size_.load(std::memory_order_relaxed); + if (byte_size > 0) { + target = stream->WriteUInt32Packed( + 1, _internal_node_ids(), byte_size, target); + } + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormat::InternalSerializeUnknownFieldsToArray( + _internal_metadata_.unknown_fields<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(::PROTOBUF_NAMESPACE_ID::UnknownFieldSet::default_instance), target, stream); + } + // @@protoc_insertion_point(serialize_to_array_end:protoembedding.NodeEmbeddingRequest) + return target; +} + +size_t NodeEmbeddingRequest::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:protoembedding.NodeEmbeddingRequest) + size_t total_size = 0; + + ::PROTOBUF_NAMESPACE_ID::uint32 cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated uint32 node_ids = 1; + { + size_t data_size = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + UInt32Size(this->node_ids_); + if (data_size > 0) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::Int32Size( + static_cast<::PROTOBUF_NAMESPACE_ID::int32>(data_size)); + } + int cached_size = ::PROTOBUF_NAMESPACE_ID::internal::ToCachedSize(data_size); + _node_ids_cached_byte_size_.store(cached_size, + std::memory_order_relaxed); + total_size += data_size; + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + return ::PROTOBUF_NAMESPACE_ID::internal::ComputeUnknownFieldsSize( + _internal_metadata_, total_size, &_cached_size_); + } + int cached_size = ::PROTOBUF_NAMESPACE_ID::internal::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void NodeEmbeddingRequest::MergeFrom(const ::PROTOBUF_NAMESPACE_ID::Message& from) { +// @@protoc_insertion_point(generalized_merge_from_start:protoembedding.NodeEmbeddingRequest) + GOOGLE_DCHECK_NE(&from, this); + const NodeEmbeddingRequest* source = + ::PROTOBUF_NAMESPACE_ID::DynamicCastToGenerated( + &from); + if (source == nullptr) { + // @@protoc_insertion_point(generalized_merge_from_cast_fail:protoembedding.NodeEmbeddingRequest) + ::PROTOBUF_NAMESPACE_ID::internal::ReflectionOps::Merge(from, this); + } else { + // @@protoc_insertion_point(generalized_merge_from_cast_success:protoembedding.NodeEmbeddingRequest) + MergeFrom(*source); + } +} + +void NodeEmbeddingRequest::MergeFrom(const NodeEmbeddingRequest& from) { +// @@protoc_insertion_point(class_specific_merge_from_start:protoembedding.NodeEmbeddingRequest) + GOOGLE_DCHECK_NE(&from, this); + _internal_metadata_.MergeFrom<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(from._internal_metadata_); + ::PROTOBUF_NAMESPACE_ID::uint32 cached_has_bits = 0; + (void) cached_has_bits; + + node_ids_.MergeFrom(from.node_ids_); +} + +void NodeEmbeddingRequest::CopyFrom(const ::PROTOBUF_NAMESPACE_ID::Message& from) { +// @@protoc_insertion_point(generalized_copy_from_start:protoembedding.NodeEmbeddingRequest) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +void NodeEmbeddingRequest::CopyFrom(const NodeEmbeddingRequest& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:protoembedding.NodeEmbeddingRequest) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool NodeEmbeddingRequest::IsInitialized() const { + return true; +} + +void NodeEmbeddingRequest::InternalSwap(NodeEmbeddingRequest* other) { + using std::swap; + _internal_metadata_.Swap<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(&other->_internal_metadata_); + node_ids_.InternalSwap(&other->node_ids_); +} + +::PROTOBUF_NAMESPACE_ID::Metadata NodeEmbeddingRequest::GetMetadata() const { + return GetMetadataStatic(); +} + + +// =================================================================== + +void NodeEmbeddingResponse::InitAsDefaultInstance() { +} +class NodeEmbeddingResponse::_Internal { + public: +}; + +NodeEmbeddingResponse::NodeEmbeddingResponse(::PROTOBUF_NAMESPACE_ID::Arena* arena) + : ::PROTOBUF_NAMESPACE_ID::Message(arena), + dimensions_(arena), + missing_ids_(arena) { + SharedCtor(); + RegisterArenaDtor(arena); + // @@protoc_insertion_point(arena_constructor:protoembedding.NodeEmbeddingResponse) +} +NodeEmbeddingResponse::NodeEmbeddingResponse(const NodeEmbeddingResponse& from) + : ::PROTOBUF_NAMESPACE_ID::Message(), + dimensions_(from.dimensions_), + missing_ids_(from.missing_ids_) { + _internal_metadata_.MergeFrom<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(from._internal_metadata_); + embeddings_data_.UnsafeSetDefault(&::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited()); + if (!from._internal_embeddings_data().empty()) { + embeddings_data_.Set(&::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited(), from._internal_embeddings_data(), + GetArena()); + } + // @@protoc_insertion_point(copy_constructor:protoembedding.NodeEmbeddingResponse) +} + +void NodeEmbeddingResponse::SharedCtor() { + ::PROTOBUF_NAMESPACE_ID::internal::InitSCC(&scc_info_NodeEmbeddingResponse_embedding_2eproto.base); + embeddings_data_.UnsafeSetDefault(&::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited()); +} + +NodeEmbeddingResponse::~NodeEmbeddingResponse() { + // @@protoc_insertion_point(destructor:protoembedding.NodeEmbeddingResponse) + SharedDtor(); + _internal_metadata_.Delete<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(); +} + +void NodeEmbeddingResponse::SharedDtor() { + GOOGLE_DCHECK(GetArena() == nullptr); + embeddings_data_.DestroyNoArena(&::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited()); +} + +void NodeEmbeddingResponse::ArenaDtor(void* object) { + NodeEmbeddingResponse* _this = reinterpret_cast< NodeEmbeddingResponse* >(object); + (void)_this; +} +void NodeEmbeddingResponse::RegisterArenaDtor(::PROTOBUF_NAMESPACE_ID::Arena*) { +} +void NodeEmbeddingResponse::SetCachedSize(int size) const { + _cached_size_.Set(size); +} +const NodeEmbeddingResponse& NodeEmbeddingResponse::default_instance() { + ::PROTOBUF_NAMESPACE_ID::internal::InitSCC(&::scc_info_NodeEmbeddingResponse_embedding_2eproto.base); + return *internal_default_instance(); +} + + +void NodeEmbeddingResponse::Clear() { +// @@protoc_insertion_point(message_clear_start:protoembedding.NodeEmbeddingResponse) + ::PROTOBUF_NAMESPACE_ID::uint32 cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + dimensions_.Clear(); + missing_ids_.Clear(); + embeddings_data_.ClearToEmpty(&::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited(), GetArena()); + _internal_metadata_.Clear<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(); +} + +const char* NodeEmbeddingResponse::_InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + ::PROTOBUF_NAMESPACE_ID::Arena* arena = GetArena(); (void)arena; + while (!ctx->Done(&ptr)) { + ::PROTOBUF_NAMESPACE_ID::uint32 tag; + ptr = ::PROTOBUF_NAMESPACE_ID::internal::ReadTag(ptr, &tag); + CHK_(ptr); + switch (tag >> 3) { + // bytes embeddings_data = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast<::PROTOBUF_NAMESPACE_ID::uint8>(tag) == 10)) { + auto str = _internal_mutable_embeddings_data(); + ptr = ::PROTOBUF_NAMESPACE_ID::internal::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else goto handle_unusual; + continue; + // repeated int32 dimensions = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast<::PROTOBUF_NAMESPACE_ID::uint8>(tag) == 18)) { + ptr = ::PROTOBUF_NAMESPACE_ID::internal::PackedInt32Parser(_internal_mutable_dimensions(), ptr, ctx); + CHK_(ptr); + } else if (static_cast<::PROTOBUF_NAMESPACE_ID::uint8>(tag) == 16) { + _internal_add_dimensions(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr)); + CHK_(ptr); + } else goto handle_unusual; + continue; + // repeated uint32 missing_ids = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast<::PROTOBUF_NAMESPACE_ID::uint8>(tag) == 26)) { + ptr = ::PROTOBUF_NAMESPACE_ID::internal::PackedUInt32Parser(_internal_mutable_missing_ids(), ptr, ctx); + CHK_(ptr); + } else if (static_cast<::PROTOBUF_NAMESPACE_ID::uint8>(tag) == 24) { + _internal_add_missing_ids(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr)); + CHK_(ptr); + } else goto handle_unusual; + continue; + default: { + handle_unusual: + if ((tag & 7) == 4 || tag == 0) { + ctx->SetLastTag(tag); + goto success; + } + ptr = UnknownFieldParse(tag, + _internal_metadata_.mutable_unknown_fields<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(), + ptr, ctx); + CHK_(ptr != nullptr); + continue; + } + } // switch + } // while +success: + return ptr; +failure: + ptr = nullptr; + goto success; +#undef CHK_ +} + +::PROTOBUF_NAMESPACE_ID::uint8* NodeEmbeddingResponse::_InternalSerialize( + ::PROTOBUF_NAMESPACE_ID::uint8* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:protoembedding.NodeEmbeddingResponse) + ::PROTOBUF_NAMESPACE_ID::uint32 cached_has_bits = 0; + (void) cached_has_bits; + + // bytes embeddings_data = 1; + if (this->embeddings_data().size() > 0) { + target = stream->WriteBytesMaybeAliased( + 1, this->_internal_embeddings_data(), target); + } + + // repeated int32 dimensions = 2; + { + int byte_size = _dimensions_cached_byte_size_.load(std::memory_order_relaxed); + if (byte_size > 0) { + target = stream->WriteInt32Packed( + 2, _internal_dimensions(), byte_size, target); + } + } + + // repeated uint32 missing_ids = 3; + { + int byte_size = _missing_ids_cached_byte_size_.load(std::memory_order_relaxed); + if (byte_size > 0) { + target = stream->WriteUInt32Packed( + 3, _internal_missing_ids(), byte_size, target); + } + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormat::InternalSerializeUnknownFieldsToArray( + _internal_metadata_.unknown_fields<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(::PROTOBUF_NAMESPACE_ID::UnknownFieldSet::default_instance), target, stream); + } + // @@protoc_insertion_point(serialize_to_array_end:protoembedding.NodeEmbeddingResponse) + return target; +} + +size_t NodeEmbeddingResponse::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:protoembedding.NodeEmbeddingResponse) + size_t total_size = 0; + + ::PROTOBUF_NAMESPACE_ID::uint32 cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated int32 dimensions = 2; + { + size_t data_size = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + Int32Size(this->dimensions_); + if (data_size > 0) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::Int32Size( + static_cast<::PROTOBUF_NAMESPACE_ID::int32>(data_size)); + } + int cached_size = ::PROTOBUF_NAMESPACE_ID::internal::ToCachedSize(data_size); + _dimensions_cached_byte_size_.store(cached_size, + std::memory_order_relaxed); + total_size += data_size; + } + + // repeated uint32 missing_ids = 3; + { + size_t data_size = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + UInt32Size(this->missing_ids_); + if (data_size > 0) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::Int32Size( + static_cast<::PROTOBUF_NAMESPACE_ID::int32>(data_size)); + } + int cached_size = ::PROTOBUF_NAMESPACE_ID::internal::ToCachedSize(data_size); + _missing_ids_cached_byte_size_.store(cached_size, + std::memory_order_relaxed); + total_size += data_size; + } + + // bytes embeddings_data = 1; + if (this->embeddings_data().size() > 0) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_embeddings_data()); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + return ::PROTOBUF_NAMESPACE_ID::internal::ComputeUnknownFieldsSize( + _internal_metadata_, total_size, &_cached_size_); + } + int cached_size = ::PROTOBUF_NAMESPACE_ID::internal::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void NodeEmbeddingResponse::MergeFrom(const ::PROTOBUF_NAMESPACE_ID::Message& from) { +// @@protoc_insertion_point(generalized_merge_from_start:protoembedding.NodeEmbeddingResponse) + GOOGLE_DCHECK_NE(&from, this); + const NodeEmbeddingResponse* source = + ::PROTOBUF_NAMESPACE_ID::DynamicCastToGenerated( + &from); + if (source == nullptr) { + // @@protoc_insertion_point(generalized_merge_from_cast_fail:protoembedding.NodeEmbeddingResponse) + ::PROTOBUF_NAMESPACE_ID::internal::ReflectionOps::Merge(from, this); + } else { + // @@protoc_insertion_point(generalized_merge_from_cast_success:protoembedding.NodeEmbeddingResponse) + MergeFrom(*source); + } +} + +void NodeEmbeddingResponse::MergeFrom(const NodeEmbeddingResponse& from) { +// @@protoc_insertion_point(class_specific_merge_from_start:protoembedding.NodeEmbeddingResponse) + GOOGLE_DCHECK_NE(&from, this); + _internal_metadata_.MergeFrom<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(from._internal_metadata_); + ::PROTOBUF_NAMESPACE_ID::uint32 cached_has_bits = 0; + (void) cached_has_bits; + + dimensions_.MergeFrom(from.dimensions_); + missing_ids_.MergeFrom(from.missing_ids_); + if (from.embeddings_data().size() > 0) { + _internal_set_embeddings_data(from._internal_embeddings_data()); + } +} + +void NodeEmbeddingResponse::CopyFrom(const ::PROTOBUF_NAMESPACE_ID::Message& from) { +// @@protoc_insertion_point(generalized_copy_from_start:protoembedding.NodeEmbeddingResponse) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +void NodeEmbeddingResponse::CopyFrom(const NodeEmbeddingResponse& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:protoembedding.NodeEmbeddingResponse) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool NodeEmbeddingResponse::IsInitialized() const { + return true; +} + +void NodeEmbeddingResponse::InternalSwap(NodeEmbeddingResponse* other) { + using std::swap; + _internal_metadata_.Swap<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(&other->_internal_metadata_); + dimensions_.InternalSwap(&other->dimensions_); + missing_ids_.InternalSwap(&other->missing_ids_); + embeddings_data_.Swap(&other->embeddings_data_, &::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited(), GetArena()); +} + +::PROTOBUF_NAMESPACE_ID::Metadata NodeEmbeddingResponse::GetMetadata() const { + return GetMetadataStatic(); +} + + +// @@protoc_insertion_point(namespace_scope) +} // namespace protoembedding +PROTOBUF_NAMESPACE_OPEN +template<> PROTOBUF_NOINLINE ::protoembedding::NodeEmbeddingRequest* Arena::CreateMaybeMessage< ::protoembedding::NodeEmbeddingRequest >(Arena* arena) { + return Arena::CreateMessageInternal< ::protoembedding::NodeEmbeddingRequest >(arena); +} +template<> PROTOBUF_NOINLINE ::protoembedding::NodeEmbeddingResponse* Arena::CreateMaybeMessage< ::protoembedding::NodeEmbeddingResponse >(Arena* arena) { + return Arena::CreateMessageInternal< ::protoembedding::NodeEmbeddingResponse >(arena); +} +PROTOBUF_NAMESPACE_CLOSE + +// @@protoc_insertion_point(global_scope) +#include diff --git a/packages/leann-backend-diskann/third_party/embedding.proto b/packages/leann-backend-diskann/third_party/embedding.proto new file mode 100644 index 0000000..8481b39 --- /dev/null +++ b/packages/leann-backend-diskann/third_party/embedding.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package protoembedding; + +message NodeEmbeddingRequest { + repeated uint32 node_ids = 1; +} + +message NodeEmbeddingResponse { + bytes embeddings_data = 1; // All embedded binary datas + repeated int32 dimensions = 2; // Shape [batch_size, embedding_dim] + repeated uint32 missing_ids = 3; // Missing node ids +} diff --git a/packages/leann-backend-flashlib-ivf/README.md b/packages/leann-backend-flashlib-ivf/README.md new file mode 100644 index 0000000..79fb38a --- /dev/null +++ b/packages/leann-backend-flashlib-ivf/README.md @@ -0,0 +1,71 @@ +# leann-backend-flashlib-ivf + +GPU-accelerated [FlashLib](https://github.com/FlashML-org/flashlib) IVF-Flat +(inverted file) backend for LEANN - the GPU counterpart of the FAISS +[`leann-backend-ivf`](../leann-backend-ivf) backend. + +FlashLib is a GPU library of classical ML operators built on Triton / CuteDSL. +Its IVF-Flat index coarse-quantizes the corpus into `nlist` cells and, at search +time, scans only the `nprobe` nearest cells - entirely on CUDA tensors. At a fixed +`(nlist, nprobe)` it probes the same candidate set as a reference IVF-Flat +(FAISS / cuVS), so recall is comparable; the difference is GPU vs CPU kernels. + +This is registered as the `flashlib_ivf` backend, distinct from the exact GPU +k-NN `flashlib` backend (which does brute-force `NearestNeighbors`, not IVF). + +## Requirements + +- A CUDA GPU (required at **build** time for k-means training and at **search** time). +- `pip install flashlib` and `torch`. + +## Install + +```bash +# from a LEANN checkout +uv sync --extra flashlib-ivf +# or +pip install leann-backend-flashlib-ivf +``` + +## Usage + +```python +from leann import LeannBuilder, LeannSearcher + +builder = LeannBuilder(backend_name="flashlib_ivf", nlist=1024, distance_metric="cosine") +builder.add_text("LEANN recomputes embeddings to save storage.") +builder.build_index("demo.leann") + +searcher = LeannSearcher("demo.leann") +print(searcher.search("How does LEANN save storage?", top_k=3, complexity=32)) # nprobe=min(32, nlist) +``` + +Or from the example apps: + +```bash +python -m apps.document_rag --query "What are the main techniques LEANN explores?" \ + --backend-name flashlib_ivf +``` + +## How it works + +The built IVF-Flat index is a small set of torch tensors (centroids, +cell-contiguous data, row ids, CSR offsets), so this backend persists it with +`torch.save` (`.flashlib_ivf.pt`) plus an id map +(`.flashlib_ivf_id_map.json`) and reloads it onto the GPU at searcher +start-up - no k-means re-train. + +FlashLib's only distance metric is squared L2. For `mips` / `cosine` the vectors +are L2-normalized at build and query time, on which squared-L2 ranking is +equivalent to inner-product / cosine ranking. + +## Parameters + +| kwarg | default | meaning | +|-------|---------|---------| +| `nlist` | `1024` | number of IVF partitions / coarse centroids (clamped to corpus size) | +| `nprobe` (build) | `16` | default partitions probed per query (recall knob) | +| `niter` | `20` | Lloyd k-means iterations for the coarse quantizer | +| `seed` | `0` | RNG seed (deterministic build) | +| `distance_metric` | `"mips"` | `mips`, `cosine`, or `l2` | +| `complexity` (search) | `64` | sets `nprobe = min(complexity, nlist)` when `nprobe` is not given | diff --git a/packages/leann-backend-flashlib-ivf/leann_backend_flashlib_ivf/__init__.py b/packages/leann-backend-flashlib-ivf/leann_backend_flashlib_ivf/__init__.py new file mode 100644 index 0000000..25fdeaa --- /dev/null +++ b/packages/leann-backend-flashlib-ivf/leann_backend_flashlib_ivf/__init__.py @@ -0,0 +1,13 @@ +"""LEANN FlashLib IVF backend: GPU-accelerated IVF-Flat (Triton/CuteDSL) approximate-NN search.""" + +from .flashlib_ivf_backend import ( + FlashlibIVFBackend, + FlashlibIVFBuilder, + FlashlibIVFSearcher, +) + +__all__ = [ + "FlashlibIVFBackend", + "FlashlibIVFBuilder", + "FlashlibIVFSearcher", +] diff --git a/packages/leann-backend-flashlib-ivf/leann_backend_flashlib_ivf/flashlib_ivf_backend.py b/packages/leann-backend-flashlib-ivf/leann_backend_flashlib_ivf/flashlib_ivf_backend.py new file mode 100644 index 0000000..71eff52 --- /dev/null +++ b/packages/leann-backend-flashlib-ivf/leann_backend_flashlib_ivf/flashlib_ivf_backend.py @@ -0,0 +1,266 @@ +""" +FlashLib IVF backend: GPU-accelerated IVF-Flat (inverted file) ANN search. + +FlashLib (https://github.com/FlashML-org/flashlib) is a GPU library of classical +ML primitives. This backend uses its IVF-Flat index, which runs an *approximate* +nearest-neighbor search entirely on CUDA tensors: + + from flashlib import flash_ivf_flat_build, flash_ivf_flat_search + index = flash_ivf_flat_build(db_cuda, nlist, nprobe=..., niter=...) # (M, D) CUDA + vals, ids = flash_ivf_flat_search(index, queries_cuda, k, nprobe=...) + +This is the GPU counterpart of the FAISS ``ivf`` backend (FAISS ``IndexIVFFlat``): +both coarse-quantize the corpus into ``nlist`` cells and, at search time, scan only +the ``nprobe`` nearest cells. At a fixed ``(nlist, nprobe)`` FlashLib probes the same +candidate set as a reference IVF-Flat, so recall is comparable; the difference is GPU +vs CPU kernels. + +The built index is a small set of torch tensors (centroids, cell-contiguous data, +row ids, CSR offsets), so we persist it with ``torch.save`` (``.flashlib_ivf.pt``) +plus an id map (``.flashlib_ivf_id_map.json``) and reload it onto the GPU at +searcher start-up (no k-means re-train). + +FlashLib ranks by squared L2. For ``mips`` / ``cosine`` we L2-normalize both the +database and query vectors, on which squared-L2 ranking is equivalent to +inner-product / cosine ranking. + +Requires a CUDA GPU at both build (k-means training) and search time. +""" + +import json +import logging +from pathlib import Path +from typing import Any, Optional + +import numpy as np +from leann.interface import ( + LeannBackendBuilderInterface, + LeannBackendFactoryInterface, + LeannBackendSearcherInterface, +) +from leann.registry import register_backend +from leann.searcher_base import BaseSearcher + +logger = logging.getLogger(__name__) + +INDEX_SUFFIX = "flashlib_ivf.pt" +ID_MAP_SUFFIX = "flashlib_ivf_id_map.json" + +# IvfFlatIndex dataclass fields, split by how they serialize. +_TENSOR_FIELDS = ("centroids", "data", "ids", "list_offsets") +_SCALAR_FIELDS = ("metric", "D", "Dp", "nlist", "nprobe", "max_list_len") + + +def _import_flashlib(): + try: + import torch # noqa: F401 + from flashlib import ( + IvfFlatIndex, + flash_ivf_flat_build, + flash_ivf_flat_search, + ) + except ImportError as e: + raise ImportError( + "The FlashLib IVF backend requires 'flashlib' and 'torch' with CUDA. " + "Install with: pip install flashlib (a CUDA GPU is required at build and search time)." + ) from e + return IvfFlatIndex, flash_ivf_flat_build, flash_ivf_flat_search + + +def _normalize_l2(data: np.ndarray) -> np.ndarray: + norms = np.linalg.norm(data, axis=1, keepdims=True) + norms[norms == 0] = 1 + return data / norms + + +def _needs_normalize(distance_metric: str) -> bool: + return distance_metric.lower() in ("mips", "cosine") + + +def _index_path(index_dir: Path, index_prefix: str) -> Path: + return index_dir / f"{index_prefix}.{INDEX_SUFFIX}" + + +def _id_map_path(index_dir: Path, index_prefix: str) -> Path: + return index_dir / f"{index_prefix}.{ID_MAP_SUFFIX}" + + +def _save_id_map(index_dir: Path, index_prefix: str, ids: list[str]) -> None: + with open(_id_map_path(index_dir, index_prefix), "w", encoding="utf-8") as f: + json.dump({"ids": ids}, f) + + +def _load_id_map(index_dir: Path, index_prefix: str) -> list[str]: + path = _id_map_path(index_dir, index_prefix) + if not path.exists(): + raise FileNotFoundError(f"FlashLib IVF id map not found at {path}") + with open(path, encoding="utf-8") as f: + return json.load(f)["ids"] + + +def _save_index(index, path: Path) -> None: + import torch + + state: dict[str, Any] = {f: getattr(index, f).detach().cpu() for f in _TENSOR_FIELDS} + for f in _SCALAR_FIELDS: + state[f] = getattr(index, f) + torch.save(state, str(path)) + + +def _load_index(path: Path, device: str = "cuda"): + import torch + + IvfFlatIndex, _, _ = _import_flashlib() + state = torch.load(str(path), map_location=device, weights_only=False) + kwargs: dict[str, Any] = {f: state[f].to(device) for f in _TENSOR_FIELDS} + for f in _SCALAR_FIELDS: + kwargs[f] = state[f] + return IvfFlatIndex(**kwargs) + + +@register_backend("flashlib_ivf") +class FlashlibIVFBackend(LeannBackendFactoryInterface): + @staticmethod + def builder(**kwargs) -> LeannBackendBuilderInterface: + return FlashlibIVFBuilder(**kwargs) + + @staticmethod + def searcher(index_path: str, **kwargs) -> LeannBackendSearcherInterface: + return FlashlibIVFSearcher(index_path, **kwargs) + + +class FlashlibIVFBuilder(LeannBackendBuilderInterface): + def __init__(self, **kwargs): + self.build_params = kwargs.copy() + self.distance_metric = self.build_params.setdefault("distance_metric", "mips") + self.nlist = self.build_params.setdefault("nlist", 1024) + self.nprobe = self.build_params.setdefault("nprobe", 16) + self.niter = self.build_params.setdefault("niter", 20) + self.seed = self.build_params.setdefault("seed", 0) + self.dimensions = self.build_params.get("dimensions") + + def build(self, data: np.ndarray, ids: list[str], index_path: str, **kwargs) -> None: + import torch + + _, flash_ivf_flat_build, _ = _import_flashlib() + if not torch.cuda.is_available(): + raise RuntimeError( + "FlashLib IVF backend requires a CUDA GPU at build time (k-means training), " + "but none is available." + ) + + path = Path(index_path) + index_dir = path.parent + index_prefix = path.stem + index_dir.mkdir(parents=True, exist_ok=True) + + if data.dtype != np.float32: + data = data.astype(np.float32) + data = np.ascontiguousarray(data) + if _needs_normalize(self.distance_metric): + data = _normalize_l2(data) + + n = data.shape[0] + nlist = int(min(self.nlist, n)) if n > 0 else int(self.nlist) + + db = torch.from_numpy(data).cuda() + index = flash_ivf_flat_build( + db, + nlist, + metric="l2", + nprobe=int(self.nprobe), + niter=int(self.niter), + seed=int(self.seed), + ) + _save_index(index, _index_path(index_dir, index_prefix)) + _save_id_map(index_dir, index_prefix, list(ids)) + logger.info( + "FlashLib IVF build: %d vectors (dim=%d, metric=%s, nlist=%d, nprobe=%d) at %s", + n, + data.shape[1], + self.distance_metric, + nlist, + self.nprobe, + _index_path(index_dir, index_prefix), + ) + + +class FlashlibIVFSearcher(BaseSearcher): + def __init__(self, index_path: str, **kwargs): + # Reuse the HNSW embedding server (if present) to embed queries, exactly like + # the other non-recompute backends; falls back to direct model loading. + super().__init__( + index_path, + backend_module_name="leann_backend_hnsw.hnsw_embedding_server", + **kwargs, + ) + backend_kwargs = self.meta.get("backend_kwargs", {}) + self.distance_metric = backend_kwargs.get("distance_metric", "mips").lower() + + index_prefix = self.index_path.stem + index_file = _index_path(self.index_dir, index_prefix) + if not index_file.exists(): + raise FileNotFoundError(f"FlashLib IVF index file not found at {index_file}") + + import torch + + if not torch.cuda.is_available(): + raise RuntimeError( + "FlashLib IVF backend requires a CUDA GPU at search time, but none is available." + ) + + self._ids = _load_id_map(self.index_dir, index_prefix) + self._index = _load_index(index_file, device="cuda") + self._nlist = int(self._index.nlist) + self._ntotal = int(self._index.data.shape[0]) + logger.info( + "FlashLib IVF searcher ready: %d vectors, nlist=%d, metric=%s", + self._ntotal, + self._nlist, + self.distance_metric, + ) + + def search( + self, + query: np.ndarray, + top_k: int, + complexity: int = 64, + nprobe: Optional[int] = None, + **kwargs, + ) -> dict[str, Any]: + import torch + + _, _, flash_ivf_flat_search = _import_flashlib() + if query.dtype != np.float32: + query = query.astype(np.float32) + if _needs_normalize(self.distance_metric): + query = _normalize_l2(query) + + # complexity is the recall knob shared with the FAISS ivf backend. + nprobe = nprobe or min(complexity, self._nlist) + k = min(top_k, self._ntotal) + q = torch.from_numpy(np.ascontiguousarray(query)).cuda() + distances, indices = flash_ivf_flat_search(self._index, q, k, nprobe=int(nprobe)) + distances_np = distances.detach().cpu().numpy().astype(np.float32) + indices_np = indices.detach().cpu().numpy() + + def map_label(i: int) -> str: + # flash_ivf_flat_search pads short candidate lists with -1. + return self._ids[i] if i >= 0 else "-1" + + string_labels = [[map_label(int(i)) for i in row] for row in indices_np] + return {"labels": string_labels, "distances": distances_np} + + def compute_query_embedding( + self, + query: str, + use_server_if_available: bool = True, + zmq_port: Optional[int] = None, + query_template: Optional[str] = None, + ) -> np.ndarray: + return super().compute_query_embedding( + query, + use_server_if_available=use_server_if_available, + zmq_port=zmq_port, + query_template=query_template, + ) diff --git a/packages/leann-backend-flashlib-ivf/pyproject.toml b/packages/leann-backend-flashlib-ivf/pyproject.toml new file mode 100644 index 0000000..0437b70 --- /dev/null +++ b/packages/leann-backend-flashlib-ivf/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "leann-backend-flashlib-ivf" +version = "0.3.6" +description = "GPU-accelerated FlashLib IVF-Flat (Triton/CuteDSL) approximate-NN backend for LEANN." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "leann-core>=0.3.6", + "numpy>=1.20.0", + "flashlib", + "torch", +] + +[project.optional-dependencies] +# Optional: use the HNSW embedding server for query embedding (same as other backends). +query-server = ["leann-backend-hnsw>=0.3.6", "pyzmq>=23.0.0", "msgpack>=1.0.0"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["leann_backend_flashlib_ivf*"] diff --git a/packages/leann-backend-flashlib/README.md b/packages/leann-backend-flashlib/README.md new file mode 100644 index 0000000..67af47b --- /dev/null +++ b/packages/leann-backend-flashlib/README.md @@ -0,0 +1,61 @@ +# leann-backend-flashlib + +GPU-accelerated [FlashLib](https://github.com/FlashML-org/flashlib) `IVFFlat` +backend for LEANN. + +FlashLib is a GPU library of classical ML operators built on Triton / CuteDSL. +Its `IVFFlat` index runs approximate nearest-neighbor search entirely on CUDA +tensors and, at a fixed `(nlist, nprobe)`, probes the same candidate set as a +reference IVF-Flat (FAISS / cuVS). + +## Requirements + +- A CUDA GPU (required at **search** time; index building only needs numpy). +- `pip install flashlib` and `torch`. + +## Install + +```bash +# from a LEANN checkout +uv sync --extra flashlib +# or +pip install leann-backend-flashlib +``` + +## Usage + +```python +from leann import LeannBuilder, LeannSearcher + +builder = LeannBuilder(backend_name="flashlib") # nlist=1024, distance_metric="mips" +builder.add_text("LEANN recomputes embeddings to save storage.") +builder.build_index("demo.leann") + +searcher = LeannSearcher("demo.leann") +print(searcher.search("How does LEANN save storage?", top_k=3)) +``` + +Or from the CLI / example apps: + +```bash +python -m apps.document_rag --query "What are the main techniques LEANN explores?" \ + --backend-name flashlib +``` + +## How it works + +FlashLib's `IVFFlat` has no on-disk format, so this backend persists the raw +float32 vectors (`.flashlib.npy`) plus an id map (`.flashlib_id_map.json`) +and rebuilds the GPU index at searcher start-up via `IVFFlat(...).fit(db)`. + +FlashLib's only distance metric is squared L2. For `mips` / `cosine` the vectors +are L2-normalized at build and query time, on which squared-L2 ranking is +equivalent to inner-product / cosine ranking. + +## Parameters + +| kwarg | default | meaning | +|-------|---------|---------| +| `nlist` | `1024` | number of IVF partitions (clamped to corpus size) | +| `distance_metric` | `"mips"` | `mips`, `cosine`, or `l2` | +| `nprobe` (search) | derived from `complexity` | partitions probed per query (recall knob) | diff --git a/packages/leann-backend-flashlib/leann_backend_flashlib/__init__.py b/packages/leann-backend-flashlib/leann_backend_flashlib/__init__.py new file mode 100644 index 0000000..7527b74 --- /dev/null +++ b/packages/leann-backend-flashlib/leann_backend_flashlib/__init__.py @@ -0,0 +1,13 @@ +"""LEANN FlashLib backend: GPU-accelerated IVFFlat (Triton/CuteDSL) ANN search.""" + +from .flashlib_backend import ( + FlashlibBackend, + FlashlibBuilder, + FlashlibSearcher, +) + +__all__ = [ + "FlashlibBackend", + "FlashlibBuilder", + "FlashlibSearcher", +] diff --git a/packages/leann-backend-flashlib/leann_backend_flashlib/flashlib_backend.py b/packages/leann-backend-flashlib/leann_backend_flashlib/flashlib_backend.py new file mode 100644 index 0000000..0353dc2 --- /dev/null +++ b/packages/leann-backend-flashlib/leann_backend_flashlib/flashlib_backend.py @@ -0,0 +1,201 @@ +""" +FlashLib backend: GPU-accelerated nearest-neighbor search built on Triton / CuteDSL. + +FlashLib (https://github.com/FlashML-org/flashlib) is a GPU library of classical +ML primitives. This backend uses its ``NearestNeighbors`` application (a fused +flash-knn search) which runs entirely on CUDA tensors: + + from flashlib import NearestNeighbors + index = NearestNeighbors().fit(db_cuda) # db_cuda: (N, D) CUDA tensor + distances, indices = index.kneighbors(queries, n_neighbors=10) + +FlashLib's search has no on-disk format, so this backend persists the raw float32 +vectors (``.flashlib.npy``) plus an id map, and reconstructs the GPU index at +searcher start-up via ``NearestNeighbors().fit(db)``. + +FlashLib ranks by squared L2. For ``mips`` / ``cosine`` we L2-normalize both the +database and query vectors, on which squared-L2 ranking is equivalent to +inner-product / cosine ranking. + +Requires a CUDA GPU at search time. Index building only needs numpy (no GPU). +""" + +import json +import logging +from pathlib import Path +from typing import Any, Optional + +import numpy as np +from leann.interface import ( + LeannBackendBuilderInterface, + LeannBackendFactoryInterface, + LeannBackendSearcherInterface, +) +from leann.registry import register_backend +from leann.searcher_base import BaseSearcher + +logger = logging.getLogger(__name__) + +VECTORS_SUFFIX = "flashlib.npy" +ID_MAP_SUFFIX = "flashlib_id_map.json" + + +def _import_flashlib(): + try: + import torch # noqa: F401 + from flashlib import NearestNeighbors + except ImportError as e: + raise ImportError( + "The FlashLib backend requires 'flashlib' and 'torch' with CUDA. " + "Install with: pip install flashlib (a CUDA GPU is required at search time)." + ) from e + return NearestNeighbors + + +def _normalize_l2(data: np.ndarray) -> np.ndarray: + norms = np.linalg.norm(data, axis=1, keepdims=True) + norms[norms == 0] = 1 + return data / norms + + +def _needs_normalize(distance_metric: str) -> bool: + return distance_metric.lower() in ("mips", "cosine") + + +def _vectors_path(index_dir: Path, index_prefix: str) -> Path: + return index_dir / f"{index_prefix}.{VECTORS_SUFFIX}" + + +def _id_map_path(index_dir: Path, index_prefix: str) -> Path: + return index_dir / f"{index_prefix}.{ID_MAP_SUFFIX}" + + +def _save_id_map(index_dir: Path, index_prefix: str, ids: list[str]) -> None: + with open(_id_map_path(index_dir, index_prefix), "w", encoding="utf-8") as f: + json.dump({"ids": ids}, f) + + +def _load_id_map(index_dir: Path, index_prefix: str) -> list[str]: + path = _id_map_path(index_dir, index_prefix) + if not path.exists(): + raise FileNotFoundError(f"FlashLib id map not found at {path}") + with open(path, encoding="utf-8") as f: + return json.load(f)["ids"] + + +@register_backend("flashlib") +class FlashlibBackend(LeannBackendFactoryInterface): + @staticmethod + def builder(**kwargs) -> LeannBackendBuilderInterface: + return FlashlibBuilder(**kwargs) + + @staticmethod + def searcher(index_path: str, **kwargs) -> LeannBackendSearcherInterface: + return FlashlibSearcher(index_path, **kwargs) + + +class FlashlibBuilder(LeannBackendBuilderInterface): + def __init__(self, **kwargs): + self.build_params = kwargs.copy() + self.distance_metric = self.build_params.setdefault("distance_metric", "mips") + self.dimensions = self.build_params.get("dimensions") + + def build(self, data: np.ndarray, ids: list[str], index_path: str, **kwargs) -> None: + path = Path(index_path) + index_dir = path.parent + index_prefix = path.stem + index_dir.mkdir(parents=True, exist_ok=True) + + if data.dtype != np.float32: + data = data.astype(np.float32) + data = np.ascontiguousarray(data) + + if _needs_normalize(self.distance_metric): + data = _normalize_l2(data) + + # FlashLib search has no disk format, so we persist the raw vectors and + # rebuild the GPU index when the searcher starts. + np.save(_vectors_path(index_dir, index_prefix), data) + _save_id_map(index_dir, index_prefix, list(ids)) + logger.info( + "FlashLib build: stored %d vectors (dim=%d, metric=%s) at %s", + data.shape[0], + data.shape[1], + self.distance_metric, + _vectors_path(index_dir, index_prefix), + ) + + +class FlashlibSearcher(BaseSearcher): + def __init__(self, index_path: str, **kwargs): + # Reuse the HNSW embedding server (if present) to embed queries, exactly like + # the other non-recompute backends; falls back to direct model loading. + super().__init__( + index_path, + backend_module_name="leann_backend_hnsw.hnsw_embedding_server", + **kwargs, + ) + backend_kwargs = self.meta.get("backend_kwargs", {}) + self.distance_metric = backend_kwargs.get("distance_metric", "mips").lower() + + index_prefix = self.index_path.stem + vectors_file = _vectors_path(self.index_dir, index_prefix) + if not vectors_file.exists(): + raise FileNotFoundError(f"FlashLib vectors file not found at {vectors_file}") + + self._ids = _load_id_map(self.index_dir, index_prefix) + + NearestNeighbors = _import_flashlib() + import torch + + if not torch.cuda.is_available(): + raise RuntimeError( + "FlashLib backend requires a CUDA GPU at search time, but none is available." + ) + + vectors = np.load(vectors_file) + self._ntotal = vectors.shape[0] + db = torch.from_numpy(np.ascontiguousarray(vectors)).cuda() + self._index = NearestNeighbors().fit(db) + logger.info( + "FlashLib searcher ready: %d vectors, metric=%s", + self._ntotal, + self.distance_metric, + ) + + def search( + self, + query: np.ndarray, + top_k: int, + complexity: int = 64, + **kwargs, + ) -> dict[str, Any]: + import torch + + if query.dtype != np.float32: + query = query.astype(np.float32) + if _needs_normalize(self.distance_metric): + query = _normalize_l2(query) + + k = min(top_k, self._ntotal) + q = torch.from_numpy(np.ascontiguousarray(query)).cuda() + distances, indices = self._index.kneighbors(q, n_neighbors=k) + distances_np = distances.detach().cpu().numpy().astype(np.float32) + indices_np = indices.detach().cpu().numpy() + + string_labels = [[self._ids[int(i)] for i in row] for row in indices_np] + return {"labels": string_labels, "distances": distances_np} + + def compute_query_embedding( + self, + query: str, + use_server_if_available: bool = True, + zmq_port: Optional[int] = None, + query_template: Optional[str] = None, + ) -> np.ndarray: + return super().compute_query_embedding( + query, + use_server_if_available=use_server_if_available, + zmq_port=zmq_port, + query_template=query_template, + ) diff --git a/packages/leann-backend-flashlib/pyproject.toml b/packages/leann-backend-flashlib/pyproject.toml new file mode 100644 index 0000000..ccd52bc --- /dev/null +++ b/packages/leann-backend-flashlib/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "leann-backend-flashlib" +version = "0.3.6" +description = "GPU-accelerated FlashLib (IVFFlat on Triton/CuteDSL) backend for LEANN." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "leann-core>=0.3.6", + "numpy>=1.20.0", + "flashlib", + "torch", +] + +[project.optional-dependencies] +# Optional: use the HNSW embedding server for query embedding (same as other backends). +query-server = ["leann-backend-hnsw>=0.3.6", "pyzmq>=23.0.0", "msgpack>=1.0.0"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["leann_backend_flashlib*"] diff --git a/packages/leann-backend-hnsw/CMakeLists.txt b/packages/leann-backend-hnsw/CMakeLists.txt new file mode 100644 index 0000000..d4b2c46 --- /dev/null +++ b/packages/leann-backend-hnsw/CMakeLists.txt @@ -0,0 +1,114 @@ +cmake_minimum_required(VERSION 3.24) +project(leann_backend_hnsw_wrapper) +set(CMAKE_C_COMPILER_WORKS 1) +set(CMAKE_CXX_COMPILER_WORKS 1) + +# Set OpenMP path for macOS +if(APPLE) + # Detect Homebrew installation path (Apple Silicon vs Intel) + if(EXISTS "/opt/homebrew/opt/libomp") + set(HOMEBREW_PREFIX "/opt/homebrew") + elseif(EXISTS "/usr/local/opt/libomp") + set(HOMEBREW_PREFIX "/usr/local") + else() + message(FATAL_ERROR "Could not find libomp installation. Please install with: brew install libomp") + endif() + + set(OpenMP_C_FLAGS "-Xpreprocessor -fopenmp -I${HOMEBREW_PREFIX}/opt/libomp/include") + set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp -I${HOMEBREW_PREFIX}/opt/libomp/include") + set(OpenMP_C_LIB_NAMES "omp") + set(OpenMP_CXX_LIB_NAMES "omp") + set(OpenMP_omp_LIBRARY "${HOMEBREW_PREFIX}/opt/libomp/lib/libomp.dylib") + + # Force use of system libc++ to avoid version mismatch + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -stdlib=libc++") + + # Set minimum macOS version for better compatibility + set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version") +endif() + +# Find ZMQ via pkg-config on all platforms. +# Windows CI installs pkgconfiglite + zeromq via vcpkg and exports PKG_CONFIG_PATH. +find_package(PkgConfig REQUIRED) + +# On ARM64 macOS, ensure pkg-config finds ARM64 Homebrew packages first +if(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(ENV{PKG_CONFIG_PATH} "/opt/homebrew/lib/pkgconfig:/opt/homebrew/share/pkgconfig:$ENV{PKG_CONFIG_PATH}") +endif() + +pkg_check_modules(ZMQ REQUIRED IMPORTED_TARGET libzmq) + +# This creates PkgConfig::ZMQ target automatically with correct properties +if(TARGET PkgConfig::ZMQ) + message(STATUS "Found and configured ZMQ target: PkgConfig::ZMQ") +else() + message(FATAL_ERROR "pkg_check_modules did not create IMPORTED target for ZMQ.") +endif() + +# Add cppzmq headers +include_directories(SYSTEM third_party/cppzmq) + +# Configure msgpack-c - disable boost dependency +set(MSGPACK_USE_BOOST OFF CACHE BOOL "" FORCE) +add_compile_definitions(MSGPACK_NO_BOOST) +include_directories(third_party/msgpack-c/include) + +# Faiss configuration - streamlined build +set(FAISS_ENABLE_PYTHON ON CACHE BOOL "" FORCE) +set(FAISS_ENABLE_GPU OFF CACHE BOOL "" FORCE) +set(FAISS_ENABLE_EXTRAS OFF CACHE BOOL "" FORCE) +set(BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(FAISS_ENABLE_C_API OFF CACHE BOOL "" FORCE) +set(FAISS_OPT_LEVEL "generic" CACHE STRING "" FORCE) + +# Disable x86-specific SIMD optimizations (important for ARM64 compatibility) +set(FAISS_ENABLE_AVX2 OFF CACHE BOOL "" FORCE) +set(FAISS_ENABLE_AVX512 OFF CACHE BOOL "" FORCE) +set(FAISS_ENABLE_SSE4_1 OFF CACHE BOOL "" FORCE) + +# ARM64-specific configuration +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + message(STATUS "Configuring Faiss for ARM64 architecture") + + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + # Default generic: many ARM64 Linux hosts (GitHub ubuntu-*-arm, Ampere Altra, + # Graviton2) do not implement SVE. Faiss built with FAISS_OPT_LEVEL=sve uses + # -march=armv8-a+sve and can SIGILL on those CPUs. Opt in with + # -DLEANN_FAISS_ARM64_SVE=ON for Neoverse V1 / Graviton3+ and similar. + option(LEANN_FAISS_ARM64_SVE "Faiss SVE on ARM64 Linux (requires SVE at runtime)" OFF) + if(LEANN_FAISS_ARM64_SVE) + set(FAISS_OPT_LEVEL "sve" CACHE STRING "" FORCE) + message(STATUS "Setting FAISS_OPT_LEVEL to 'sve' for ARM64 Linux (LEANN_FAISS_ARM64_SVE=ON)") + else() + set(FAISS_OPT_LEVEL "generic" CACHE STRING "" FORCE) + message(STATUS "Setting FAISS_OPT_LEVEL to 'generic' for ARM64 Linux") + endif() + else() + # Use generic optimization for other ARM64 platforms (like macOS) + set(FAISS_OPT_LEVEL "generic" CACHE STRING "" FORCE) + message(STATUS "Setting FAISS_OPT_LEVEL to 'generic' for ARM64 ${CMAKE_SYSTEM_NAME}") + endif() + + # ARM64 compatibility: Faiss submodule has been modified to fix x86 header inclusion + message(STATUS "Using ARM64-compatible Faiss submodule") +endif() + +# Additional optimization options from INSTALL.md +set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) # Static library is faster to build + +# Avoid building demos and benchmarks +set(BUILD_DEMOS OFF CACHE BOOL "" FORCE) +set(BUILD_BENCHS OFF CACHE BOOL "" FORCE) + +# NEW: Tell Faiss to only build the generic version +set(FAISS_BUILD_GENERIC ON CACHE BOOL "" FORCE) +set(FAISS_BUILD_AVX2 OFF CACHE BOOL "" FORCE) +set(FAISS_BUILD_AVX512 OFF CACHE BOOL "" FORCE) + +# IMPORTANT: Disable building AVX versions to speed up compilation +set(FAISS_BUILD_AVX_VERSIONS OFF CACHE BOOL "" FORCE) + +add_subdirectory(third_party/faiss) diff --git a/packages/leann-backend-hnsw/leann_backend_hnsw/__init__.py b/packages/leann-backend-hnsw/leann_backend_hnsw/__init__.py new file mode 100644 index 0000000..3136bdd --- /dev/null +++ b/packages/leann-backend-hnsw/leann_backend_hnsw/__init__.py @@ -0,0 +1,58 @@ +import os +from pathlib import Path + +_DLL_DIR_HANDLES: list[object] = [] + + +def _configure_windows_dll_search_path() -> None: + """Register vcpkg DLL directories so Windows can resolve native dependencies. + + Python 3.8+ no longer searches PATH for DLL dependencies of native + extensions (see https://github.com/numpy/numpy/wiki/windows-dll-notes). + The standard workaround is ``os.add_dll_directory``. + + This only matters for **CI builds and source installs** where C++ deps + (OpenBLAS, ZeroMQ, protobuf, …) live in a vcpkg tree. Pre-built wheels + shipped via PyPI bundle all required DLLs inside the wheel (via + delvewheel), so end-users installing with ``pip install`` are unaffected. + """ + if os.name != "nt" or not hasattr(os, "add_dll_directory"): + return + + candidate_dirs: list[Path] = [] + env_roots = [os.getenv("VCPKG_INSTALLATION_ROOT"), os.getenv("VCPKG_ROOT")] + for root in env_roots: + if not root: + continue + root_path = Path(root) + candidate_dirs.extend( + [ + root_path / "installed" / "x64-windows" / "bin", + root_path / "installed" / "x64-windows" / "debug" / "bin", + root_path / "installed" / "x64-windows" / "tools" / "protobuf", + ] + ) + + for path_entry in os.environ.get("PATH", "").split(";"): + entry = path_entry.strip() + if not entry: + continue + if "vcpkg" in entry.lower(): + candidate_dirs.append(Path(entry)) + + seen: set[str] = set() + for dll_dir in candidate_dirs: + resolved = str(dll_dir).lower() + if resolved in seen or not dll_dir.exists(): + continue + seen.add(resolved) + try: + _DLL_DIR_HANDLES.append(os.add_dll_directory(str(dll_dir))) + os.environ["PATH"] = f"{dll_dir};{os.environ.get('PATH', '')}" + except OSError: + continue + + +_configure_windows_dll_search_path() + +from . import hnsw_backend as hnsw_backend # noqa: E402 diff --git a/packages/leann-backend-hnsw/leann_backend_hnsw/convert_to_csr.py b/packages/leann-backend-hnsw/leann_backend_hnsw/convert_to_csr.py new file mode 100644 index 0000000..71e5533 --- /dev/null +++ b/packages/leann-backend-hnsw/leann_backend_hnsw/convert_to_csr.py @@ -0,0 +1,1046 @@ +import argparse +import gc # Import garbage collector interface +import logging +import os +import struct +import sys +import time +from dataclasses import dataclass +from typing import Any, Optional + +import numpy as np + +# Set up logging to avoid print buffer issues +logger = logging.getLogger(__name__) +LOG_LEVEL = os.getenv("LEANN_LOG_LEVEL", "WARNING").upper() +log_level = getattr(logging, LOG_LEVEL, logging.WARNING) +logger.setLevel(log_level) + +# --- FourCCs (add more if needed) --- +INDEX_HNSW_FLAT_FOURCC = int.from_bytes(b"IHNf", "little") +# Add other HNSW fourccs if you expect different storage types inside HNSW +# INDEX_HNSW_PQ_FOURCC = int.from_bytes(b'IHNp', 'little') +# INDEX_HNSW_SQ_FOURCC = int.from_bytes(b'IHNs', 'little') +# INDEX_HNSW_CAGRA_FOURCC = int.from_bytes(b'IHNc', 'little') # Example + +EXPECTED_HNSW_FOURCCS = {INDEX_HNSW_FLAT_FOURCC} # Modify if needed +NULL_INDEX_FOURCC = int.from_bytes(b"null", "little") + +# --- Helper functions for reading/writing binary data --- + + +def read_struct(f, fmt): + """Reads data according to the struct format.""" + size = struct.calcsize(fmt) + data = f.read(size) + if len(data) != size: + raise EOFError( + f"File ended unexpectedly reading struct fmt '{fmt}'. Expected {size} bytes, got {len(data)}." + ) + return struct.unpack(fmt, data)[0] + + +def read_vector_raw(f, element_fmt_char): + """Reads a vector (size followed by data), returns count and raw bytes.""" + count = -1 # Initialize count + total_bytes = -1 # Initialize total_bytes + try: + count = read_struct(f, " max_reasonable_count or count < 0: + raise MemoryError( + f"Vector count {count} seems unreasonably large, possibly due to file corruption or incorrect format read." + ) + + total_bytes = count * element_size + # --- FIX for MemoryError: Check for huge byte size before allocation --- + max_reasonable_bytes = 50 * (1024**3) # ~50 GB limit + if total_bytes > max_reasonable_bytes or total_bytes < 0: # Check for overflow + raise MemoryError( + f"Attempting to read {total_bytes} bytes ({count} elements * {element_size} bytes/element), which exceeds the safety limit. File might be corrupted or format mismatch." + ) + + data_bytes = f.read(total_bytes) + + if len(data_bytes) != total_bytes: + raise EOFError( + f"File ended unexpectedly reading vector data. Expected {total_bytes} bytes, got {len(data_bytes)}." + ) + return count, data_bytes + except (MemoryError, OverflowError) as e: + # Add context to the error message + print( + f"\nError during raw vector read (element_fmt='{element_fmt_char}', count={count}, total_bytes={total_bytes}): {e}", + file=sys.stderr, + ) + raise e # Re-raise the original error type + + +def read_numpy_vector(f, np_dtype, struct_fmt_char): + """Reads a vector into a NumPy array.""" + count = -1 # Initialize count for robust error handling + print( + f" Reading vector (dtype={np_dtype}, fmt='{struct_fmt_char}')... ", + end="", + flush=True, + ) + try: + count, data_bytes = read_vector_raw(f, struct_fmt_char) + print(f"Count={count}, Bytes={len(data_bytes)}") + if count > 0 and len(data_bytes) > 0: + arr = np.frombuffer(data_bytes, dtype=np_dtype) + if arr.size != count: + raise ValueError( + f"Inconsistent array size after reading. Expected {count}, got {arr.size}" + ) + return arr + elif count == 0: + return np.array([], dtype=np_dtype) + else: + raise ValueError("Read zero bytes but count > 0.") + except MemoryError as e: + # Now count should be defined (or -1 if error was in read_struct) + print( + f"\nMemoryError creating NumPy array (dtype={np_dtype}, count={count}). {e}", + file=sys.stderr, + ) + raise e + except Exception as e: # Catch other potential errors like ValueError + print( + f"\nError reading numpy vector (dtype={np_dtype}, fmt='{struct_fmt_char}', count={count}): {e}", + file=sys.stderr, + ) + raise e + + +def write_numpy_vector(f, arr, struct_fmt_char): + """Writes a NumPy array as a vector (size followed by data).""" + count = arr.size + f.write(struct.pack(" 0 else 0 + + +def write_compact_format( + f_out, + original_hnsw_data, + assign_probas_np, + cum_nneighbor_per_level_np, + levels_np, + compact_level_ptr, + compact_node_offsets_np, + compact_neighbors_data, + storage_fourcc, + storage_data, +): + """Write HNSW data in compact format following C++ read order exactly.""" + # Write IndexHNSW Header + f_out.write(struct.pack(" 1: + f_out.write(struct.pack(" HNSWComponents: + original_hnsw_data: dict[str, Any] = {} + + hnsw_index_fourcc = read_struct(f, " 1: + original_hnsw_data["metric_arg"] = read_struct(f, " HNSWComponents: + with open(path, "rb") as f: + return _read_hnsw_structure(f) + + +def write_original_format( + f_out, + original_hnsw_data, + assign_probas_np, + cum_nneighbor_per_level_np, + levels_np, + offsets_np, + neighbors_np, + storage_fourcc, + storage_data, +): + """Write non-compact HNSW data in original FAISS order.""" + + f_out.write(struct.pack(" 1: + f_out.write(struct.pack(" bool: + """Rewrite an HNSW index while dropping the embedded storage section.""" + + start_time = time.time() + try: + with open(input_filename, "rb") as f_in, open(output_filename, "wb") as f_out: + original_hnsw_data: dict[str, Any] = {} + + hnsw_index_fourcc = read_struct(f_in, " 1: + original_hnsw_data["metric_arg"] = read_struct(f_in, " {output_filename}") + start_time = time.time() + original_hnsw_data = {} + neighbors_np = None # Initialize to allow check in finally block + try: + with open(input_filename, "rb") as f_in, open(output_filename, "wb") as f_out: + # --- Read IndexHNSW FourCC and Header --- + print(f"[{time.time() - start_time:.2f}s] Reading Index HNSW header...") + # ... (Keep the header reading logic as before) ... + hnsw_index_fourcc = read_struct(f_in, " 1: + original_hnsw_data["metric_arg"] = read_struct(f_in, " 0 else 0 + if neighbors_np.size != expected_neighbors_size: + print( + f"Warning: neighbors vector size mismatch. Expected {expected_neighbors_size} based on offsets, got {neighbors_np.size}." + ) + gc.collect() + + original_hnsw_data["entry_point"] = read_struct(f_in, " 0 and i % (ntotal // 100 or 1) == 0: # Log progress roughly every 1% + progress = (i / ntotal) * 100 + elapsed = time.time() - start_time + print( + f"\r[{elapsed:.2f}s] Converting node {i}/{ntotal} ({progress:.1f}%)...", + end="", + ) + + node_max_level = levels_np[i] - 1 + if node_max_level < -1: + node_max_level = -1 + + node_ptr_start_index = current_level_ptr_idx + compact_node_offsets_np[i] = node_ptr_start_index + + original_offset_start = offsets_np[i] + num_pointers_expected = (node_max_level + 1) + 1 + + for level in range(node_max_level + 1): + compact_level_ptr.append(current_data_idx) + + begin_orig_np = original_offset_start + get_cum_neighbors( + cum_nneighbor_per_level_np, level + ) + end_orig_np = original_offset_start + get_cum_neighbors( + cum_nneighbor_per_level_np, level + 1 + ) + + begin_orig = int(begin_orig_np) + end_orig = int(end_orig_np) + + neighbors_len = len(neighbors_np) # Cache length + begin_orig = min(max(0, begin_orig), neighbors_len) + end_orig = min(max(begin_orig, end_orig), neighbors_len) + + if begin_orig < end_orig: + # Slicing creates a copy, could be memory intensive for large M + # Consider iterating if memory becomes an issue here + level_neighbors_slice = neighbors_np[begin_orig:end_orig] + valid_neighbors_mask = level_neighbors_slice >= 0 + num_valid = np.count_nonzero(valid_neighbors_mask) + + if num_valid > 0: + # Append valid neighbors + compact_neighbors_data.extend( + level_neighbors_slice[valid_neighbors_mask] + ) + current_data_idx += num_valid + total_valid_neighbors_counted += num_valid + + compact_level_ptr.append(current_data_idx) + current_level_ptr_idx += num_pointers_expected + + compact_node_offsets_np[ntotal] = current_level_ptr_idx + print( + f"\r[{time.time() - start_time:.2f}s] Conversion loop finished. " + ) # Clear progress line + + # --- Validation Checks --- + print(f"[{time.time() - start_time:.2f}s] Running validation checks...") + valid_check_passed = True + # Check 1: Total valid neighbors count + print(" Checking total valid neighbor count...") + expected_valid_count = np.sum(neighbors_np >= 0) + if total_valid_neighbors_counted != len(compact_neighbors_data): + print( + f"Error: Mismatch between counted valid neighbors ({total_valid_neighbors_counted}) and final compact_data size ({len(compact_neighbors_data)})!", + file=sys.stderr, + ) + valid_check_passed = False + if expected_valid_count != len(compact_neighbors_data): + print( + f"Error: Mismatch between NumPy count of valid neighbors ({expected_valid_count}) and final compact_data size ({len(compact_neighbors_data)})!", + file=sys.stderr, + ) + valid_check_passed = False + else: + print(f" OK: Total valid neighbors = {len(compact_neighbors_data)}") + + # Check 2: Final pointer indices consistency + print(" Checking final pointer indices...") + if compact_node_offsets_np[ntotal] != len(compact_level_ptr): + print( + f"Error: Final node offset ({compact_node_offsets_np[ntotal]}) doesn't match level_ptr size ({len(compact_level_ptr)})!", + file=sys.stderr, + ) + valid_check_passed = False + if ( + len(compact_level_ptr) > 0 and compact_level_ptr[-1] != len(compact_neighbors_data) + ) or (len(compact_level_ptr) == 0 and len(compact_neighbors_data) != 0): + last_ptr = compact_level_ptr[-1] if len(compact_level_ptr) > 0 else -1 + print( + f"Error: Last level pointer ({last_ptr}) doesn't match compact_data size ({len(compact_neighbors_data)})!", + file=sys.stderr, + ) + valid_check_passed = False + else: + print(" OK: Final pointers match data size.") + + if not valid_check_passed: + print( + "Error: Validation checks failed. Output file might be incorrect.", + file=sys.stderr, + ) + # Optional: Exit here if validation fails + # return False + + # --- Explicitly delete large intermediate arrays --- + print( + f"[{time.time() - start_time:.2f}s] Deleting original neighbors and offsets arrays..." + ) + del neighbors_np + del offsets_np + gc.collect() + + print( + f" CSR Stats: |data|={len(compact_neighbors_data)}, |level_ptr|={len(compact_level_ptr)}" + ) + + # --- Write CSR HNSW graph data using unified function --- + print( + f"[{time.time() - start_time:.2f}s] Writing CSR HNSW graph data in FAISS-compatible order..." + ) + + # Determine storage fourcc and data based on prune_embeddings + if prune_embeddings: + print(" Pruning embeddings: Writing NULL storage marker.") + output_storage_fourcc = NULL_INDEX_FOURCC + storage_data = b"" + else: + # Keep embeddings - read and preserve original storage data + if storage_fourcc and storage_fourcc != NULL_INDEX_FOURCC: + print(" Preserving embeddings: Reading original storage data...") + storage_data = f_in.read() # Read remaining storage data + output_storage_fourcc = storage_fourcc + print(f" Read {len(storage_data)} bytes of storage data") + else: + print(" No embeddings found in original file (NULL storage)") + output_storage_fourcc = NULL_INDEX_FOURCC + storage_data = b"" + + # Use the unified write function + write_compact_format( + f_out, + original_hnsw_data, + assign_probas_np, + cum_nneighbor_per_level_np, + levels_np, + compact_level_ptr, + compact_node_offsets_np, + compact_neighbors_data, + output_storage_fourcc, + storage_data, + ) + + # Clean up memory + del assign_probas_np, cum_nneighbor_per_level_np, levels_np + del compact_neighbors_data, compact_level_ptr, compact_node_offsets_np + gc.collect() + + end_time = time.time() + print(f"[{end_time - start_time:.2f}s] Conversion complete.") + return True + + except FileNotFoundError: + print(f"Error: Input file not found: {input_filename}", file=sys.stderr) + return False + except MemoryError as e: + print( + f"\nFatal MemoryError during conversion: {e}. Insufficient RAM.", + file=sys.stderr, + ) + # Clean up potentially partially written output file? + try: + os.remove(output_filename) + except OSError: + pass + return False + except EOFError as e: + print( + f"Error: Reached end of file unexpectedly reading {input_filename}. {e}", + file=sys.stderr, + ) + try: + os.remove(output_filename) + except OSError: + pass + return False + except Exception as e: + print(f"An unexpected error occurred during conversion: {e}", file=sys.stderr) + import traceback + + traceback.print_exc() + try: + os.remove(output_filename) + except OSError: + pass + return False + # Ensure neighbors_np is deleted even if an error occurs after its allocation + finally: + try: + if "neighbors_np" in locals() and neighbors_np is not None: + del neighbors_np + gc.collect() + except NameError: + pass + + +def prune_hnsw_embeddings_inplace(index_filename: str) -> bool: + """Convenience wrapper to prune embeddings in-place.""" + + temp_path = f"{index_filename}.prune.tmp" + success = prune_hnsw_embeddings(index_filename, temp_path) + if success: + try: + os.replace(temp_path, index_filename) + except Exception as exc: # pragma: no cover - defensive + logger.error(f"Failed to replace original index with pruned version: {exc}") + try: + os.remove(temp_path) + except OSError: + pass + return False + else: + try: + os.remove(temp_path) + except OSError: + pass + return success + + +# --- Script Execution --- +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Convert a Faiss IndexHNSWFlat file to a CSR-based HNSW graph file." + ) + parser.add_argument("input_index_file", help="Path to the input IndexHNSWFlat file") + parser.add_argument( + "output_csr_graph_file", help="Path to write the output CSR HNSW graph file" + ) + parser.add_argument( + "--prune-embeddings", + action="store_true", + default=True, + help="Prune embedding storage (write NULL storage marker)", + ) + parser.add_argument( + "--keep-embeddings", + action="store_true", + help="Keep embedding storage (overrides --prune-embeddings)", + ) + + args = parser.parse_args() + + if not os.path.exists(args.input_index_file): + print(f"Error: Input file not found: {args.input_index_file}", file=sys.stderr) + sys.exit(1) + + if os.path.abspath(args.input_index_file) == os.path.abspath(args.output_csr_graph_file): + print("Error: Input and output filenames cannot be the same.", file=sys.stderr) + sys.exit(1) + + prune_embeddings = args.prune_embeddings and not args.keep_embeddings + success = convert_hnsw_graph_to_csr( + args.input_index_file, args.output_csr_graph_file, prune_embeddings + ) + if not success: + sys.exit(1) diff --git a/packages/leann-backend-hnsw/leann_backend_hnsw/hnsw_backend.py b/packages/leann-backend-hnsw/leann_backend_hnsw/hnsw_backend.py new file mode 100644 index 0000000..7022009 --- /dev/null +++ b/packages/leann-backend-hnsw/leann_backend_hnsw/hnsw_backend.py @@ -0,0 +1,289 @@ +import logging +import os +import shutil +import time +from pathlib import Path +from typing import Any, Literal, Optional + +import numpy as np +from leann.interface import ( + LeannBackendBuilderInterface, + LeannBackendFactoryInterface, + LeannBackendSearcherInterface, +) +from leann.registry import register_backend +from leann.searcher_base import BaseSearcher + +from .convert_to_csr import convert_hnsw_graph_to_csr, prune_hnsw_embeddings_inplace + +logger = logging.getLogger(__name__) + + +def get_metric_map(): + from . import faiss # type: ignore + + return { + "mips": faiss.METRIC_INNER_PRODUCT, + "l2": faiss.METRIC_L2, + "cosine": faiss.METRIC_INNER_PRODUCT, + } + + +def normalize_l2(data: np.ndarray) -> np.ndarray: + norms = np.linalg.norm(data, axis=1, keepdims=True) + norms[norms == 0] = 1 # Avoid division by zero + return data / norms + + +@register_backend("hnsw") +class HNSWBackend(LeannBackendFactoryInterface): + @staticmethod + def builder(**kwargs) -> LeannBackendBuilderInterface: + return HNSWBuilder(**kwargs) + + @staticmethod + def searcher(index_path: str, **kwargs) -> LeannBackendSearcherInterface: + return HNSWSearcher(index_path, **kwargs) + + +class HNSWBuilder(LeannBackendBuilderInterface): + def __init__(self, **kwargs): + self.build_params = kwargs.copy() + self.is_compact = self.build_params.setdefault("is_compact", True) + self.is_recompute = self.build_params.setdefault("is_recompute", True) + self.M = self.build_params.setdefault("M", 32) + self.efConstruction = self.build_params.setdefault("efConstruction", 200) + self.distance_metric = self.build_params.setdefault("distance_metric", "mips") + self.dimensions = self.build_params.get("dimensions") + if not self.is_recompute and self.is_compact: + # Auto-correct: non-recompute requires non-compact storage for HNSW + logger.warning( + "is_recompute=False requires non-compact HNSW. Forcing is_compact=False." + ) + self.is_compact = False + self.build_params["is_compact"] = False + + def build(self, data: np.ndarray, ids: list[str], index_path: str, **kwargs): + from . import faiss # type: ignore + + path = Path(index_path) + index_dir = path.parent + index_prefix = path.stem + index_dir.mkdir(parents=True, exist_ok=True) + + if data.dtype != np.float32: + logger.warning(f"Converting data to float32, shape: {data.shape}") + data = data.astype(np.float32) + + metric_enum = get_metric_map().get(self.distance_metric.lower()) + if metric_enum is None: + raise ValueError(f"Unsupported distance_metric '{self.distance_metric}'.") + + dim = self.dimensions or data.shape[1] + index = faiss.IndexHNSWFlat(dim, self.M, metric_enum) + index.hnsw.efConstruction = self.efConstruction + + if self.distance_metric.lower() == "cosine": + data = normalize_l2(data) + + index.add(data.shape[0], faiss.swig_ptr(data)) + index_file = index_dir / f"{index_prefix}.index" + faiss.write_index(index, str(index_file)) + + # Persist ID map so searcher can map FAISS integer labels back to passage IDs + try: + idmap_file = index_dir / f"{index_prefix}.ids.txt" + with open(idmap_file, "w", encoding="utf-8") as f: + for id_str in ids: + f.write(str(id_str) + "\n") + except Exception as e: + logger.warning(f"Failed to write ID map: {e}") + + if self.is_compact: + self._convert_to_csr(index_file) + elif self.is_recompute: + prune_hnsw_embeddings_inplace(str(index_file)) + + def _convert_to_csr(self, index_file: Path): + """Convert built index to CSR format""" + mode_str = "CSR-pruned" if self.is_recompute else "CSR-standard" + logger.info(f"INFO: Converting HNSW index to {mode_str} format...") + + csr_temp_file = index_file.with_suffix(".csr.tmp") + + success = convert_hnsw_graph_to_csr( + str(index_file), str(csr_temp_file), prune_embeddings=self.is_recompute + ) + + if success: + logger.info("βœ… CSR conversion successful.") + # index_file_old = index_file.with_suffix(".old") + # shutil.move(str(index_file), str(index_file_old)) + shutil.move(str(csr_temp_file), str(index_file)) + logger.info(f"INFO: Replaced original index with {mode_str} version at '{index_file}'") + else: + # Clean up and fail fast + if csr_temp_file.exists(): + os.remove(csr_temp_file) + raise RuntimeError("CSR conversion failed - cannot proceed with compact format") + + +class HNSWSearcher(BaseSearcher): + def __init__(self, index_path: str, **kwargs): + super().__init__( + index_path, + backend_module_name="leann_backend_hnsw.hnsw_embedding_server", + **kwargs, + ) + from . import faiss # type: ignore + + self.distance_metric = ( + self.meta.get("backend_kwargs", {}).get("distance_metric", "mips").lower() + ) + metric_enum = get_metric_map().get(self.distance_metric) + if metric_enum is None: + raise ValueError(f"Unsupported distance_metric '{self.distance_metric}'.") + + backend_meta_kwargs = self.meta.get("backend_kwargs", {}) + self.is_compact = self.meta.get("is_compact", backend_meta_kwargs.get("is_compact", True)) + default_pruned = backend_meta_kwargs.get("is_recompute", self.is_compact) + self.is_pruned = bool(self.meta.get("is_pruned", default_pruned)) + + index_file = self.index_dir / f"{self.index_path.stem}.index" + if not index_file.exists(): + raise FileNotFoundError(f"HNSW index file not found at {index_file}") + + hnsw_config = faiss.HNSWIndexConfig() + hnsw_config.is_compact = self.is_compact + hnsw_config.is_recompute = ( + self.is_pruned + ) # In C++ code, it's called is_recompute, but it's only for loading IIUC. + + self._index = faiss.read_index(str(index_file), faiss.IO_FLAG_MMAP, hnsw_config) + + # Load ID map if available + self._id_map: list[str] = [] + try: + idmap_file = self.index_dir / f"{self.index_path.stem}.ids.txt" + if idmap_file.exists(): + with open(idmap_file, encoding="utf-8") as f: + self._id_map = [line.rstrip("\n") for line in f] + except Exception as e: + logger.warning(f"Failed to load ID map: {e}") + + def search( + self, + query: np.ndarray, + top_k: int, + zmq_port: Optional[int] = None, + complexity: int = 64, + beam_width: int = 1, + prune_ratio: float = 0.0, + recompute_embeddings: bool = True, + pruning_strategy: Literal["global", "local", "proportional"] = "global", + batch_size: int = 0, + **kwargs, + ) -> dict[str, Any]: + """ + Search for nearest neighbors using HNSW index. + + Args: + query: Query vectors (B, D) where B is batch size, D is dimension + top_k: Number of nearest neighbors to return + complexity: Search complexity/efSearch, higher = more accurate but slower + beam_width: Number of parallel search paths/beam_size + prune_ratio: Ratio of neighbors to prune via PQ (0.0-1.0) + recompute_embeddings: Whether to fetch fresh embeddings from server + pruning_strategy: PQ candidate selection strategy: + - "global": Use global PQ queue size for selection (default) + - "local": Local pruning, sort and select best candidates + - "proportional": Base selection on new neighbor count ratio + zmq_port: ZMQ port for embedding server communication. Must be provided if recompute_embeddings is True. + batch_size: Neighbor processing batch size, 0=disabled (HNSW-specific) + **kwargs: Additional HNSW-specific parameters (for legacy compatibility) + + Returns: + Dict with 'labels' (list of lists) and 'distances' (ndarray) + """ + from . import faiss # type: ignore + + if not recompute_embeddings and self.is_pruned: + raise RuntimeError( + "Recompute is required for pruned/compact HNSW index. " + "Re-run search with --recompute, or rebuild with --no-recompute and --no-compact." + ) + if recompute_embeddings: + if zmq_port is None: + raise ValueError("zmq_port must be provided if recompute_embeddings is True") + if hasattr(self._index, "set_zmq_port"): + self._index.set_zmq_port(zmq_port) + + if query.dtype != np.float32: + query = query.astype(np.float32) + if self.distance_metric == "cosine": + query = normalize_l2(query) + + params = faiss.SearchParametersHNSW() + if zmq_port is not None: + params.zmq_port = zmq_port # C++ code won't use this if recompute_embeddings is False + params.efSearch = complexity + params.beam_size = beam_width + + # For OpenAI embeddings with cosine distance, disable relative distance check + # This prevents early termination when all scores are in a narrow range + embedding_model = self.meta.get("embedding_model", "").lower() + if self.distance_metric == "cosine" and any( + openai_model in embedding_model for openai_model in ["text-embedding", "openai"] + ): + params.check_relative_distance = False + else: + params.check_relative_distance = True + + # PQ pruning: direct mapping to HNSW's pq_pruning_ratio + params.pq_pruning_ratio = prune_ratio + + # Map pruning_strategy to HNSW parameters + if pruning_strategy == "local": + params.local_prune = True + params.send_neigh_times_ratio = 0.0 + elif pruning_strategy == "proportional": + params.local_prune = False + params.send_neigh_times_ratio = 1.0 # Any value > 1e-6 triggers proportional mode + else: # "global" + params.local_prune = False + params.send_neigh_times_ratio = 0.0 + + # HNSW-specific batch processing parameter + params.batch_size = batch_size + + batch_size_query = query.shape[0] + distances = np.empty((batch_size_query, top_k), dtype=np.float32) + labels = np.empty((batch_size_query, top_k), dtype=np.int64) + + search_time = time.time() + self._index.search( + query.shape[0], + faiss.swig_ptr(query), + top_k, + faiss.swig_ptr(distances), + faiss.swig_ptr(labels), + params, + ) + search_time = time.time() - search_time + logger.info(f" Search time in HNSWSearcher.search() backend: {search_time} seconds") + if self._id_map: + + def map_label(x: int) -> str: + if 0 <= x < len(self._id_map): + return self._id_map[x] + return str(x) + + string_labels = [ + [map_label(int(label)) for label in batch_labels] for batch_labels in labels + ] + else: + string_labels = [ + [str(int_label) for int_label in batch_labels] for batch_labels in labels + ] + + return {"labels": string_labels, "distances": distances} diff --git a/packages/leann-backend-hnsw/leann_backend_hnsw/hnsw_embedding_server.py b/packages/leann-backend-hnsw/leann_backend_hnsw/hnsw_embedding_server.py new file mode 100644 index 0000000..c93a996 --- /dev/null +++ b/packages/leann-backend-hnsw/leann_backend_hnsw/hnsw_embedding_server.py @@ -0,0 +1,546 @@ +""" +HNSW-specific embedding server +""" + +import argparse +import json +import logging +import os +import sys +import threading +import time +from pathlib import Path +from typing import Any, Optional + +import msgpack +import numpy as np +import zmq + +# Set up logging based on environment variable +LOG_LEVEL = os.getenv("LEANN_LOG_LEVEL", "WARNING").upper() +logger = logging.getLogger(__name__) + +# Force set logger level (don't rely on basicConfig in subprocess) +log_level = getattr(logging, LOG_LEVEL, logging.WARNING) +logger.setLevel(log_level) + +# Ensure we have handlers if none exist +if not logger.handlers: + stream_handler = logging.StreamHandler() + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) + +log_path = os.getenv("LEANN_HNSW_LOG_PATH") +if log_path: + try: + file_handler = logging.FileHandler(log_path, mode="a", encoding="utf-8") + file_formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - [pid=%(process)d] %(message)s" + ) + file_handler.setFormatter(file_formatter) + logger.addHandler(file_handler) + except Exception as exc: # pragma: no cover - best effort logging + logger.warning(f"Failed to attach file handler for log path {log_path}: {exc}") + +logger.propagate = False + +_RAW_PROVIDER_OPTIONS = os.getenv("LEANN_EMBEDDING_OPTIONS") +try: + PROVIDER_OPTIONS: dict[str, Any] = ( + json.loads(_RAW_PROVIDER_OPTIONS) if _RAW_PROVIDER_OPTIONS else {} + ) +except json.JSONDecodeError: + logger.warning("Failed to parse LEANN_EMBEDDING_OPTIONS; ignoring provider options") + PROVIDER_OPTIONS = {} + + +def create_hnsw_embedding_server( + passages_file: Optional[str] = None, + zmq_port: int = 5555, + model_name: str = "sentence-transformers/all-mpnet-base-v2", + distance_metric: str = "mips", + embedding_mode: str = "sentence-transformers", + enable_warmup: bool = False, + daemon_ttl: int = 0, +): + """ + Create and start a ZMQ-based embedding server for HNSW backend. + Simplified version using unified embedding computation module. + """ + logger.info(f"Starting HNSW server on port {zmq_port} with model {model_name}") + logger.info(f"Using embedding mode: {embedding_mode}") + + # Add leann-core to path for unified embedding computation + current_dir = Path(__file__).parent + leann_core_path = current_dir.parent.parent / "leann-core" / "src" + sys.path.insert(0, str(leann_core_path)) + + try: + from leann.api import PassageManager + from leann.embedding_compute import compute_embeddings + + logger.info("Successfully imported unified embedding computation module") + except ImportError as e: + logger.error(f"Failed to import embedding computation module: {e}") + return + finally: + sys.path.pop(0) + + if enable_warmup: + try: + logger.info("Starting warmup embedding request...") + _ = compute_embeddings( + ["__LEANN_WARMUP__"], + model_name, + mode=embedding_mode, + provider_options=PROVIDER_OPTIONS, + ) + logger.info("Warmup complete.") + except Exception as exc: + logger.warning(f"Warmup failed (continuing): {exc}") + + # Check port availability + import socket + + def check_port(port): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(("localhost", port)) == 0 + + if check_port(zmq_port): + logger.error(f"Port {zmq_port} is already in use") + return + + # Only support metadata file, fail fast for everything else + if not passages_file or not passages_file.endswith(".meta.json"): + raise ValueError("Only metadata files (.meta.json) are supported") + + # Load metadata to get passage sources + with open(passages_file) as f: + meta = json.load(f) + + # Let PassageManager handle path resolution uniformly. It supports fallback order: + # 1) path/index_path; 2) *_relative; 3) standard siblings next to meta + passages = PassageManager(meta["passage_sources"], metadata_file_path=passages_file) + # Dimension from metadata for shaping responses + try: + embedding_dim: int = int(meta.get("dimensions", 0)) + except Exception: + embedding_dim = 0 + logger.info(f"Loaded PassageManager with {len(passages)} passages from metadata") + + # Attempt to load ID map (maps FAISS integer labels -> passage IDs) + id_map: list[str] = [] + try: + meta_path = Path(passages_file) + base = meta_path.name + if base.endswith(".meta.json"): + base = base[: -len(".meta.json")] # e.g., laion_index.leann + if base.endswith(".leann"): + base = base[: -len(".leann")] # e.g., laion_index + idmap_file = meta_path.parent / f"{base}.ids.txt" + if idmap_file.exists(): + with open(idmap_file, encoding="utf-8") as f: + id_map = [line.rstrip("\n") for line in f] + logger.info(f"Loaded ID map with {len(id_map)} entries from {idmap_file}") + else: + logger.warning(f"ID map file not found at {idmap_file}; will use raw labels") + except Exception as e: + logger.warning(f"Failed to load ID map: {e}") + + def _map_node_id(nid) -> str: + try: + if id_map is not None and len(id_map) > 0 and isinstance(nid, (int, np.integer)): + idx = int(nid) + if 0 <= idx < len(id_map): + return id_map[idx] + except Exception: + pass + return str(nid) + + def zmq_server_thread_with_shutdown(shutdown_event): + """ZMQ server thread that respects shutdown signal. + + Creates its own REP socket bound to zmq_port and polls with timeouts + to allow graceful shutdown. + """ + logger.info("ZMQ server thread started with shutdown support") + + context = zmq.Context() + rep_socket = context.socket(zmq.REP) + rep_socket.bind(f"tcp://*:{zmq_port}") + logger.info(f"HNSW ZMQ REP server listening on port {zmq_port}") + rep_socket.setsockopt(zmq.RCVTIMEO, 1000) + rep_socket.setsockopt(zmq.SNDTIMEO, 1000) + rep_socket.setsockopt(zmq.LINGER, 0) + + last_request_type = "unknown" + last_request_length = 0 + + def _build_safe_fallback(): + if last_request_type == "distance": + large_distance = 1e9 + fallback_len = max(0, int(last_request_length)) + return [[large_distance] * fallback_len] + if last_request_type == "embedding": + bsz = max(0, int(last_request_length)) + dim = max(0, int(embedding_dim)) + if dim > 0: + return [[bsz, dim], [0.0] * (bsz * dim)] + return [[0, 0], []] + if last_request_type == "text": + return [] + return [[0, int(embedding_dim) if embedding_dim > 0 else 0], []] + + def _handle_text_embedding(request: list[str]) -> None: + nonlocal last_request_type, last_request_length + + e2e_start = time.time() + last_request_type = "text" + last_request_length = len(request) + embeddings = compute_embeddings( + request, + model_name, + mode=embedding_mode, + provider_options=PROVIDER_OPTIONS, + ) + rep_socket.send(msgpack.packb(embeddings.tolist())) + e2e_end = time.time() + logger.info(f"⏱️ Direct text embedding E2E time: {e2e_end - e2e_start:.6f}s") + + def _handle_distance_request(request: list[Any]) -> None: + nonlocal last_request_type, last_request_length + + e2e_start = time.time() + node_ids = request[0] + if len(node_ids) == 1 and isinstance(node_ids[0], list): + node_ids = node_ids[0] + query_vector = np.array(request[1], dtype=np.float32) + last_request_type = "distance" + last_request_length = len(node_ids) + + logger.debug("Distance calculation request received") + logger.debug(f" Node IDs: {node_ids}") + logger.debug(f" Query vector dim: {len(query_vector)}") + + texts: list[str] = [] + found_indices: list[int] = [] + for idx, nid in enumerate(node_ids): + try: + passage_id = _map_node_id(nid) + passage_data = passages.get_passage(passage_id) + txt = passage_data.get("text", "") + if isinstance(txt, str) and len(txt) > 0: + texts.append(txt) + found_indices.append(idx) + else: + logger.error(f"Empty text for passage ID {passage_id}") + except KeyError: + logger.error(f"Passage ID {nid} not found") + except Exception as exc: + logger.error(f"Exception looking up passage ID {nid}: {exc}") + + large_distance = 1e9 + response_distances = [large_distance] * len(node_ids) + + if texts: + try: + embeddings = compute_embeddings( + texts, + model_name, + mode=embedding_mode, + provider_options=PROVIDER_OPTIONS, + ) + logger.info( + f"Computed embeddings for {len(texts)} texts, shape: {embeddings.shape}" + ) + if distance_metric == "l2": + partial = np.sum( + np.square(embeddings - query_vector.reshape(1, -1)), axis=1 + ) + else: + partial = -np.dot(embeddings, query_vector) + + for pos, dval in zip(found_indices, partial.flatten().tolist()): + response_distances[pos] = float(dval) + except Exception as exc: + logger.error(f"Distance computation error, using sentinels: {exc}") + + rep_socket.send(msgpack.packb([response_distances], use_single_float=True)) + e2e_end = time.time() + logger.info(f"⏱️ Distance calculation E2E time: {e2e_end - e2e_start:.6f}s") + + def _handle_embedding_by_id(request: Any) -> None: + nonlocal last_request_type, last_request_length + + if isinstance(request, list) and len(request) == 1 and isinstance(request[0], list): + node_ids = request[0] + elif isinstance(request, list): + node_ids = request + else: + node_ids = [] + + e2e_start = time.time() + last_request_type = "embedding" + last_request_length = len(node_ids) + logger.info(f"ZMQ received {len(node_ids)} node IDs for embedding fetch") + + if embedding_dim <= 0: + dims = [0, 0] + flat_data: list[float] = [] + else: + dims = [len(node_ids), embedding_dim] + flat_data = [0.0] * (dims[0] * dims[1]) + + texts: list[str] = [] + found_indices: list[int] = [] + for idx, nid in enumerate(node_ids): + try: + passage_id = _map_node_id(nid) + passage_data = passages.get_passage(passage_id) + txt = passage_data.get("text", "") + if isinstance(txt, str) and len(txt) > 0: + texts.append(txt) + found_indices.append(idx) + else: + logger.error(f"Empty text for passage ID {passage_id}") + except KeyError: + logger.error(f"Passage with ID {nid} not found") + except Exception as exc: + logger.error(f"Exception looking up passage ID {nid}: {exc}") + + if texts: + try: + embeddings = compute_embeddings( + texts, + model_name, + mode=embedding_mode, + provider_options=PROVIDER_OPTIONS, + ) + logger.info( + f"Computed embeddings for {len(texts)} texts, shape: {embeddings.shape}" + ) + + if np.isnan(embeddings).any() or np.isinf(embeddings).any(): + logger.error( + f"NaN or Inf detected in embeddings! Requested IDs: {node_ids[:5]}..." + ) + dims = [0, embedding_dim] + flat_data = [] + else: + emb_f32 = np.ascontiguousarray(embeddings, dtype=np.float32) + flat = emb_f32.flatten().tolist() + for j, pos in enumerate(found_indices): + start = pos * embedding_dim + end = start + embedding_dim + if end <= len(flat_data): + flat_data[start:end] = flat[ + j * embedding_dim : (j + 1) * embedding_dim + ] + except Exception as exc: + logger.error(f"Embedding computation error, returning zeros: {exc}") + + response_payload = [dims, flat_data] + rep_socket.send(msgpack.packb(response_payload, use_single_float=True)) + e2e_end = time.time() + logger.info(f"⏱️ Fallback Embed by Id E2E time: {e2e_end - e2e_start:.6f}s") + + try: + while not shutdown_event.is_set(): + try: + logger.debug("πŸ” Waiting for ZMQ message...") + request_bytes = rep_socket.recv() + last_activity[0] = time.time() + except zmq.Again: + continue + + try: + request = msgpack.unpackb(request_bytes) + except Exception as exc: + if shutdown_event.is_set(): + logger.info("Shutdown in progress, ignoring ZMQ error") + break + logger.error(f"Error unpacking ZMQ message: {exc}") + try: + safe = _build_safe_fallback() + rep_socket.send(msgpack.packb(safe, use_single_float=True)) + except Exception: + pass + continue + + try: + # Model query + if ( + isinstance(request, list) + and len(request) == 1 + and request[0] == "__QUERY_MODEL__" + ): + rep_socket.send(msgpack.packb([model_name])) + # Direct text embedding + elif ( + isinstance(request, list) + and request + and all(isinstance(item, str) for item in request) + ): + _handle_text_embedding(request) + # Distance calculation: [[ids], [query_vector]] + elif ( + isinstance(request, list) + and len(request) == 2 + and isinstance(request[0], list) + and isinstance(request[1], list) + ): + _handle_distance_request(request) + # Embedding-by-id fallback + else: + _handle_embedding_by_id(request) + except Exception as exc: + if shutdown_event.is_set(): + logger.info("Shutdown in progress, ignoring ZMQ error") + break + logger.error(f"Error in ZMQ server loop: {exc}") + try: + safe = _build_safe_fallback() + rep_socket.send(msgpack.packb(safe, use_single_float=True)) + except Exception: + pass + finally: + try: + rep_socket.close(0) + except Exception: + pass + try: + context.term() + except Exception: + pass + + logger.info("ZMQ server thread exiting gracefully") + + # Add shutdown coordination + shutdown_event = threading.Event() + last_activity = [time.time()] + + def shutdown_zmq_server(): + """Gracefully shutdown ZMQ server.""" + logger.info("Initiating graceful shutdown...") + shutdown_event.set() + + if zmq_thread.is_alive(): + logger.info("Waiting for ZMQ thread to finish...") + zmq_thread.join(timeout=5) + if zmq_thread.is_alive(): + logger.warning("ZMQ thread did not finish in time") + + # Clean up ZMQ resources + try: + # Note: socket and context are cleaned up by thread exit + logger.info("ZMQ resources cleaned up") + except Exception as e: + logger.warning(f"Error cleaning ZMQ resources: {e}") + + # Clean up other resources + try: + import gc + + gc.collect() + logger.info("Additional resources cleaned up") + except Exception as e: + logger.warning(f"Error cleaning additional resources: {e}") + + logger.info("Graceful shutdown completed") + sys.exit(0) + + # Register signal handlers within this function scope + import signal + + def signal_handler(sig, frame): + logger.info(f"Received signal {sig}, shutting down gracefully...") + shutdown_zmq_server() + + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + # Pass shutdown_event to ZMQ thread + zmq_thread = threading.Thread( + target=lambda: zmq_server_thread_with_shutdown(shutdown_event), + daemon=False, # Not daemon - we want to wait for it + ) + zmq_thread.start() + logger.info(f"Started HNSW ZMQ server thread on port {zmq_port}") + + # Keep the main thread alive + try: + while not shutdown_event.is_set(): + if daemon_ttl > 0 and (time.time() - last_activity[0]) >= daemon_ttl: + logger.info( + f"No requests for {daemon_ttl} seconds, shutting down daemon on port {zmq_port}" + ) + shutdown_zmq_server() + return + time.sleep(0.1) # Check shutdown more frequently + except KeyboardInterrupt: + logger.info("HNSW Server shutting down...") + shutdown_zmq_server() + return + + # If we reach here, shutdown was triggered by signal + logger.info("Main loop exited, process should be shutting down") + + +if __name__ == "__main__": + import sys + + # Signal handlers are now registered within create_hnsw_embedding_server + + parser = argparse.ArgumentParser(description="HNSW Embedding service") + parser.add_argument("--zmq-port", type=int, default=5555, help="ZMQ port to run on") + parser.add_argument( + "--passages-file", + type=str, + help="JSON file containing passage ID to text mapping", + ) + parser.add_argument( + "--model-name", + type=str, + default="sentence-transformers/all-mpnet-base-v2", + help="Embedding model name", + ) + parser.add_argument( + "--distance-metric", type=str, default="mips", help="Distance metric to use" + ) + parser.add_argument( + "--embedding-mode", + type=str, + default="sentence-transformers", + choices=["sentence-transformers", "openai", "mlx", "ollama"], + help="Embedding backend mode", + ) + parser.add_argument( + "--enable-warmup", + action="store_true", + help="Preload model by running one warmup embedding at startup", + ) + parser.add_argument( + "--daemon-mode", + action="store_true", + help="Run as daemon mode (enables idle TTL checks)", + ) + parser.add_argument( + "--daemon-ttl", + type=int, + default=0, + help="Idle TTL in seconds for daemon mode; 0 disables auto-exit", + ) + + args = parser.parse_args() + + # Create and start the HNSW embedding server + create_hnsw_embedding_server( + passages_file=args.passages_file, + zmq_port=args.zmq_port, + model_name=args.model_name, + distance_metric=args.distance_metric, + embedding_mode=args.embedding_mode, + enable_warmup=args.enable_warmup, + daemon_ttl=args.daemon_ttl if args.daemon_mode else 0, + ) diff --git a/packages/leann-backend-hnsw/pyproject.toml b/packages/leann-backend-hnsw/pyproject.toml new file mode 100644 index 0000000..bc4a4ff --- /dev/null +++ b/packages/leann-backend-hnsw/pyproject.toml @@ -0,0 +1,27 @@ +# packages/leann-backend-hnsw/pyproject.toml + +[build-system] +requires = ["scikit-build-core>=0.10", "numpy", "swig"] +build-backend = "scikit_build_core.build" + +[project] +name = "leann-backend-hnsw" +version = "0.3.7" +description = "Custom-built HNSW (Faiss) backend for the Leann toolkit." +dependencies = [ + "leann-core==0.3.7", + "numpy", + "pyzmq>=23.0.0", + "msgpack>=1.0.0", +] + +[tool.scikit-build] +wheel.packages = ["leann_backend_hnsw"] +editable.mode = "redirect" +cmake.build-type = "Release" +build.verbose = true + +# CMake definitions to optimize compilation and find Homebrew packages +[tool.scikit-build.cmake.define] +CMAKE_PREFIX_PATH = {env = "CMAKE_PREFIX_PATH"} +OpenMP_ROOT = {env = "OpenMP_ROOT"} diff --git a/packages/leann-backend-ivf/README.md b/packages/leann-backend-ivf/README.md new file mode 100644 index 0000000..bb76153 --- /dev/null +++ b/packages/leann-backend-ivf/README.md @@ -0,0 +1,52 @@ +# LEANN IVF Backend + +FAISS **IndexIVFFlat** backend for LEANN with **add** and **delete** APIs for incremental updates (#231, #89, #141). + +## Install + +```bash +uv sync --package leann-backend-ivf +# or +pip install leann-backend-ivf +``` + +Requires `faiss-cpu`. For query embedding during search, the IVF searcher uses the same embedding server as HNSW; install `leann-backend-hnsw` if you use recompute/query embedding. + +## Build + +```bash +leann build my-index --docs ./src --backend-name ivf +``` + +Optional backend kwargs (e.g. via API): `nlist` (default 100), `distance_metric` ("l2" or "cosine"/"mips"). + +## Add / Delete API + +Follows the same idea as HNSW’s update path; use these for incremental workflows (e.g. after Merkle tree / file-change API): + +- **`add_vectors(index_path, embeddings, passage_ids)`** + Appends vectors to an existing IVF index. `embeddings`: `(N, D)` float32; `passage_ids`: list of N passage id strings (must not already exist). + +- **`remove_ids(index_path, passage_ids)`** + Removes vectors by passage id. Returns the number of vectors removed. Use after detecting changed chunk ids: delete old, then re-insert new. + +Example: + +```python +from leann_backend_ivf import add_vectors, remove_ids + +# After file-change API says chunk id "abc123" changed: +remove_ids("/path/to/index.leann", ["abc123"]) +# Re-embed and add new chunk with same or new id +add_vectors("/path/to/index.leann", new_embeddings, ["abc123"]) +``` + +## Core integration (#89, #141) + +`LeannBuilder.update_index(index_path, remove_passage_ids=None)` in leann-core supports IVF: it calls `add_vectors` for appends and, when `remove_passage_ids` is set, calls `remove_ids` first (e.g. from a file-change / Merkle API). Use for incremental build or reindex: detect changed chunk ids β†’ `update_index(path, remove_passage_ids=changed_ids)` then add chunks for the new content. + +## Notes + +- IVF stores vectors in flat lists per cluster; no compact/recompute mode like HNSW. Good for frequent add/delete. +- ID mapping is stored in `ivf_id_map.json` next to the index; passage ids are stable across add/delete. +- Search uses `nprobe` (default from `complexity`, capped by `nlist`). Tune `nlist` at build time for speed/recall. diff --git a/packages/leann-backend-ivf/leann_backend_ivf/__init__.py b/packages/leann-backend-ivf/leann_backend_ivf/__init__.py new file mode 100644 index 0000000..006d822 --- /dev/null +++ b/packages/leann-backend-ivf/leann_backend_ivf/__init__.py @@ -0,0 +1,17 @@ +"""LEANN IVF backend: FAISS IndexIVFFlat with add/delete APIs for incremental updates.""" + +from .ivf_backend import ( + IVFBackend, + IVFBuilder, + IVFSearcher, + add_vectors, + remove_ids, +) + +__all__ = [ + "IVFBackend", + "IVFBuilder", + "IVFSearcher", + "add_vectors", + "remove_ids", +] diff --git a/packages/leann-backend-ivf/leann_backend_ivf/ivf_backend.py b/packages/leann-backend-ivf/leann_backend_ivf/ivf_backend.py new file mode 100644 index 0000000..23c1411 --- /dev/null +++ b/packages/leann-backend-ivf/leann_backend_ivf/ivf_backend.py @@ -0,0 +1,291 @@ +""" +IVF backend: FAISS IndexIVFFlat with DirectMap.Hashtable for add/remove by passage id. + +Uses IndexIVFFlat + DirectMap.Hashtable (no IndexIDMap2) so remove_ids works correctly. +Provides add_vectors() and remove_ids() for incremental updates. +""" + +import json +import logging +from pathlib import Path +from typing import Any, Optional + +import numpy as np + +try: + import faiss +except ImportError: + faiss = None # type: ignore[assignment] + +from leann.interface import ( + LeannBackendBuilderInterface, + LeannBackendFactoryInterface, + LeannBackendSearcherInterface, +) +from leann.registry import register_backend +from leann.searcher_base import BaseSearcher + +logger = logging.getLogger(__name__) + +ID_MAP_FILENAME = "ivf_id_map.json" + + +def _check_faiss(): + if faiss is None: + raise ImportError( + "faiss-cpu is required for IVF backend. Install with: pip install faiss-cpu" + ) + + +def _get_metric_map(): + _check_faiss() + return { + "mips": faiss.METRIC_INNER_PRODUCT, + "l2": faiss.METRIC_L2, + "cosine": faiss.METRIC_INNER_PRODUCT, + } + + +def _normalize_l2(data: np.ndarray) -> np.ndarray: + norms = np.linalg.norm(data, axis=1, keepdims=True) + norms[norms == 0] = 1 + return data / norms + + +def _load_id_map(index_dir: Path, index_prefix: str) -> tuple[dict[int, str], dict[str, int], int]: + """Load id_map.json. Returns (id_to_passage, passage_to_id, next_id).""" + path = index_dir / f"{index_prefix}.{ID_MAP_FILENAME}" + if not path.exists(): + return {}, {}, 0 + with open(path, encoding="utf-8") as f: + data = json.load(f) + id_to_passage = {int(k): v for k, v in data.get("id_to_passage", {}).items()} + passage_to_id = data.get("passage_to_id", {}) + next_id = int(data.get("next_id", 0)) + return id_to_passage, passage_to_id, next_id + + +def _save_id_map( + index_dir: Path, index_prefix: str, id_to_passage: dict[int, str], next_id: int +) -> None: + passage_to_id = {v: k for k, v in id_to_passage.items()} + path = index_dir / f"{index_prefix}.{ID_MAP_FILENAME}" + with open(path, "w", encoding="utf-8") as f: + json.dump( + { + "id_to_passage": {str(k): v for k, v in id_to_passage.items()}, + "passage_to_id": passage_to_id, + "next_id": next_id, + }, + f, + indent=2, + ) + + +@register_backend("ivf") +class IVFBackend(LeannBackendFactoryInterface): + @staticmethod + def builder(**kwargs) -> LeannBackendBuilderInterface: + return IVFBuilder(**kwargs) + + @staticmethod + def searcher(index_path: str, **kwargs) -> LeannBackendSearcherInterface: + return IVFSearcher(index_path, **kwargs) + + +class IVFBuilder(LeannBackendBuilderInterface): + def __init__(self, **kwargs): + _check_faiss() + self.build_params = kwargs.copy() + self.nlist = self.build_params.setdefault("nlist", 100) + self.distance_metric = self.build_params.setdefault("distance_metric", "l2") + self.dimensions = self.build_params.get("dimensions") + + def build(self, data: np.ndarray, ids: list[str], index_path: str, **kwargs) -> None: + _check_faiss() + path = Path(index_path) + index_dir = path.parent + index_prefix = path.stem + index_dir.mkdir(parents=True, exist_ok=True) + + if data.dtype != np.float32: + data = data.astype(np.float32) + data = np.ascontiguousarray(data) + dim = self.dimensions or data.shape[1] + n = data.shape[0] + metric_enum = _get_metric_map().get(self.distance_metric.lower()) + if metric_enum is None: + raise ValueError(f"Unsupported distance_metric '{self.distance_metric}'.") + + if self.distance_metric.lower() == "cosine": + data = _normalize_l2(data) + + quantizer = ( + faiss.IndexFlatL2(dim) if metric_enum == faiss.METRIC_L2 else faiss.IndexFlatIP(dim) + ) + ivf = faiss.IndexIVFFlat(quantizer, dim, self.nlist, metric_enum) + ivf.train(data) + ivf.set_direct_map_type(faiss.DirectMap.Hashtable) + faiss_ids = np.arange(n, dtype=np.int64) + ivf.add_with_ids(data, faiss_ids) + + index_file = index_dir / f"{index_prefix}.index" + faiss.write_index(ivf, str(index_file)) + + id_to_passage = dict(enumerate(ids)) + _save_id_map(index_dir, index_prefix, id_to_passage, next_id=n) + + +class IVFSearcher(BaseSearcher): + def __init__(self, index_path: str, **kwargs): + # Use HNSW embedding server for query embedding if available (same as other backends) + super().__init__( + index_path, + backend_module_name="leann_backend_hnsw.hnsw_embedding_server", + **kwargs, + ) + _check_faiss() + self.distance_metric = ( + self.meta.get("backend_kwargs", {}).get("distance_metric", "l2").lower() + ) + index_prefix = self.index_path.stem + index_file = self.index_dir / f"{index_prefix}.index" + if not index_file.exists(): + raise FileNotFoundError(f"IVF index file not found at {index_file}") + + self._index = faiss.read_index(str(index_file)) + self._id_to_passage: dict[int, str] = {} + id_to_passage, _, _ = _load_id_map(self.index_dir, index_prefix) + self._id_to_passage = id_to_passage + + def search( + self, + query: np.ndarray, + top_k: int, + complexity: int = 64, + nprobe: Optional[int] = None, + **kwargs, + ) -> dict[str, Any]: + _check_faiss() + if query.dtype != np.float32: + query = query.astype(np.float32) + if self.distance_metric == "cosine": + query = _normalize_l2(query) + ivf_index = faiss.extract_index_ivf(self._index) + nprobe = nprobe or min(complexity, ivf_index.nlist) + ivf_index.nprobe = nprobe + distances, label_rows = self._index.search(query, top_k) + + def map_label(x: int) -> str: + return self._id_to_passage.get(int(x), str(x)) + + string_labels = [[map_label(int(lab)) for lab in row] for row in label_rows] + return {"labels": string_labels, "distances": distances} + + def compute_query_embedding( + self, + query: str, + use_server_if_available: bool = True, + zmq_port: Optional[int] = None, + query_template: Optional[str] = None, + ) -> np.ndarray: + return super().compute_query_embedding( + query, + use_server_if_available=use_server_if_available, + zmq_port=zmq_port, + query_template=query_template, + ) + + +def add_vectors(index_path: str, embeddings: np.ndarray, passage_ids: list[str]) -> None: + """ + Append vectors to an existing IVF index (same role as HNSW update_index add path). + + Args: + index_path: Path to the .leann index (e.g. .../documents.leann). + embeddings: (N, D) float32 array. + passage_ids: List of N passage id strings (must not already exist in index). + """ + _check_faiss() + path = Path(index_path) + index_dir = path.parent + index_prefix = path.stem + index_file = index_dir / f"{index_prefix}.index" + if not index_file.exists(): + raise FileNotFoundError(f"IVF index not found: {index_file}") + + embeddings = np.ascontiguousarray(embeddings.astype(np.float32)) + id_to_passage, passage_to_id, next_id = _load_id_map(index_dir, index_prefix) + for pid in passage_ids: + if pid in passage_to_id: + raise ValueError(f"Passage id '{pid}' already exists in index.") + + n = embeddings.shape[0] + if n != len(passage_ids): + raise ValueError("embeddings.shape[0] must equal len(passage_ids).") + + index = faiss.read_index(str(index_file)) + new_ids = np.arange(next_id, next_id + n, dtype=np.int64) + index.add_with_ids(embeddings, new_ids) + faiss.write_index(index, str(index_file)) + + for i, pid in enumerate(passage_ids): + id_to_passage[next_id + i] = pid + _save_id_map(index_dir, index_prefix, id_to_passage, next_id=next_id + n) + logger.info("IVF add_vectors: appended %d vectors, next_id=%d", n, next_id + n) + + +def remove_ids(index_path: str, passage_ids: list[str]) -> int: + """ + Remove vectors by passage id (for incremental update: delete changed chunks before re-insert). + + Args: + index_path: Path to the .leann index. + passage_ids: List of passage id strings to remove. + + Returns: + Number of vectors actually removed. + """ + _check_faiss() + path = Path(index_path) + index_dir = path.parent + index_prefix = path.stem + index_file = index_dir / f"{index_prefix}.index" + if not index_file.exists(): + raise FileNotFoundError(f"IVF index not found: {index_file}") + + id_to_passage, passage_to_id, next_id = _load_id_map(index_dir, index_prefix) + to_remove_int: list[int] = [] + for pid in passage_ids: + if pid in passage_to_id: + to_remove_int.append(passage_to_id[pid]) + if not to_remove_int: + return 0 + + index = faiss.read_index(str(index_file)) + ntotal_before = index.ntotal + sel = np.array(to_remove_int, dtype=np.int64) + nremoved = index.remove_ids(sel) + faiss.write_index(index, str(index_file)) + + for pid in passage_ids: + if pid in passage_to_id: + i = passage_to_id[pid] + id_to_passage.pop(i, None) + # passage_to_id is rebuilt from id_to_passage when we save; we don't decrease next_id so new adds get new ids + _save_id_map(index_dir, index_prefix, id_to_passage, next_id=next_id) + logger.info( + "IVF remove_ids: ntotal %d -> %d, removed %d vectors (requested %d, found %d in id_map)", + ntotal_before, + index.ntotal, + nremoved, + len(passage_ids), + len(to_remove_int), + ) + if nremoved != len(to_remove_int): + logger.warning( + "IVF remove_ids: FAISS removed %d but expected %d. Possible index inconsistency.", + nremoved, + len(to_remove_int), + ) + return nremoved diff --git a/packages/leann-backend-ivf/pyproject.toml b/packages/leann-backend-ivf/pyproject.toml new file mode 100644 index 0000000..caff74c --- /dev/null +++ b/packages/leann-backend-ivf/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "leann-backend-ivf" +version = "0.3.6" +description = "FAISS IVF backend for LEANN with add/delete support for incremental updates." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "leann-core>=0.3.6", + "numpy>=1.20.0", + "faiss-cpu>=1.7.4", +] + +[project.optional-dependencies] +# Optional: use HNSW embedding server for query embedding (same as other backends) +query-server = ["leann-backend-hnsw>=0.3.6", "pyzmq>=23.0.0", "msgpack>=1.0.0"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["leann_backend_ivf*"] diff --git a/packages/leann-core/README.md b/packages/leann-core/README.md new file mode 100644 index 0000000..7f9e2ca --- /dev/null +++ b/packages/leann-core/README.md @@ -0,0 +1,5 @@ +# leann-core + +Core APIs, CLI (`leann`), embedding pipeline, and backend plugin registry for [LEANN](https://github.com/yichuan-w/LEANN) β€” a lightweight local vector/RAG stack. + +For installation, backends (HNSW, IVF, DiskANN), and full documentation, see the [main repository README](https://github.com/yichuan-w/LEANN/blob/main/README.md). diff --git a/packages/leann-core/pyproject.toml b/packages/leann-core/pyproject.toml new file mode 100644 index 0000000..3173282 --- /dev/null +++ b/packages/leann-core/pyproject.toml @@ -0,0 +1,67 @@ +[build-system] +# setuptools>=77: SPDX `license` string (avoids deprecated license table warning). +requires = ["setuptools>=77.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "leann-core" +version = "0.3.7" +description = "Core API and plugin system for LEANN" +readme = "README.md" +requires-python = ">=3.10" +license = "MIT" + +# All required dependencies included +dependencies = [ + # NumPy wheel availability varies by platform/Python version. + # Keep the constraint wide enough that the Arch smoke test can always + # resolve a binary distribution (numpy>=2 isn't available in that env). + "numpy>=1.20.0,<3", + "tqdm>=4.60.0", + "psutil>=5.8.0", + "pyzmq>=23.0.0", + "msgpack>=1.0.0", + "torch>=2.0.0", + "sentence-transformers>=3.0.0", + "llama-index-core>=0.12.0", + "llama-index-readers-file>=0.4.0", # Essential for document reading + "llama-index-embeddings-huggingface>=0.5.5", # For embeddings + "python-dotenv>=1.0.0", + "openai>=1.0.0", + "huggingface-hub>=0.20.0", + # Keep Py3.9 compatible below 4.46; allow newer for Py>=3.10 (e.g., ColQwen/ColPali). + "transformers>=4.30.0,<4.46; python_version < '3.10'", + "transformers>=4.53.1,<4.58; python_version >= '3.10'", + "requests>=2.25.0", + "accelerate>=0.20.0", + "PyPDF2>=3.0.0", + "pymupdf>=1.23.0", + "pdfplumber>=0.10.0", + "nbconvert>=7.0.0", # For .ipynb file support + "gitignore-parser>=0.1.12", # For proper .gitignore handling + "mlx>=0.26.3; sys_platform == 'darwin' and platform_machine == 'arm64'", + "mlx-lm>=0.26.0; sys_platform == 'darwin' and platform_machine == 'arm64'", +] + +[project.optional-dependencies] +cpu = [ + "torch==2.2.2; platform_system == 'Linux' and python_version < '3.13'", +] +colab = [ + "torch>=2.0.0,<3.0.0", # Limit torch version to avoid conflicts + "transformers>=4.30.0,<4.46; python_version < '3.10'", # Py3.9 compatibility + "transformers>=4.53.1,<4.58; python_version >= '3.10'", + "accelerate>=0.20.0,<1.0.0", # Limit accelerate version +] +server = [ + "fastapi>=0.115.0", + "pydantic>=2.0.0", + "uvicorn[standard]>=0.34.0", +] + +[project.scripts] +leann = "leann.cli:main" +leann_mcp = "leann.mcp:main" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/packages/leann-core/src/leann/__init__.py b/packages/leann-core/src/leann/__init__.py new file mode 100644 index 0000000..bd9f327 --- /dev/null +++ b/packages/leann-core/src/leann/__init__.py @@ -0,0 +1,40 @@ +# packages/leann-core/src/leann/__init__.py +import os +import platform + +# ruff: noqa: E402 (env vars must be set before importing the rest of the package) + +# Fix OpenMP/FAISS threading defaults for common platforms +system = platform.system() + +if system == "Darwin": + # macOS ARM64: prevent runaway threading and duplicate lib issues + os.environ["OMP_NUM_THREADS"] = "1" + os.environ["MKL_NUM_THREADS"] = "1" + os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" + os.environ["KMP_BLOCKTIME"] = "0" + # Additional fixes for PyTorch/sentence-transformers on macOS ARM64 only in CI + if os.environ.get("CI") == "true": + os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "0" + os.environ["TOKENIZERS_PARALLELISM"] = "false" +elif system == "Linux": + # Linux CPU-only: default to single-thread to avoid FAISS/ZMQ hangs (issue #208) + os.environ.setdefault("OMP_NUM_THREADS", "1") + os.environ.setdefault("MKL_NUM_THREADS", "1") + os.environ.setdefault("FAISS_NUM_THREADS", "1") + os.environ.setdefault("OMP_WAIT_POLICY", "PASSIVE") + +from .api import LeannBuilder, LeannChat, LeannSearcher +from .react_agent import ReActAgent, create_react_agent +from .registry import BACKEND_REGISTRY, autodiscover_backends + +autodiscover_backends() + +__all__ = [ + "BACKEND_REGISTRY", + "LeannBuilder", + "LeannChat", + "LeannSearcher", + "ReActAgent", + "create_react_agent", +] diff --git a/packages/leann-core/src/leann/__main__.py b/packages/leann-core/src/leann/__main__.py new file mode 100644 index 0000000..ec55b17 --- /dev/null +++ b/packages/leann-core/src/leann/__main__.py @@ -0,0 +1,3 @@ +from leann.cli import main + +main() diff --git a/packages/leann-core/src/leann/api.py b/packages/leann-core/src/leann/api.py new file mode 100644 index 0000000..f5f8f4d --- /dev/null +++ b/packages/leann-core/src/leann/api.py @@ -0,0 +1,1794 @@ +""" +This file contains the core API for the LEANN project, now definitively updated +with the correct, original embedding logic from the user's reference code. +""" + +import json +import logging +import os +import pickle +import re +import subprocess +import time +import warnings +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Literal, Optional, Union + +import numpy as np +from leann_backend_hnsw.convert_to_csr import prune_hnsw_embeddings_inplace + +from leann.interactive_utils import create_api_session +from leann.interface import LeannBackendSearcherInterface + +from .chat import get_llm +from .embedding_server_manager import EmbeddingServerManager +from .interface import LeannBackendFactoryInterface +from .metadata_filter import MetadataFilterEngine +from .registry import BACKEND_REGISTRY + +logger = logging.getLogger(__name__) + +# Passage ID schemes recorded in .meta.json["passage_id_scheme"]. +# - "sequential": today's default; IDs are str(insertion_index) (api.py:add_text). +# - "content-hash": planned in #329; IDs are sha256(text)[:16], stable across +# file moves and reorderings. +# Older indexes have no passage_id_scheme field β€” readers must default to +# "sequential" when the key is absent. See #329 for the rollout plan. +PASSAGE_ID_SCHEME_SEQUENTIAL = "sequential" +PASSAGE_ID_SCHEME_CONTENT_HASH = "content-hash" + + +def get_registered_backends() -> list[str]: + """Get list of registered backend names.""" + return list(BACKEND_REGISTRY.keys()) + + +def compute_embeddings( + chunks: list[str], + model_name: str, + mode: str = "sentence-transformers", + use_server: bool = True, + port: Optional[int] = None, + is_build=False, + provider_options: Optional[dict[str, Any]] = None, +) -> np.ndarray: + """ + Computes embeddings using different backends. + + Args: + chunks: List of text chunks to embed + model_name: Name of the embedding model + mode: Embedding backend mode. Options: + - "sentence-transformers": Use sentence-transformers library (default) + - "mlx": Use MLX backend for Apple Silicon + - "openai": Use OpenAI embedding API + - "gemini": Use Google Gemini embedding API + use_server: Whether to use embedding server (True for search, False for build) + + Returns: + numpy array of embeddings + """ + if use_server: + # Use embedding server (for search/query) + if port is None: + raise ValueError("port is required when use_server is True") + return compute_embeddings_via_server(chunks, model_name, port=port) + else: + # Use direct computation (for build_index) + from .embedding_compute import ( + compute_embeddings as compute_embeddings_direct, + ) + + return compute_embeddings_direct( + chunks, + model_name, + mode=mode, + is_build=is_build, + provider_options=provider_options, + ) + + +def compute_embeddings_via_server(chunks: list[str], model_name: str, port: int) -> np.ndarray: + """Computes embeddings using sentence-transformers. + + Args: + chunks: List of text chunks to embed + model_name: Name of the sentence transformer model + """ + logger.info( + f"Computing embeddings for {len(chunks)} chunks using SentenceTransformer model '{model_name}' (via embedding server)..." + ) + import msgpack + import numpy as np + import zmq + + # Connect to embedding server + context = zmq.Context() + socket = context.socket(zmq.REQ) + socket.connect(f"tcp://localhost:{port}") + + # Send chunks to server for embedding computation + request = chunks + socket.send(msgpack.packb(request)) + + # Receive embeddings from server + response = socket.recv() + embeddings_list = msgpack.unpackb(response) + + # Convert back to numpy array + embeddings = np.array(embeddings_list, dtype=np.float32) + + socket.close() + context.term() + + return embeddings + + +@dataclass +class SearchResult: + id: str + score: float + text: str + metadata: dict[str, Any] = field(default_factory=dict) + + +class PassageManager: + def __init__( + self, passage_sources: list[dict[str, Any]], metadata_file_path: Optional[str] = None + ): + self.offset_maps: dict[str, dict[str, int]] = {} + self.passage_files: dict[str, str] = {} + # Avoid materializing a single gigantic global map to reduce memory + # footprint on very large corpora (e.g., 60M+ passages). Instead, keep + # per-shard maps and do a lightweight per-shard lookup on demand. + self._total_count: int = 0 + self.filter_engine = MetadataFilterEngine() # Initialize filter engine + + # Derive index base name for standard sibling fallbacks, e.g., .passages.* + index_name_base = None + if metadata_file_path: + meta_name = Path(metadata_file_path).name + if meta_name.endswith(".meta.json"): + index_name_base = meta_name[: -len(".meta.json")] + + for source in passage_sources: + assert source["type"] == "jsonl", "only jsonl is supported" + passage_file = source.get("path", "") + index_file = source.get("index_path", "") # .idx file + + # Fix path resolution - relative paths should be relative to metadata file directory + def _resolve_candidates( + primary: str, + relative_key: str, + default_name: Optional[str], + source_dict: dict[str, Any], + ) -> list[Path]: + """ + Build an ordered list of candidate paths. For relative paths specified in + metadata, prefer resolution relative to the metadata file directory first, + then fall back to CWD-based resolution, and finally to conventional + sibling defaults (e.g., .passages.idx / .jsonl). + """ + candidates: list[Path] = [] + # 1) Primary path + if primary: + p = Path(primary) + if p.is_absolute(): + candidates.append(p) + else: + # Prefer metadata-relative resolution for relative paths + if metadata_file_path: + candidates.append(Path(metadata_file_path).parent / p) + # Also consider CWD-relative as a fallback for legacy layouts + candidates.append(Path.cwd() / p) + # 2) metadata-relative explicit relative key (if present) + if metadata_file_path and source_dict.get(relative_key): + candidates.append(Path(metadata_file_path).parent / source_dict[relative_key]) + # 3) metadata-relative standard sibling filename + if metadata_file_path and default_name: + candidates.append(Path(metadata_file_path).parent / default_name) + return candidates + + # Build candidate lists and pick first existing; otherwise keep last candidate for error message + idx_default = f"{index_name_base}.passages.idx" if index_name_base else None + idx_candidates = _resolve_candidates( + index_file, "index_path_relative", idx_default, source + ) + pas_default = f"{index_name_base}.passages.jsonl" if index_name_base else None + pas_candidates = _resolve_candidates(passage_file, "path_relative", pas_default, source) + + def _pick_existing(cands: list[Path]) -> str: + for c in cands: + if c.exists(): + return str(c.resolve()) + # Fallback to last candidate (best guess) even if not exists; will error below + return str(cands[-1].resolve()) if cands else "" + + index_file = _pick_existing(idx_candidates) + passage_file = _pick_existing(pas_candidates) + + if not Path(index_file).exists(): + raise FileNotFoundError(f"Passage index file not found: {index_file}") + + with open(index_file, "rb") as f: + offset_map: dict[str, int] = pickle.load(f) + self.offset_maps[passage_file] = offset_map + self.passage_files[passage_file] = passage_file + self._total_count += len(offset_map) + + def get_passage(self, passage_id: str) -> dict[str, Any]: + # Fast path: check each shard map (there are typically few shards). + # This avoids building a massive combined dict while keeping lookups + # bounded by the number of shards. + for passage_file, offset_map in self.offset_maps.items(): + try: + offset = offset_map[passage_id] + with open(passage_file, encoding="utf-8") as f: + f.seek(offset) + return json.loads(f.readline()) + except KeyError: + continue + raise KeyError(f"Passage ID not found: {passage_id}") + + def filter_search_results( + self, + search_results: list[SearchResult], + metadata_filters: Optional[dict[str, dict[str, Union[str, int, float, bool, list]]]], + ) -> list[SearchResult]: + """ + Apply metadata filters to search results. + + Args: + search_results: List of SearchResult objects + metadata_filters: Filter specifications to apply + + Returns: + Filtered list of SearchResult objects + """ + if not metadata_filters: + return search_results + + logger.debug(f"Applying metadata filters to {len(search_results)} results") + + # Convert SearchResult objects to dictionaries for the filter engine + result_dicts = [] + for result in search_results: + result_dicts.append( + { + "id": result.id, + "score": result.score, + "text": result.text, + "metadata": result.metadata, + } + ) + + # Apply filters using the filter engine + filtered_dicts = self.filter_engine.apply_filters(result_dicts, metadata_filters) + + # Convert back to SearchResult objects + filtered_results = [] + for result_dict in filtered_dicts: + filtered_results.append( + SearchResult( + id=result_dict["id"], + score=result_dict["score"], + text=result_dict["text"], + metadata=result_dict["metadata"], + ) + ) + + logger.debug(f"Filtered results: {len(filtered_results)} remaining") + return filtered_results + + def __len__(self) -> int: + return self._total_count + + +class BM25Index(ABC): + """Minimal contract for a BM25-style sparse index over LEANN passages.""" + + @abstractmethod + def fit(self, documents: list[dict[str, Any]]) -> None: + """Build the index from a corpus. + + `documents` is a list of `{"id": str, "text": str, ...}` entries. Extra + fields are ignored by BM25 implementations but preserved by the caller + for use elsewhere. + """ + + @abstractmethod + def search(self, query: str, top_k: int = 5) -> list["SearchResult"]: + """Return up to `top_k` SearchResult entries ranked by descending score. + + Returned SearchResults have `id` and `score` populated; `text` and + `metadata` are filled in by `LeannSearcher` from the passage store. + """ + + +class Fts5BM25Index(BM25Index): + """BM25 over a SQLite FTS5 virtual table, persisted on disk. + + Built once at `leann build` time, queried memory-bounded at search time. + SQLite owns the on-disk term/posting data; queries hit `bm25()` directly. + """ + + # SQLite's FTS5 bm25() returns lower-is-better. We negate so the rest of + # LeannSearcher (and the hybrid fusion math) can keep higher-is-better. + _SCHEMA = ( + "CREATE VIRTUAL TABLE bm25_passages USING fts5(" + "id UNINDEXED, text, tokenize='unicode61 remove_diacritics 2'" + ")" + ) + + def __init__(self, db_path: str): + self._db_path = db_path + self._conn: Optional[Any] = None + + def _connect(self): + import sqlite3 + + if self._conn is None: + self._conn = sqlite3.connect(self._db_path) + return self._conn + + def fit(self, documents: list[dict[str, Any]]) -> None: + import sqlite3 + + # Fresh DB every fit β€” fit() is a one-shot bulk-load. + if os.path.exists(self._db_path): + os.unlink(self._db_path) + conn = sqlite3.connect(self._db_path) + try: + conn.execute(self._SCHEMA) + conn.executemany( + "INSERT INTO bm25_passages(id, text) VALUES (?, ?)", + ((d["id"], d.get("text", "")) for d in documents), + ) + conn.commit() + finally: + conn.close() + + def search(self, query: str, top_k: int = 5) -> list["SearchResult"]: + # Strip punctuation, lowercase, OR the terms together. Avoids FTS5 + # query syntax surprises (`:`, `*`, etc.) for natural-language queries. + terms = re.sub(r"[^\w\s]", "", query).lower().split() + if not terms: + return [] + fts5_query = " OR ".join(terms) + conn = self._connect() + rows = conn.execute( + "SELECT id, -bm25(bm25_passages) AS score " + "FROM bm25_passages WHERE bm25_passages MATCH ? " + "ORDER BY score DESC LIMIT ?", + (fts5_query, top_k), + ).fetchall() + return [ + SearchResult(id=doc_id, score=float(score), text="", metadata={}) + for doc_id, score in rows + ] + + def close(self) -> None: + if self._conn is not None: + self._conn.close() + self._conn = None + + +class LeannBuilder: + def __init__( + self, + backend_name: str, + embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2", + dimensions: Optional[int] = None, + embedding_mode: str = "sentence-transformers", + embedding_options: Optional[dict[str, Any]] = None, + prebuild_bm25: bool = False, + bm25_backend: str = "fts5", + passage_id_scheme: str = PASSAGE_ID_SCHEME_SEQUENTIAL, + **backend_kwargs, + ): + if bm25_backend != "fts5": + logger.warning(f"bm25_backend={bm25_backend!r} is deprecated; using 'fts5'.") + bm25_backend = "fts5" + self.bm25_backend = bm25_backend + self.prebuild_bm25 = prebuild_bm25 or bm25_backend == "fts5" + if passage_id_scheme not in ( + PASSAGE_ID_SCHEME_SEQUENTIAL, + PASSAGE_ID_SCHEME_CONTENT_HASH, + ): + raise ValueError( + f"Unknown passage_id_scheme: {passage_id_scheme!r}. " + f"Expected one of: {PASSAGE_ID_SCHEME_SEQUENTIAL!r}, " + f"{PASSAGE_ID_SCHEME_CONTENT_HASH!r}." + ) + self.passage_id_scheme = passage_id_scheme + self.backend_name = backend_name + # Normalize incompatible combinations early (for consistent metadata) + if backend_name == "hnsw": + is_recompute = backend_kwargs.get("is_recompute", True) + is_compact = backend_kwargs.get("is_compact", True) + if is_recompute is False and is_compact is True: + warnings.warn( + "HNSW with is_recompute=False requires non-compact storage. Forcing is_compact=False.", + UserWarning, + stacklevel=2, + ) + backend_kwargs["is_compact"] = False + + backend_factory: Optional[LeannBackendFactoryInterface] = BACKEND_REGISTRY.get(backend_name) + if backend_factory is None: + raise ValueError(f"Backend '{backend_name}' not found or not registered.") + self.backend_factory = backend_factory + self.embedding_model = embedding_model + self.dimensions = dimensions + self.embedding_mode = embedding_mode + self.embedding_options = embedding_options or {} + + # Check if we need to use cosine distance for normalized embeddings + normalized_embeddings_models = { + # OpenAI models + ("openai", "text-embedding-ada-002"), + ("openai", "text-embedding-3-small"), + ("openai", "text-embedding-3-large"), + # Voyage AI models + ("voyage", "voyage-2"), + ("voyage", "voyage-3"), + ("voyage", "voyage-large-2"), + ("voyage", "voyage-multilingual-2"), + ("voyage", "voyage-code-2"), + # Cohere models + ("cohere", "embed-english-v3.0"), + ("cohere", "embed-multilingual-v3.0"), + ("cohere", "embed-english-light-v3.0"), + ("cohere", "embed-multilingual-light-v3.0"), + } + + # Also check for patterns in model names + is_normalized = False + current_model_lower = embedding_model.lower() + current_mode_lower = embedding_mode.lower() + + # Check exact matches + for mode, model in normalized_embeddings_models: + if (current_mode_lower == mode and current_model_lower == model) or ( + mode in current_mode_lower and model in current_model_lower + ): + is_normalized = True + break + + # Check patterns + if not is_normalized: + # OpenAI patterns + if "openai" in current_mode_lower or "openai" in current_model_lower: + if any( + pattern in current_model_lower + for pattern in ["text-embedding", "ada", "3-small", "3-large"] + ): + is_normalized = True + # Voyage patterns + elif "voyage" in current_mode_lower or "voyage" in current_model_lower: + is_normalized = True + # Cohere patterns + elif "cohere" in current_mode_lower or "cohere" in current_model_lower: + if "embed" in current_model_lower: + is_normalized = True + + # Handle distance metric + if is_normalized and "distance_metric" not in backend_kwargs: + backend_kwargs["distance_metric"] = "cosine" + warnings.warn( + f"Detected normalized embeddings model '{embedding_model}' with mode '{embedding_mode}'. " + f"Automatically setting distance_metric='cosine' for optimal performance. " + f"Normalized embeddings (L2 norm = 1) should use cosine similarity instead of MIPS.", + UserWarning, + stacklevel=2, + ) + elif is_normalized and backend_kwargs.get("distance_metric", "").lower() != "cosine": + current_metric = backend_kwargs.get("distance_metric", "mips") + warnings.warn( + f"Warning: Using '{current_metric}' distance metric with normalized embeddings model " + f"'{embedding_model}' may lead to suboptimal search results. " + f"Consider using 'cosine' distance metric for better performance.", + UserWarning, + stacklevel=2, + ) + + self.backend_kwargs = backend_kwargs + self.chunks: list[dict[str, Any]] = [] + + def _generate_passage_id(self, text: str) -> str: + """Generate a passage ID per the configured scheme. + + sequential: str(insertion index) β€” fast, position-dependent, current default. + content-hash: sha256(text)[:16] β€” content-stable, dedup-friendly across + file moves and reorderings. See #329 for the design. + """ + if self.passage_id_scheme == PASSAGE_ID_SCHEME_CONTENT_HASH: + import hashlib + + return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16] + return str(len(self.chunks)) + + def add_text(self, text: str, metadata: Optional[dict[str, Any]] = None): + if metadata is None: + metadata = {} + passage_id = metadata.get("id") or self._generate_passage_id(text) + chunk_data = {"id": passage_id, "text": text, "metadata": metadata} + self.chunks.append(chunk_data) + + def build_index(self, index_path: str): + if not self.chunks: + raise ValueError("No chunks added.") + + # Filter out invalid/empty text chunks early to keep passage and embedding counts aligned + valid_chunks: list[dict[str, Any]] = [] + skipped = 0 + for chunk in self.chunks: + text = chunk.get("text", "") + if isinstance(text, str) and text.strip(): + valid_chunks.append(chunk) + else: + skipped += 1 + if skipped > 0: + print( + f"Warning: Skipping {skipped} empty/invalid text chunk(s). Processing {len(valid_chunks)} valid chunks" + ) + self.chunks = valid_chunks + if not self.chunks: + raise ValueError("All provided chunks are empty or invalid. Nothing to index.") + if self.dimensions is None: + self.dimensions = len( + compute_embeddings( + ["dummy"], + self.embedding_model, + self.embedding_mode, + use_server=False, + provider_options=self.embedding_options, + )[0] + ) + path = Path(index_path) + index_dir = path.parent + index_name = path.name + index_dir.mkdir(parents=True, exist_ok=True) + passages_file = index_dir / f"{index_name}.passages.jsonl" + offset_file = index_dir / f"{index_name}.passages.idx" + offset_map = {} + with open(passages_file, "w", encoding="utf-8") as f: + try: + from tqdm import tqdm + + chunk_iterator = tqdm(self.chunks, desc="Writing passages", unit="chunk") + except ImportError: + chunk_iterator = self.chunks + + for chunk in chunk_iterator: + offset = f.tell() + json.dump( + { + "id": chunk["id"], + "text": chunk["text"], + "metadata": chunk["metadata"], + }, + f, + ensure_ascii=False, + ) + f.write("\n") + offset_map[chunk["id"]] = offset + with open(offset_file, "wb") as f: + pickle.dump(offset_map, f) + texts_to_embed = [c["text"] for c in self.chunks] + embeddings = compute_embeddings( + texts_to_embed, + self.embedding_model, + self.embedding_mode, + use_server=False, + is_build=True, + provider_options=self.embedding_options, + ) + string_ids = [chunk["id"] for chunk in self.chunks] + # Persist ID map alongside index so backends that return integer labels can remap to passage IDs + try: + idmap_file = ( + index_dir + / f"{index_name[: -len('.leann')] if index_name.endswith('.leann') else index_name}.ids.txt" + ) + with open(idmap_file, "w", encoding="utf-8") as f: + for sid in string_ids: + f.write(str(sid) + "\n") + except Exception: + pass + current_backend_kwargs = {**self.backend_kwargs, "dimensions": self.dimensions} + builder_instance = self.backend_factory.builder(**current_backend_kwargs) + builder_instance.build(embeddings, string_ids, index_path, **current_backend_kwargs) + leann_meta_path = index_dir / f"{index_name}.meta.json" + meta_data = { + "version": "1.1", + "backend_name": self.backend_name, + "embedding_model": self.embedding_model, + "dimensions": self.dimensions, + "backend_kwargs": self.backend_kwargs, + "embedding_mode": self.embedding_mode, + "passage_id_scheme": self.passage_id_scheme, + "passage_sources": [ + { + "type": "jsonl", + # Preserve existing relative file names (backward-compatible) + "path": passages_file.name, + "index_path": offset_file.name, + # Add optional redundant relative keys for remote build portability (non-breaking) + "path_relative": passages_file.name, + "index_path_relative": offset_file.name, + } + ], + } + + if self.embedding_options: + meta_data["embedding_options"] = self.embedding_options + + # Add storage status flags for HNSW backend + if self.backend_name == "hnsw": + is_compact = self.backend_kwargs.get("is_compact", True) + is_recompute = self.backend_kwargs.get("is_recompute", True) + meta_data["is_compact"] = is_compact + meta_data["is_pruned"] = bool(is_recompute) + + if self.prebuild_bm25: + self._build_bm25_fts5(index_dir, index_name) + meta_data["bm25_backend"] = "fts5" + meta_data["bm25_db"] = f"{index_name}.bm25.sqlite" + + with open(leann_meta_path, "w", encoding="utf-8") as f: + json.dump(meta_data, f, indent=2) + + def _build_bm25_fts5(self, index_dir: Path, index_name: str) -> None: + """Build a SQLite FTS5 BM25 index alongside the vector index. + + Queries via SQLite's bm25() function β€” memory-bounded at search time + (the term/posting data lives on disk, not in RAM). Replaces + BM25Scorer's full-corpus-in-memory model for paper-scale corpora. + """ + db_path = index_dir / f"{index_name}.bm25.sqlite" + index = Fts5BM25Index(str(db_path)) + index.fit(self.chunks) + index.close() + logger.info(f"Wrote BM25 FTS5 index to {db_path}") + + def build_index_from_arrays(self, index_path: str, ids: list, embeddings: np.ndarray): + """Build an index from pre-computed embedding arrays. + + This is the core method for building indexes from pre-computed embeddings. + Use this when embeddings are already in memory (e.g., from MLX, GPU computation, + or database queries). For pickle-file based workflows, use build_index_from_embeddings(). + + Args: + index_path: Path where the index will be saved + ids: List of document IDs (will be converted to strings) + embeddings: numpy array of shape (n_documents, embedding_dim) + + Raises: + ValueError: If ids and embeddings counts don't match, or dimension mismatch + """ + if len(ids) != embeddings.shape[0]: + raise ValueError( + f"Mismatch between number of IDs ({len(ids)}) and embeddings ({embeddings.shape[0]})" + ) + + # Validate/set dimensions + embedding_dim = embeddings.shape[1] + if self.dimensions is None: + self.dimensions = embedding_dim + elif self.dimensions != embedding_dim: + raise ValueError(f"Dimension mismatch: expected {self.dimensions}, got {embedding_dim}") + + logger.info( + f"Building index from precomputed embeddings: {len(ids)} items, {embedding_dim} dimensions" + ) + + # Ensure we have text data for each embedding + if len(self.chunks) != len(ids): + # If no text chunks provided, create placeholder text entries + if not self.chunks: + logger.info("No text chunks provided, creating placeholder entries...") + for id_val in ids: + self.add_text( + f"Document {id_val}", + metadata={"id": str(id_val), "from_embeddings": True}, + ) + else: + raise ValueError( + f"Number of text chunks ({len(self.chunks)}) doesn't match number of embeddings ({len(ids)})" + ) + + # Build file structure + path = Path(index_path) + index_dir = path.parent + index_name = path.name + index_dir.mkdir(parents=True, exist_ok=True) + passages_file = index_dir / f"{index_name}.passages.jsonl" + offset_file = index_dir / f"{index_name}.passages.idx" + + # Write passages and create offset map + offset_map = {} + with open(passages_file, "w", encoding="utf-8") as f: + for chunk in self.chunks: + offset = f.tell() + json.dump( + { + "id": chunk["id"], + "text": chunk["text"], + "metadata": chunk["metadata"], + }, + f, + ensure_ascii=False, + ) + f.write("\n") + offset_map[chunk["id"]] = offset + + with open(offset_file, "wb") as f: + pickle.dump(offset_map, f) + + # Build the vector index using precomputed embeddings + string_ids = [str(id_val) for id_val in ids] + # Persist ID map (order == embeddings order) + try: + idmap_file = ( + index_dir + / f"{index_name[: -len('.leann')] if index_name.endswith('.leann') else index_name}.ids.txt" + ) + with open(idmap_file, "w", encoding="utf-8") as f: + for sid in string_ids: + f.write(str(sid) + "\n") + except Exception: + pass + current_backend_kwargs = {**self.backend_kwargs, "dimensions": self.dimensions} + builder_instance = self.backend_factory.builder(**current_backend_kwargs) + builder_instance.build(embeddings, string_ids, index_path) + + # Create metadata file + leann_meta_path = index_dir / f"{index_name}.meta.json" + meta_data = { + "version": "1.1", + "backend_name": self.backend_name, + "embedding_model": self.embedding_model, + "dimensions": self.dimensions, + "backend_kwargs": self.backend_kwargs, + "embedding_mode": self.embedding_mode, + "passage_id_scheme": self.passage_id_scheme, + "passage_sources": [ + { + "type": "jsonl", + # Preserve existing relative file names (backward-compatible) + "path": passages_file.name, + "index_path": offset_file.name, + # Add optional redundant relative keys for remote build portability (non-breaking) + "path_relative": passages_file.name, + "index_path_relative": offset_file.name, + } + ], + "built_from_precomputed_embeddings": True, + } + + if self.embedding_options: + meta_data["embedding_options"] = self.embedding_options + + # Add storage status flags for HNSW backend + if self.backend_name == "hnsw": + is_compact = self.backend_kwargs.get("is_compact", True) + is_recompute = self.backend_kwargs.get("is_recompute", True) + meta_data["is_compact"] = is_compact + meta_data["is_pruned"] = bool(is_recompute) + + with open(leann_meta_path, "w", encoding="utf-8") as f: + json.dump(meta_data, f, indent=2) + + logger.info(f"Index built successfully from precomputed embeddings: {index_path}") + + def build_index_from_embeddings(self, index_path: str, embeddings_file: str): + """ + Build an index from pre-computed embeddings stored in a pickle file. + + Args: + index_path: Path where the index will be saved + embeddings_file: Path to pickle file containing (ids, embeddings) tuple + """ + # Load pre-computed embeddings + with open(embeddings_file, "rb") as f: + data = pickle.load(f) + + if not isinstance(data, tuple) or len(data) != 2: + raise ValueError( + f"Invalid embeddings file format. Expected tuple with 2 elements, got {type(data)}" + ) + + ids, embeddings = data + + if not isinstance(embeddings, np.ndarray): + raise ValueError(f"Expected embeddings to be numpy array, got {type(embeddings)}") + + self.build_index_from_arrays(index_path, ids, embeddings) + + @staticmethod + def _compact_passages( + passages_file: Path, offset_file: Path, offset_map: dict[str, int] + ) -> None: + """Rewrite passages.jsonl keeping only entries referenced by offset_map.""" + live_entries: list[str] = [] + for _pid, offset in sorted(offset_map.items(), key=lambda x: x[1]): + with open(passages_file, encoding="utf-8") as f: + f.seek(offset) + live_entries.append(f.readline()) + + tmp_file = passages_file.with_suffix(".jsonl.tmp") + new_offset_map: dict[str, int] = {} + with open(tmp_file, "w", encoding="utf-8") as f: + for line in live_entries: + data = json.loads(line) + new_offset_map[data["id"]] = f.tell() + f.write(line if line.endswith("\n") else line + "\n") + + tmp_file.replace(passages_file) + offset_map.clear() + offset_map.update(new_offset_map) + with open(offset_file, "wb") as f: + pickle.dump(offset_map, f) + + def update_index(self, index_path: str, remove_passage_ids: Optional[list[str]] = None) -> None: + """Append new passages and vectors to an existing index (HNSW or IVF). + For IVF, optional remove_passage_ids removes those ids first (e.g. from file-change API). + """ + if not self.chunks and not remove_passage_ids: + raise ValueError("No new chunks or passage ids to remove provided for update.") + + path = Path(index_path) + index_dir = path.parent + index_name = path.name + index_prefix = path.stem + + meta_path = index_dir / f"{index_name}.meta.json" + passages_file = index_dir / f"{index_name}.passages.jsonl" + offset_file = index_dir / f"{index_name}.passages.idx" + index_file = index_dir / f"{index_prefix}.index" + + if not meta_path.exists() or not passages_file.exists() or not offset_file.exists(): + raise FileNotFoundError("Index metadata or passage files are missing; cannot update.") + if not index_file.exists(): + raise FileNotFoundError(f"Index file not found: {index_file}") + + with open(meta_path, encoding="utf-8") as f: + meta = json.load(f) + backend_name = meta.get("backend_name") + if backend_name != self.backend_name: + raise ValueError( + f"Index was built with backend '{backend_name}', cannot update with '{self.backend_name}'." + ) + + with open(offset_file, "rb") as f: + offset_map: dict[str, int] = pickle.load(f) + existing_ids = set(offset_map.keys()) + + # IVF: optional delete (for reindex / file-change: remove then re-insert) + if remove_passage_ids and backend_name == "ivf": + try: + from leann_backend_ivf import remove_ids as ivf_remove_ids + + nremoved = ivf_remove_ids(str(path), remove_passage_ids) + if nremoved < len(remove_passage_ids): + logger.warning( + "IVF update_index: removed %d of %d requested passage IDs " + "(some may have been stale).", + nremoved, + len(remove_passage_ids), + ) + except ImportError: + raise RuntimeError( + "IVF backend required for remove_ids. Install leann-backend-ivf." + ) + for pid in remove_passage_ids: + offset_map.pop(pid, None) + existing_ids -= set(remove_passage_ids) + + # Compact passages.jsonl: rewrite keeping only entries in offset_map + self._compact_passages(passages_file, offset_file, offset_map) + + if not self.chunks: + meta["total_passages"] = len(offset_map) + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, indent=2) + self.chunks.clear() + return + + meta_backend_kwargs = meta.get("backend_kwargs", {}) + if backend_name == "hnsw": + index_is_compact = meta.get("is_compact", meta_backend_kwargs.get("is_compact", True)) + if index_is_compact: + raise ValueError( + "Compact HNSW indices do not support in-place updates. Rebuild required." + ) + + distance_metric = meta_backend_kwargs.get( + "distance_metric", self.backend_kwargs.get("distance_metric", "mips") + ).lower() + needs_recompute = bool( + meta.get("is_pruned") + or meta_backend_kwargs.get("is_recompute") + or self.backend_kwargs.get("is_recompute") + ) + + valid_chunks: list[dict[str, Any]] = [] + for chunk in self.chunks: + text = chunk.get("text", "") + if not isinstance(text, str) or not text.strip(): + continue + metadata = chunk.setdefault("metadata", {}) + passage_id = chunk.get("id") or metadata.get("id") + if passage_id and passage_id in existing_ids: + raise ValueError(f"Passage ID '{passage_id}' already exists in the index.") + valid_chunks.append(chunk) + + if not valid_chunks: + # Remove-only or file emptied: we may have already removed ids, just update meta + meta["total_passages"] = len(offset_map) + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, indent=2) + self.chunks.clear() + return + + texts_to_embed = [chunk["text"] for chunk in valid_chunks] + embeddings = compute_embeddings( + texts_to_embed, + self.embedding_model, + self.embedding_mode, + use_server=False, + is_build=True, + provider_options=self.embedding_options, + ) + + embedding_dim = embeddings.shape[1] + expected_dim = meta.get("dimensions") + if expected_dim is not None and expected_dim != embedding_dim: + raise ValueError( + f"Dimension mismatch during update: existing index uses {expected_dim}, got {embedding_dim}." + ) + + embeddings = np.ascontiguousarray(embeddings, dtype=np.float32) + if distance_metric == "cosine": + norms = np.linalg.norm(embeddings, axis=1, keepdims=True) + norms[norms == 0] = 1 + embeddings = embeddings / norms + + # IVF: add_vectors then append passages/offset (no ZMQ/server) + if backend_name == "ivf": + for i, chunk in enumerate(valid_chunks): + pid = chunk.get("id") or chunk.get("metadata", {}).get("id") + if not pid: + pid = str(len(offset_map) + i) + chunk.setdefault("metadata", {})["id"] = pid + chunk["id"] = pid + passage_ids = [c["id"] for c in valid_chunks] + try: + from leann_backend_ivf import add_vectors as ivf_add_vectors + + ivf_add_vectors(str(path), embeddings, passage_ids) + except ImportError: + raise RuntimeError("IVF backend required. Install leann-backend-ivf.") + rollback_passages_size = passages_file.stat().st_size if passages_file.exists() else 0 + offset_map_backup = offset_map.copy() + try: + with open(passages_file, "a", encoding="utf-8") as f: + for chunk in valid_chunks: + off = f.tell() + json.dump( + { + "id": chunk["id"], + "text": chunk["text"], + "metadata": chunk.get("metadata", {}), + }, + f, + ensure_ascii=False, + ) + f.write("\n") + offset_map[chunk["id"]] = off + with open(offset_file, "wb") as f: + pickle.dump(offset_map, f) + meta["total_passages"] = len(offset_map) + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, indent=2) + logger.info( + "Appended %d passages to IVF index '%s'. Total: %d", + len(valid_chunks), + index_path, + len(offset_map), + ) + except Exception: + if passages_file.exists(): + with open(passages_file, "rb+") as f: + f.truncate(rollback_passages_size) + offset_map = offset_map_backup + with open(offset_file, "wb") as f: + pickle.dump(offset_map, f) + raise + self.chunks.clear() + return + + # HNSW path below + from leann_backend_hnsw import faiss + + index = faiss.read_index(str(index_file)) + if hasattr(index, "is_recompute"): + index.is_recompute = needs_recompute + print(f"index.is_recompute: {index.is_recompute}") + if getattr(index, "storage", None) is None: + if index.metric_type == faiss.METRIC_INNER_PRODUCT: + storage_index = faiss.IndexFlatIP(index.d) + else: + storage_index = faiss.IndexFlatL2(index.d) + index.storage = storage_index + index.own_fields = True + # Faiss expects storage.ntotal to reflect the existing graph's + # population (even if the vectors themselves were pruned from disk + # for recompute mode). When we attach a fresh IndexFlat here its + # ntotal starts at zero, which later causes IndexHNSW::add to + # believe new "preset" levels were provided and trips the + # `n0 + n == levels.size()` assertion. Seed the temporary storage + # with the current ntotal so Faiss maintains the proper offset for + # incoming vectors. + try: + storage_index.ntotal = index.ntotal + except AttributeError: + # Older Faiss builds may not expose ntotal as a writable + # attribute; in that case we fall back to the default behaviour. + pass + if index.d != embedding_dim: + raise ValueError( + f"Existing index dimension ({index.d}) does not match new embeddings ({embedding_dim})." + ) + + passage_meta_mode = meta.get("embedding_mode", self.embedding_mode) + passage_provider_options = meta.get("embedding_options", self.embedding_options) + + base_id = index.ntotal + for offset, chunk in enumerate(valid_chunks): + new_id = str(base_id + offset) + chunk.setdefault("metadata", {})["id"] = new_id + chunk["id"] = new_id + + # Append passages/offsets before we attempt index.add so the ZMQ server + # can resolve newly assigned IDs during recompute. Keep rollback hooks + # so we can restore files if the update fails mid-way. + rollback_passages_size = passages_file.stat().st_size if passages_file.exists() else 0 + offset_map_backup = offset_map.copy() + + try: + with open(passages_file, "a", encoding="utf-8") as f: + for chunk in valid_chunks: + offset = f.tell() + json.dump( + { + "id": chunk["id"], + "text": chunk["text"], + "metadata": chunk.get("metadata", {}), + }, + f, + ensure_ascii=False, + ) + f.write("\n") + offset_map[chunk["id"]] = offset + + with open(offset_file, "wb") as f: + pickle.dump(offset_map, f) + + server_manager: Optional[EmbeddingServerManager] = None + server_started = False + requested_zmq_port = int(os.getenv("LEANN_UPDATE_ZMQ_PORT", "5557")) + + try: + if needs_recompute: + server_manager = EmbeddingServerManager( + backend_module_name="leann_backend_hnsw.hnsw_embedding_server" + ) + server_started, actual_port = server_manager.start_server( + port=requested_zmq_port, + model_name=self.embedding_model, + embedding_mode=passage_meta_mode, + passages_file=str(meta_path), + distance_metric=distance_metric, + use_daemon=False, + enable_warmup=False, + provider_options=passage_provider_options, + ) + if not server_started: + raise RuntimeError( + "Failed to start HNSW embedding server for recompute update." + ) + if actual_port != requested_zmq_port: + logger.warning( + "Embedding server started on port %s instead of requested %s. " + "Using reassigned port.", + actual_port, + requested_zmq_port, + ) + if hasattr(index.hnsw, "set_zmq_port"): + index.hnsw.set_zmq_port(actual_port) + elif hasattr(index, "set_zmq_port"): + index.set_zmq_port(actual_port) + + if needs_recompute: + for i in range(embeddings.shape[0]): + print(f"add {i} embeddings") + index.add(1, faiss.swig_ptr(embeddings[i : i + 1])) + else: + index.add(embeddings.shape[0], faiss.swig_ptr(embeddings)) + faiss.write_index(index, str(index_file)) + finally: + if server_started and server_manager is not None: + server_manager.stop_server() + + except Exception: + # Roll back appended passages/offset map to keep files consistent. + if passages_file.exists(): + with open(passages_file, "rb+") as f: + f.truncate(rollback_passages_size) + offset_map = offset_map_backup + with open(offset_file, "wb") as f: + pickle.dump(offset_map, f) + raise + + meta["total_passages"] = len(offset_map) + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, indent=2) + + logger.info( + "Appended %d passages to index '%s'. New total: %d", + len(valid_chunks), + index_path, + len(offset_map), + ) + + self.chunks.clear() + + if needs_recompute: + prune_hnsw_embeddings_inplace(str(index_file)) + + +class LeannSearcher: + def __init__( + self, + index_path: str, + enable_warmup: bool = True, + recompute_embeddings: bool = True, + use_daemon: bool = True, + daemon_ttl_seconds: int = 900, + **backend_kwargs, + ): + # Fix path resolution for Colab and other environments + if not Path(index_path).is_absolute(): + index_path = str(Path(index_path).resolve()) + + self.meta_path_str = f"{index_path}.meta.json" + if not Path(self.meta_path_str).exists(): + parent_dir = Path(index_path).parent + print( + f"Leann metadata file not found at {self.meta_path_str}, and you may need to rm -rf {parent_dir}" + ) + # highlight in red the filenotfound error + raise FileNotFoundError( + f"Leann metadata file not found at {self.meta_path_str}, \033[91m you may need to rm -rf {parent_dir}\033[0m" + ) + with open(self.meta_path_str, encoding="utf-8") as f: + self.meta_data = json.load(f) + backend_name = self.meta_data["backend_name"] + self.embedding_model = self.meta_data["embedding_model"] + # Support both old and new format + self.embedding_mode = self.meta_data.get("embedding_mode", "sentence-transformers") + self.embedding_options = self.meta_data.get("embedding_options", {}) + # Delegate portability handling to PassageManager + self.passage_manager = PassageManager( + self.meta_data.get("passage_sources", []), metadata_file_path=self.meta_path_str + ) + # Preserve backend name for conditional parameter forwarding + self.backend_name = backend_name + backend_factory = BACKEND_REGISTRY.get(backend_name) + if backend_factory is None: + raise ValueError(f"Backend '{backend_name}' not found.") + + # Global recompute flag for this searcher (explicit knob, default True) + self.recompute_embeddings: bool = bool(recompute_embeddings) + + # Warmup flag: keep using the existing enable_warmup parameter, + # but default it to True so cold-start happens earlier. + self._warmup: bool = bool(enable_warmup) + self._use_daemon: bool = bool(use_daemon) + self._daemon_ttl_seconds: int = int(daemon_ttl_seconds) + + final_kwargs = {**self.meta_data.get("backend_kwargs", {}), **backend_kwargs} + final_kwargs["enable_warmup"] = self._warmup + final_kwargs["use_daemon"] = self._use_daemon + final_kwargs["daemon_ttl_seconds"] = self._daemon_ttl_seconds + if self.embedding_options: + final_kwargs.setdefault("embedding_options", self.embedding_options) + self.backend_impl: LeannBackendSearcherInterface = backend_factory.searcher( + index_path, **final_kwargs + ) + self.bm25_scorer: Optional[BM25Index] = None + + # Surface the index's passage ID scheme so callers can introspect. + # Older indexes (pre-#330) don't record this field β€” they're sequential. + self.passage_id_scheme: str = self.meta_data.get( + "passage_id_scheme", PASSAGE_ID_SCHEME_SEQUENTIAL + ) + + # Optional query log path: set via LEANN_QUERY_LOG=. When set, each + # search appends a JSON line containing the query, embedding (if computed), + # top_k, and result IDs/scores. Useful for offline benchmark replay. + self._query_log_path: Optional[str] = os.environ.get("LEANN_QUERY_LOG") or None + + # Optional one-shot warmup at construction time to hide cold-start latency. + if self._warmup: + self.warmup() + + def warmup(self) -> None: + """Warm up embedding path so first user query is faster.""" + try: + _ = self.backend_impl.compute_query_embedding( + "__LEANN_WARMUP__", + use_server_if_available=self.recompute_embeddings, + ) + except Exception as exc: + logger.warning(f"Warmup embedding failed (ignored): {exc}") + + def search( + self, + query: str, + top_k: int = 5, + complexity: int = 64, + beam_width: int = 1, + prune_ratio: float = 0.0, + recompute_embeddings: Optional[bool] = None, + pruning_strategy: Literal["global", "local", "proportional"] = "global", + expected_zmq_port: int = 5557, + metadata_filters: Optional[dict[str, dict[str, Union[str, int, float, bool, list]]]] = None, + batch_size: int = 0, + use_grep: bool = False, + vector_weight: float = 1.0, + provider_options: Optional[dict[str, Any]] = None, + **kwargs, + ) -> list[SearchResult]: + """ + Search for nearest neighbors with optional metadata filtering. + + Args: + query: Text query to search for + top_k: Number of nearest neighbors to return + complexity: Search complexity/candidate list size, higher = more accurate but slower + beam_width: Number of parallel search paths/IO requests per iteration + prune_ratio: Ratio of neighbors to prune via approximate distance (0.0-1.0) + recompute_embeddings: (Deprecated) Per-call override for recompute mode. + Configure this at LeannSearcher(..., recompute_embeddings=...) instead. + pruning_strategy: Candidate selection strategy - "global" (default), "local", or "proportional" + expected_zmq_port: ZMQ port for embedding server communication + metadata_filters: Optional filters to apply to search results based on metadata. + Format: {"field_name": {"operator": value}} + Supported operators: + - Comparison: "==", "!=", "<", "<=", ">", ">=" + - Membership: "in", "not_in" + - String: "contains", "starts_with", "ends_with" + Example: {"chapter": {"<=": 5}, "tags": {"in": ["fiction", "drama"]}} + vector_weight: Weight of vector search in hybrid scoring (0.0-1.0). + 1.0 = pure vector search (default), 0.0 = pure BM25 keyword search, + anything in between linearly fuses the two. + **kwargs: Backend-specific parameters. Accepts a deprecated `gemma=` alias + for `vector_weight`; passing it emits a DeprecationWarning. + + Returns: + List of SearchResult objects with text, metadata, and similarity scores + """ + # Accept the legacy `gemma=` kwarg (typo of "gamma") as a deprecated alias + # for vector_weight. Pop before forwarding to backend so it doesn't leak. + if "gemma" in kwargs: + warnings.warn( + "search(gemma=...) is deprecated and will be removed in a future release; " + "use vector_weight= instead.", + DeprecationWarning, + stacklevel=2, + ) + vector_weight = kwargs.pop("gemma") + + # Handle grep search + if use_grep: + return self._grep_search(query, top_k) + + logger.info("πŸ” LeannSearcher.search() called:") + logger.info(f" Query: '{query}'") + logger.info(f" Top_k: {top_k}") + logger.info(f" Metadata filters: {metadata_filters}") + logger.info(f" Additional kwargs: {kwargs}") + + # Smart top_k detection and adjustment + # Use PassageManager length (sum of shard sizes) to avoid + # depending on a massive combined map + total_docs = len(self.passage_manager) + original_top_k = top_k + if top_k > total_docs: + top_k = total_docs + logger.warning( + f" ⚠️ Requested top_k ({original_top_k}) exceeds total documents ({total_docs})" + ) + logger.warning(f" βœ… Auto-adjusted top_k to {top_k} to match available documents") + + # Initialize so it's in scope for the query-log path even when only BM25 runs. + query_embedding: Optional[np.ndarray] = None + + # Handle pure keyword search + if vector_weight == 0.0: + start_time = time.time() + bm25_results = self._bm25_search(query, top_k) + # Convert BM25 results to the expected format + results = { + "labels": [[r.id for r in bm25_results]], + "distances": [[r.score for r in bm25_results]], + } + else: + # Perform vector search + zmq_port = None + + # Resolve effective recompute flag for this search. + if recompute_embeddings is not None: + logger.warning( + "LeannSearcher.search(..., recompute_embeddings=...) is deprecated and " + "will be removed in a future version. Configure recompute at " + "LeannSearcher(..., recompute_embeddings=...) instead." + ) + effective_recompute = bool(recompute_embeddings) + else: + effective_recompute = self.recompute_embeddings + + start_time = time.time() + if effective_recompute: + zmq_port = self.backend_impl._ensure_server_running( + self.meta_path_str, + port=expected_zmq_port, + enable_warmup=self._warmup, + use_daemon=self._use_daemon, + daemon_ttl_seconds=self._daemon_ttl_seconds, + **kwargs, + ) + del expected_zmq_port + zmq_time = time.time() - start_time + logger.info(f" Launching server time: {zmq_time} seconds") + + start_time = time.time() + + # Extract query template from stored embedding_options with fallback chain: + # 1. Check provider_options override (highest priority) + # 2. Check query_prompt_template (new format) + # 3. Check prompt_template (old format for backward compat) + # 4. None (no template) + query_template = None + if provider_options and "prompt_template" in provider_options: + query_template = provider_options["prompt_template"] + elif "query_prompt_template" in self.embedding_options: + query_template = self.embedding_options["query_prompt_template"] + elif "prompt_template" in self.embedding_options: + query_template = self.embedding_options["prompt_template"] + + query_embedding = self.backend_impl.compute_query_embedding( + query, + use_server_if_available=effective_recompute, + zmq_port=zmq_port, + query_template=query_template, + ) + logger.info(f" Generated embedding shape: {query_embedding.shape}") + embedding_time = time.time() - start_time + logger.info(f" Embedding time: {embedding_time} seconds") + + start_time = time.time() + backend_search_kwargs: dict[str, Any] = { + "complexity": complexity, + "beam_width": beam_width, + "prune_ratio": prune_ratio, + "recompute_embeddings": effective_recompute, + "pruning_strategy": pruning_strategy, + "zmq_port": zmq_port, + } + # Only HNSW supports batching; forward conditionally + if self.backend_name == "hnsw": + backend_search_kwargs["batch_size"] = batch_size + + # Merge any extra kwargs last + backend_search_kwargs.update(kwargs) + + results = self.backend_impl.search( + query_embedding, + top_k, + **backend_search_kwargs, + ) + + # Handle hybrid search + if 0.0 < vector_weight < 1.0: + logger.info(f" 🌟 Hybrid search enabled with vector_weight={vector_weight}") + bm25_weight = 1.0 - vector_weight + bm25_results = self._bm25_search(query, top_k) + hybrid_scores: dict[str, float] = {} + # Add vector search scores (weighted by vector_weight) + if "labels" in results and "distances" in results: + for doc_id, score in zip(results["labels"][0], results["distances"][0]): + hybrid_scores[doc_id] = vector_weight * score + # Add BM25 scores (weighted by bm25_weight) + for bm25_result in bm25_results: + doc_id = bm25_result.id + if doc_id in hybrid_scores: + hybrid_scores[doc_id] += bm25_weight * bm25_result.score + else: + hybrid_scores[doc_id] = bm25_weight * bm25_result.score + + sorted_hybrid = sorted(hybrid_scores.items(), key=lambda x: x[1], reverse=True)[:top_k] + results["labels"] = [[doc_id for doc_id, _ in sorted_hybrid]] + results["distances"] = [[score for _, score in sorted_hybrid]] + + logger.info( + f" Combined {len(hybrid_scores)} unique documents from vector and BM25 search" + ) + + search_time = time.time() - start_time + logger.info(f" Search time in search() LEANN searcher: {search_time} seconds") + logger.info(f" Backend returned: labels={len(results.get('labels', [[]])[0])} results") + + enriched_results = [] + if "labels" in results and "distances" in results: + logger.info(f" Processing {len(results['labels'][0])} passage IDs:") + # Python 3.9 does not support zip(strict=...); lengths are expected to match + for i, (string_id, dist) in enumerate( + zip(results["labels"][0], results["distances"][0]) + ): + try: + passage_data = self.passage_manager.get_passage(string_id) + enriched_results.append( + SearchResult( + id=string_id, + score=float(dist), + text=passage_data["text"], + metadata=passage_data.get("metadata", {}), + ) + ) + + # Color codes for better logging + GREEN = "\033[92m" + BLUE = "\033[94m" + YELLOW = "\033[93m" + RESET = "\033[0m" + + # Truncate text for display (first 100 chars) + display_text = passage_data["text"] + logger.info( + f" {GREEN}βœ“{RESET} {BLUE}[{i + 1:2d}]{RESET} {YELLOW}ID:{RESET} '{string_id}' {YELLOW}Score:{RESET} {dist:.4f} {YELLOW}Text:{RESET} {display_text}" + ) + except KeyError: + RED = "\033[91m" + RESET = "\033[0m" + logger.error( + f" {RED}βœ—{RESET} [{i + 1:2d}] ID: '{string_id}' -> {RED}ERROR: Passage not found!{RESET}" + ) + + # Apply metadata filters if specified + if metadata_filters: + logger.info(f" πŸ” Applying metadata filters: {metadata_filters}") + enriched_results = self.passage_manager.filter_search_results( + enriched_results, metadata_filters + ) + + # Define color codes outside the loop for final message + GREEN = "\033[92m" + RESET = "\033[0m" + logger.info(f" {GREEN}βœ“ Final enriched results: {len(enriched_results)} passages{RESET}") + + if self._query_log_path: + self._log_query(query, query_embedding, top_k, enriched_results) + + return enriched_results + + def _log_query( + self, + query: str, + query_embedding: Optional[np.ndarray], + top_k: int, + results: list[SearchResult], + ) -> None: + """Append a JSONL line to LEANN_QUERY_LOG for later benchmark replay.""" + path = self._query_log_path + if path is None: + return + entry: dict[str, Any] = { + "ts": time.time(), + "query": query, + "top_k": top_k, + "results": [{"id": r.id, "score": r.score} for r in results], + } + if query_embedding is not None: + entry["embedding"] = query_embedding.flatten().tolist() + try: + with open(path, "a", encoding="utf-8") as f: + json.dump(entry, f) + f.write("\n") + except Exception as exc: + logger.warning(f"Failed to append to query log {path}: {exc}") + + def _init_bm25(self) -> None: + """Initialize a BM25Index, preferring a build-time artifact when present.""" + backend = self.meta_data.get("bm25_backend") + meta_dir = Path(self.meta_path_str).parent + + if backend == "fts5": + db_name = self.meta_data.get("bm25_db") + if db_name: + db_path = meta_dir / db_name + if db_path.exists(): + self.bm25_scorer = Fts5BM25Index(str(db_path)) + logger.info(f"Using FTS5 BM25 index at {db_path}") + return + logger.warning( + f"meta.json says bm25_backend=fts5 but {db_path} is missing; " + f"falling back to fit-on-search." + ) + + # No FTS5 artifact: build one on the fly from passages. + db_path = meta_dir / (Path(self.meta_path_str).stem.replace(".meta", "") + ".bm25.sqlite") + index = Fts5BM25Index(str(db_path)) + passages = [] + for passage_file in self.passage_manager.passage_files.values(): + try: + with open(passage_file, encoding="utf-8") as f: + for line in f: + if line.strip(): + try: + passages.append(json.loads(line)) + except json.JSONDecodeError as exc: + logger.warning(f"Skipping malformed JSONL in {passage_file}: {exc}") + except FileNotFoundError: + logger.warning(f"Passage file missing: {passage_file}") + + if not passages: + logger.error( + "No passages found for on-demand BM25 index. " + "BM25/hybrid search will return empty results. " + "Re-run 'leann build' to regenerate passage files." + ) + return + + try: + index.fit(passages) + except (PermissionError, OSError) as exc: + logger.error( + f"Cannot write BM25 index to {db_path}: {exc}. " + f"Ensure the index directory is writable, or rebuild with prebuild_bm25=True." + ) + return + + self.bm25_scorer = index + logger.info(f"Built FTS5 BM25 index on-demand at {db_path}") + + def _bm25_search(self, query: str, top_k: int = 5) -> list[SearchResult]: + """Perform BM25 search on raw passages""" + if self.bm25_scorer is None: + self._init_bm25() + logger.info(" BM25 scorer initialized") + scorer = self.bm25_scorer + if scorer is None: + raise RuntimeError("BM25 scorer failed to initialize") + return scorer.search(query, top_k) + + def _find_jsonl_file(self) -> Optional[str]: + """Find the .jsonl file containing raw passages for grep search""" + index_path = Path(self.meta_path_str).parent + potential_files = [ + index_path / "documents.leann.passages.jsonl", + index_path.parent / "documents.leann.passages.jsonl", + ] + + for file_path in potential_files: + if file_path.exists(): + return str(file_path) + return None + + def _grep_search(self, query: str, top_k: int = 5) -> list[SearchResult]: + """Perform grep-based search on raw passages""" + jsonl_file = self._find_jsonl_file() + if not jsonl_file: + raise FileNotFoundError("No .jsonl passages file found for grep search") + + try: + cmd = ["grep", "-i", "-n", query, jsonl_file] + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + + if result.returncode == 1: + return [] + elif result.returncode != 0: + raise RuntimeError(f"Grep failed: {result.stderr}") + + matches = [] + for line in result.stdout.strip().split("\n"): + if not line: + continue + parts = line.split(":", 1) + if len(parts) != 2: + continue + + try: + data = json.loads(parts[1]) + text = data.get("text", "") + score = text.lower().count(query.lower()) + + matches.append( + SearchResult( + id=data.get("id", parts[0]), + text=text, + metadata=data.get("metadata", {}), + score=float(score), + ) + ) + except json.JSONDecodeError: + continue + + matches.sort(key=lambda x: x.score, reverse=True) + return matches[:top_k] + + except FileNotFoundError: + raise RuntimeError( + "grep command not found. Please install grep or use semantic search." + ) + + def cleanup(self): + """Explicitly cleanup embedding server and backend index resources. + This method should be called after you're done using the searcher, + especially in test environments or batch processing scenarios. + On Windows, this releases file handles held by native backends + (e.g., DiskANN memory-mapped index files). + """ + backend = getattr(self.backend_impl, "embedding_server_manager", None) + if backend is not None: + backend.stop_server() + close_fn = getattr(self.backend_impl, "close", None) + if close_fn is not None: + close_fn() + + # Enable automatic cleanup patterns + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + try: + self.cleanup() + except Exception: + pass + + def __del__(self): + try: + self.cleanup() + except Exception: + # Avoid noisy errors during interpreter shutdown + pass + + +class LeannChat: + def __init__( + self, + index_path: str, + llm_config: Optional[dict[str, Any]] = None, + enable_warmup: bool = False, + searcher: Optional[LeannSearcher] = None, + **kwargs, + ): + if searcher is None: + self.searcher = LeannSearcher(index_path, enable_warmup=enable_warmup, **kwargs) + self._owns_searcher = True + else: + self.searcher = searcher + self._owns_searcher = False + self.llm = get_llm(llm_config) + + def ask( + self, + question: str, + top_k: int = 5, + complexity: int = 64, + beam_width: int = 1, + prune_ratio: float = 0.0, + recompute_embeddings: bool = True, + pruning_strategy: Literal["global", "local", "proportional"] = "global", + llm_kwargs: Optional[dict[str, Any]] = None, + expected_zmq_port: int = 5557, + metadata_filters: Optional[dict[str, dict[str, Union[str, int, float, bool, list]]]] = None, + batch_size: int = 0, + use_grep: bool = False, + vector_weight: float = 1.0, + **search_kwargs, + ): + if "gemma" in search_kwargs: + warnings.warn( + "ask(gemma=...) is deprecated; use vector_weight= instead.", + DeprecationWarning, + stacklevel=2, + ) + vector_weight = search_kwargs.pop("gemma") + + if llm_kwargs is None: + llm_kwargs = {} + search_time = time.time() + results = self.searcher.search( + question, + top_k=top_k, + complexity=complexity, + beam_width=beam_width, + prune_ratio=prune_ratio, + recompute_embeddings=recompute_embeddings, + pruning_strategy=pruning_strategy, + expected_zmq_port=expected_zmq_port, + metadata_filters=metadata_filters, + use_grep=use_grep, + vector_weight=vector_weight, + batch_size=batch_size, + **search_kwargs, + ) + search_time = time.time() - search_time + logger.info(f" Search time: {search_time} seconds") + context = "\n\n".join([r.text for r in results]) + prompt = ( + "Here is some retrieved context that might help answer your question:\n\n" + f"{context}\n\n" + f"Question: {question}\n\n" + "Please provide the best answer you can based on this context and your knowledge." + ) + + logger.info("The context provided to the LLM is:") + logger.info(f"{'Relevance':<10} | {'Chunk id':<10} | {'Content':<60} | {'Source':<80}") + logger.info("-" * 150) + for r in results: + chunk_relevance = f"{r.score:.3f}" + chunk_id = r.id + chunk_content = r.text[:60] + chunk_source = r.metadata.get("source", "")[:80] + logger.info( + f"{chunk_relevance:<10} | {chunk_id:<10} | {chunk_content:<60} | {chunk_source:<80}" + ) + ask_time = time.time() + ans = self.llm.ask(prompt, **llm_kwargs) + ask_time = time.time() - ask_time + logger.info(f" Ask time: {ask_time} seconds") + return ans + + def start_interactive(self): + """Start interactive chat session.""" + session = create_api_session() + + def handle_query(user_input: str): + response = self.ask(user_input) + print(f"Leann: {response}") + + session.run_interactive_loop(handle_query) + + def cleanup(self): + """Explicitly cleanup embedding server resources. + + This method should be called after you're done using the chat interface, + especially in test environments or batch processing scenarios. + """ + # Only stop the embedding server if this LeannChat instance created the searcher. + # When a shared searcher is passed in, avoid shutting down the server to enable reuse. + if getattr(self, "_owns_searcher", False) and hasattr(self.searcher, "cleanup"): + self.searcher.cleanup() + + # Enable automatic cleanup patterns + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + try: + self.cleanup() + except Exception: + pass + + def __del__(self): + try: + self.cleanup() + except Exception: + pass diff --git a/packages/leann-core/src/leann/chat.py b/packages/leann-core/src/leann/chat.py new file mode 100644 index 0000000..a36383f --- /dev/null +++ b/packages/leann-core/src/leann/chat.py @@ -0,0 +1,1158 @@ +#!/usr/bin/env python3 +""" +This file contains the chat generation logic for the LEANN project, +supporting different backends like Ollama, Hugging Face Transformers, and a simulation mode. +""" + +import difflib +import logging +import os +from abc import ABC, abstractmethod +from typing import Any, Optional, cast + +from .settings import ( + resolve_anthropic_api_key, + resolve_anthropic_base_url, + resolve_minimax_api_key, + resolve_minimax_base_url, + resolve_novita_api_key, + resolve_novita_base_url, + resolve_ollama_host, + resolve_openai_api_key, + resolve_openai_base_url, +) + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def check_ollama_models(host: str) -> list[str]: + """Check available Ollama models and return a list""" + try: + import requests + + response = requests.get(f"{host}/api/tags", timeout=5) + if response.status_code == 200: + data = response.json() + return [model["name"] for model in data.get("models", [])] + return [] + except Exception: + return [] + + +def check_ollama_model_exists_remotely(model_name: str) -> tuple[bool, list[str]]: + """Check if a model exists in Ollama's remote library and return available tags + + Returns: + (model_exists, available_tags): bool and list of matching tags + """ + try: + import re + + import requests + + # Split model name and tag + if ":" in model_name: + base_model, requested_tag = model_name.split(":", 1) + else: + base_model, requested_tag = model_name, None + + # First check if base model exists in library + library_response = requests.get("https://ollama.com/library", timeout=8) + if library_response.status_code != 200: + return True, [] # Assume exists if can't check + + # Extract model names from library page + models_in_library = re.findall(r'href="/library/([^"]+)"', library_response.text) + + if base_model not in models_in_library: + return False, [] # Base model doesn't exist + + # If base model exists, get available tags + tags_response = requests.get(f"https://ollama.com/library/{base_model}/tags", timeout=8) + if tags_response.status_code != 200: + return True, [] # Base model exists but can't get tags + + # Extract tags for this model - be more specific to avoid HTML artifacts + tag_pattern = rf"{re.escape(base_model)}:[a-zA-Z0-9\.\-_]+" + raw_tags = re.findall(tag_pattern, tags_response.text) + + # Clean up tags - remove HTML artifacts and duplicates + available_tags = [] + seen = set() + for tag in raw_tags: + # Skip if it looks like HTML (contains < or >) + if "<" in tag or ">" in tag: + continue + if tag not in seen: + seen.add(tag) + available_tags.append(tag) + + # Check if exact model exists + if requested_tag is None: + # User just requested base model, suggest tags + return True, available_tags[:10] # Return up to 10 tags + else: + exact_match = model_name in available_tags + return exact_match, available_tags[:10] + + except Exception: + pass + + # If scraping fails, assume model might exist (don't block user) + return True, [] + + +def search_ollama_models_fuzzy(query: str, available_models: list[str]) -> list[str]: + """Use intelligent fuzzy search for Ollama models""" + if not available_models: + return [] + + query_lower = query.lower() + suggestions = [] + + # 1. Exact matches first + exact_matches = [m for m in available_models if query_lower == m.lower()] + suggestions.extend(exact_matches) + + # 2. Starts with query + starts_with = [ + m for m in available_models if m.lower().startswith(query_lower) and m not in suggestions + ] + suggestions.extend(starts_with) + + # 3. Contains query + contains = [m for m in available_models if query_lower in m.lower() and m not in suggestions] + suggestions.extend(contains) + + # 4. Base model name matching (remove version numbers) + def get_base_name(model_name: str) -> str: + """Extract base name without version (e.g., 'llama3:8b' -> 'llama3')""" + return model_name.split(":")[0].split("-")[0] + + query_base = get_base_name(query_lower) + base_matches = [ + m + for m in available_models + if get_base_name(m.lower()) == query_base and m not in suggestions + ] + suggestions.extend(base_matches) + + # 5. Family/variant matching + model_families = { + "llama": ["llama2", "llama3", "alpaca", "vicuna", "codellama"], + "qwen": ["qwen", "qwen2", "qwen3"], + "gemma": ["gemma", "gemma2"], + "phi": ["phi", "phi2", "phi3"], + "mistral": ["mistral", "mixtral", "openhermes"], + "dolphin": ["dolphin", "openchat"], + "deepseek": ["deepseek", "deepseek-coder"], + } + + query_family = None + for family, variants in model_families.items(): + if any(variant in query_lower for variant in variants): + query_family = family + break + + if query_family: + family_variants = model_families[query_family] + family_matches = [ + m + for m in available_models + if any(variant in m.lower() for variant in family_variants) and m not in suggestions + ] + suggestions.extend(family_matches) + + # 6. Use difflib for remaining fuzzy matches + remaining_models = [m for m in available_models if m not in suggestions] + difflib_matches = difflib.get_close_matches(query_lower, remaining_models, n=3, cutoff=0.4) + suggestions.extend(difflib_matches) + + return suggestions[:8] # Return top 8 suggestions + + +# Remove this function entirely - we don't need external API calls for Ollama + + +# Remove this too - no need for fallback + + +def suggest_similar_models(invalid_model: str, available_models: list[str]) -> list[str]: + """Use difflib to find similar model names""" + if not available_models: + return [] + + # Get close matches using fuzzy matching + suggestions = difflib.get_close_matches(invalid_model, available_models, n=3, cutoff=0.3) + return suggestions + + +def check_hf_model_exists(model_name: str) -> bool: + """Quick check if HuggingFace model exists without downloading""" + try: + from huggingface_hub import model_info + + model_info(model_name) + return True + except Exception: + return False + + +def get_popular_hf_models() -> list[str]: + """Return a list of popular HuggingFace models for suggestions""" + try: + from huggingface_hub import list_models + + # Get popular text-generation models, sorted by downloads + models = list_models( + filter="text-generation", + sort="downloads", + direction=-1, + limit=20, # Get top 20 most downloaded + ) + + # Extract model names and filter for chat/conversation models + model_names = [] + chat_keywords = ["chat", "instruct", "dialog", "conversation", "assistant"] + + for model in models: + model_name = model.id if hasattr(model, "id") else str(model) + # Prioritize models with chat-related keywords + if any(keyword in model_name.lower() for keyword in chat_keywords): + model_names.append(model_name) + elif len(model_names) < 10: # Fill up with other popular models + model_names.append(model_name) + + return model_names[:10] if model_names else _get_fallback_hf_models() + + except Exception: + # Fallback to static list if API call fails + return _get_fallback_hf_models() + + +def _get_fallback_hf_models() -> list[str]: + """Fallback list of popular HuggingFace models""" + return [ + "microsoft/DialoGPT-medium", + "microsoft/DialoGPT-large", + "facebook/blenderbot-400M-distill", + "microsoft/phi-2", + "deepseek-ai/deepseek-llm-7b-chat", + "microsoft/DialoGPT-small", + "facebook/blenderbot_small-90M", + "microsoft/phi-1_5", + "facebook/opt-350m", + "EleutherAI/gpt-neo-1.3B", + ] + + +def search_hf_models_fuzzy(query: str, limit: int = 10) -> list[str]: + """Use HuggingFace Hub's native fuzzy search for model suggestions""" + try: + from huggingface_hub import list_models + + # HF Hub's search is already fuzzy! It handles typos and partial matches + models = list_models( + search=query, + filter="text-generation", + sort="downloads", + direction=-1, + limit=limit, + ) + + model_names = [model.id if hasattr(model, "id") else str(model) for model in models] + + # If direct search doesn't return enough results, try some variations + if len(model_names) < 3: + # Try searching for partial matches or common variations + variations = [] + + # Extract base name (e.g., "gpt3" from "gpt-3.5") + base_query = query.lower().replace("-", "").replace(".", "").replace("_", "") + if base_query != query.lower(): + variations.append(base_query) + + # Try common model name patterns + if "gpt" in query.lower(): + variations.extend(["gpt2", "gpt-neo", "gpt-j", "dialoGPT"]) + elif "llama" in query.lower(): + variations.extend(["llama2", "alpaca", "vicuna"]) + elif "bert" in query.lower(): + variations.extend(["roberta", "distilbert", "albert"]) + + # Search with variations + for var in variations[:2]: # Limit to 2 variations to avoid too many API calls + try: + var_models = list_models( + search=var, + filter="text-generation", + sort="downloads", + direction=-1, + limit=3, + ) + var_names = [ + model.id if hasattr(model, "id") else str(model) for model in var_models + ] + model_names.extend(var_names) + except Exception: + continue + + # Remove duplicates while preserving order + seen = set() + unique_models = [] + for model in model_names: + if model not in seen: + seen.add(model) + unique_models.append(model) + + return unique_models[:limit] + + except Exception: + # If search fails, return empty list + return [] + + +def search_hf_models(query: str, limit: int = 10) -> list[str]: + """Simple search for HuggingFace models based on query (kept for backward compatibility)""" + return search_hf_models_fuzzy(query, limit) + + +def validate_model_and_suggest( + model_name: str, llm_type: str, host: Optional[str] = None +) -> Optional[str]: + """Validate model name and provide suggestions if invalid""" + if llm_type == "ollama": + resolved_host = resolve_ollama_host(host) + available_models = check_ollama_models(resolved_host) + if available_models and model_name not in available_models: + error_msg = f"Model '{model_name}' not found in your local Ollama installation." + + # Check if the model exists remotely and get available tags + model_exists_remotely, available_tags = check_ollama_model_exists_remotely(model_name) + + if model_exists_remotely and model_name in available_tags: + # Exact model exists remotely - suggest pulling it + error_msg += "\n\nTo install the requested model:\n" + error_msg += f" ollama pull {model_name}\n" + + # Show local alternatives + suggestions = search_ollama_models_fuzzy(model_name, available_models) + if suggestions: + error_msg += "\nOr use one of these similar installed models:\n" + for i, suggestion in enumerate(suggestions, 1): + error_msg += f" {i}. {suggestion}\n" + + elif model_exists_remotely and available_tags: + # Base model exists but requested tag doesn't - suggest correct tags + base_model = model_name.split(":")[0] + requested_tag = model_name.split(":", 1)[1] if ":" in model_name else None + + error_msg += ( + f"\n\nModel '{base_model}' exists, but tag '{requested_tag}' is not available." + ) + error_msg += f"\n\nAvailable {base_model} models you can install:\n" + for i, tag in enumerate(available_tags[:8], 1): + error_msg += f" {i}. ollama pull {tag}\n" + if len(available_tags) > 8: + error_msg += f" ... and {len(available_tags) - 8} more variants\n" + + # Also show local alternatives + suggestions = search_ollama_models_fuzzy(model_name, available_models) + if suggestions: + error_msg += "\nOr use one of these similar installed models:\n" + for i, suggestion in enumerate(suggestions, 1): + error_msg += f" {i}. {suggestion}\n" + + else: + # Model doesn't exist remotely - show fuzzy suggestions + suggestions = search_ollama_models_fuzzy(model_name, available_models) + error_msg += f"\n\nModel '{model_name}' was not found in Ollama's library." + + if suggestions: + error_msg += ( + "\n\nDid you mean one of these installed models?\n" + + "\nTry to use ollama pull to install the model you need\n" + ) + + for i, suggestion in enumerate(suggestions, 1): + error_msg += f" {i}. {suggestion}\n" + else: + error_msg += "\n\nYour installed models:\n" + for i, model in enumerate(available_models[:8], 1): + error_msg += f" {i}. {model}\n" + if len(available_models) > 8: + error_msg += f" ... and {len(available_models) - 8} more\n" + + error_msg += "\n\nCommands:" + error_msg += "\n ollama list # List installed models" + if model_exists_remotely and available_tags: + if model_name in available_tags: + error_msg += f"\n ollama pull {model_name} # Install requested model" + else: + error_msg += ( + f"\n ollama pull {available_tags[0]} # Install recommended variant" + ) + error_msg += "\n https://ollama.com/library # Browse available models" + return error_msg + + elif llm_type == "hf": + # For HF models, we can do a quick existence check + if not check_hf_model_exists(model_name): + # Use HF Hub's native fuzzy search directly + search_suggestions = search_hf_models_fuzzy(model_name, limit=8) + + error_msg = f"Model '{model_name}' not found on HuggingFace Hub." + if search_suggestions: + error_msg += "\n\nDid you mean one of these?\n" + for i, suggestion in enumerate(search_suggestions, 1): + error_msg += f" {i}. {suggestion}\n" + else: + # Fallback to popular models if search returns nothing + popular_models = get_popular_hf_models() + error_msg += "\n\nPopular chat models:\n" + for i, model in enumerate(popular_models[:5], 1): + error_msg += f" {i}. {model}\n" + + error_msg += f"\nSearch more: https://huggingface.co/models?search={model_name}&pipeline_tag=text-generation" + return error_msg + + return None # Model is valid or we can't check + + +class LLMInterface(ABC): + """Abstract base class for a generic Language Model (LLM) interface.""" + + @abstractmethod + def ask(self, prompt: str, **kwargs) -> str: + """ + Additional keyword arguments (kwargs) for advanced search customization. Example usage: + chat.ask( + "What is ANN?", + top_k=10, + complexity=64, + beam_width=8, + skip_search_reorder=True, + recompute_beighbor_embeddings=True, + dedup_node_dis=True, + prune_ratio=0.1, + batch_recompute=True, + global_pruning=True + ) + + Supported kwargs: + - complexity (int): Search complexity parameter (default: 32) + - beam_width (int): Beam width for search (default: 4) + - skip_search_reorder (bool): Skip search reorder step (default: False) + - recompute_beighbor_embeddings (bool): Enable ZMQ embedding server for neighbor recomputation (default: False) + - dedup_node_dis (bool): Deduplicate nodes by distance (default: False) + - prune_ratio (float): Pruning ratio for search (default: 0.0) + - batch_recompute (bool): Enable batch recomputation (default: False) + - global_pruning (bool): Enable global pruning (default: False) + """ + + # """ + # Sends a prompt to the LLM and returns the generated text. + + # Args: + # prompt: The input prompt for the LLM. + # **kwargs: Additional keyword arguments for the LLM backend. + + # Returns: + # The response string from the LLM. + # """ + pass + + +class OllamaChat(LLMInterface): + """LLM interface for Ollama models.""" + + def __init__(self, model: str = "llama3:8b", host: Optional[str] = None): + self.model = model + self.host = resolve_ollama_host(host) + logger.info(f"Initializing OllamaChat with model='{model}' and host='{self.host}'") + try: + import requests + + # Check if the Ollama server is responsive + if self.host: + requests.get(self.host) + + # Pre-check model availability with helpful suggestions + model_error = validate_model_and_suggest(model, "ollama", self.host) + if model_error: + raise ValueError(model_error) + + except ImportError: + raise ImportError( + "The 'requests' library is required for Ollama. Please install it with 'pip install requests'." + ) + except requests.exceptions.ConnectionError: + logger.error( + f"Could not connect to Ollama at {self.host}. Please ensure Ollama is running." + ) + raise ConnectionError( + f"Could not connect to Ollama at {self.host}. Please ensure Ollama is running." + ) + + def ask(self, prompt: str, **kwargs) -> str: + import json + + import requests + + full_url = f"{self.host}/api/generate" + + # Handle thinking budget for reasoning models + options = kwargs.copy() + thinking_budget = kwargs.get("thinking_budget") + if thinking_budget: + # Remove thinking_budget from options as it's not a standard Ollama option + options.pop("thinking_budget", None) + # Only apply reasoning parameters to models that support it + reasoning_supported_models = [ + "gpt-oss:20b", + "gpt-oss:120b", + "deepseek-r1", + "deepseek-coder", + ] + + if thinking_budget in ["low", "medium", "high"]: + if any(model in self.model.lower() for model in reasoning_supported_models): + options["reasoning"] = {"effort": thinking_budget, "exclude": False} + logger.info(f"Applied reasoning effort={thinking_budget} to model {self.model}") + else: + logger.warning( + f"Thinking budget '{thinking_budget}' requested but model '{self.model}' may not support reasoning parameters. Proceeding without reasoning." + ) + + payload = { + "model": self.model, + "prompt": prompt, + "stream": False, # Keep it simple for now + "options": options, + } + logger.debug(f"Sending request to Ollama: {payload}") + try: + logger.info("Sending request to Ollama and waiting for response...") + response = requests.post(full_url, data=json.dumps(payload)) + response.raise_for_status() + + # The response from Ollama can be a stream of JSON objects, handle this + response_parts = response.text.strip().split("\n") + full_response = "" + for part in response_parts: + if part: + json_part = json.loads(part) + full_response += json_part.get("response", "") + if json_part.get("done"): + break + return full_response + except requests.exceptions.RequestException as e: + logger.error(f"Error communicating with Ollama: {e}") + return f"Error: Could not get a response from Ollama. Details: {e}" + + +class HFChat(LLMInterface): + """LLM interface for local Hugging Face Transformers models with proper chat templates. + + Args: + model_name (str): Name of the Hugging Face model to load. + trust_remote_code (bool): Whether to allow execution of code from the model repository. + Defaults to False for security. Only enable for trusted models as this can pose + a security risk if the model repository is compromised. + """ + + def __init__( + self, model_name: str = "deepseek-ai/deepseek-llm-7b-chat", trust_remote_code: bool = False + ): + logger.info(f"Initializing HFChat with model='{model_name}'") + + # Security warning when trust_remote_code is enabled + if trust_remote_code: + logger.warning( + "SECURITY WARNING: trust_remote_code=True allows execution of arbitrary code from the model repository. " + "Only enable this for models from trusted sources. This creates a potential security risk if the model " + "repository is compromised." + ) + + self.trust_remote_code = trust_remote_code + + # Pre-check model availability with helpful suggestions + model_error = validate_model_and_suggest(model_name, "hf") + if model_error: + raise ValueError(model_error) + + try: + import torch + from transformers import AutoModelForCausalLM, AutoTokenizer + except ImportError: + raise ImportError( + "The 'transformers' and 'torch' libraries are required for Hugging Face models. Please install them with 'pip install transformers torch'." + ) + + # Auto-detect device (check environment variable first) + env_device = os.getenv("LEANN_LLM_DEVICE") + if env_device: + self.device = env_device + logger.info(f"Using device from LEANN_LLM_DEVICE: {self.device}") + elif torch.cuda.is_available(): + self.device = "cuda" + logger.info("CUDA is available. Using GPU.") + elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + self.device = "mps" + logger.info("MPS is available. Using Apple Silicon GPU.") + else: + self.device = "cpu" + logger.info("No GPU detected. Using CPU.") + + # Load tokenizer and model with timeout protection + try: + import signal + + def timeout_handler(signum, frame): + raise TimeoutError("Model download/loading timed out") + + # Set timeout for model loading (60 seconds) + old_handler = signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(60) + + try: + logger.info(f"Loading tokenizer for {model_name}...") + self.tokenizer = AutoTokenizer.from_pretrained( + model_name, trust_remote_code=self.trust_remote_code + ) + + logger.info(f"Loading model {model_name}...") + # Determine device_map based on device setting + if self.device == "cpu": + device_map = None + elif self.device.startswith("cuda:"): + # Specific GPU requested, use it exclusively + device_map = {"": self.device} + else: + # Auto mode: let HuggingFace distribute across available GPUs + device_map = "auto" + + self.model = AutoModelForCausalLM.from_pretrained( + model_name, + torch_dtype=torch.float16 if self.device != "cpu" else torch.float32, + device_map=device_map, + trust_remote_code=self.trust_remote_code, + ) + logger.info(f"Successfully loaded {model_name}") + finally: + signal.alarm(0) # Cancel the alarm + signal.signal(signal.SIGALRM, old_handler) # Restore old handler + + except TimeoutError: + logger.error(f"Model loading timed out for {model_name}") + raise RuntimeError( + f"Model loading timed out for {model_name}. Please check your internet connection or try a smaller model." + ) + except Exception as e: + logger.error(f"Failed to load model {model_name}: {e}") + raise + + # Move model to device if not using device_map + if self.device != "cpu" and "device_map" not in str(self.model): + self.model = self.model.to(self.device) + + # Set pad token if not present + if self.tokenizer.pad_token is None: + self.tokenizer.pad_token = self.tokenizer.eos_token + + def ask(self, prompt: str, **kwargs) -> str: + print("kwargs in HF: ", kwargs) + # Check if this is a Qwen model and add /no_think by default + is_qwen_model = "qwen" in self.model.config._name_or_path.lower() + + # For Qwen models, automatically add /no_think to the prompt + if is_qwen_model and "/no_think" not in prompt and "/think" not in prompt: + prompt = prompt + " /no_think" + + # Prepare chat template + messages = [{"role": "user", "content": prompt}] + + # Apply chat template if available + if hasattr(self.tokenizer, "apply_chat_template"): + try: + formatted_prompt = self.tokenizer.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True + ) + except Exception as e: + logger.warning(f"Chat template failed, using raw prompt: {e}") + formatted_prompt = prompt + else: + # Fallback for models without chat template + formatted_prompt = prompt + + # Tokenize input + inputs = self.tokenizer( + formatted_prompt, + return_tensors="pt", + padding=True, + truncation=True, + max_length=2048, + ) + + # Move inputs to device + if self.device != "cpu": + inputs = {k: v.to(self.device) for k, v in inputs.items()} + + # Set generation parameters + generation_config = { + "max_new_tokens": kwargs.get("max_tokens", kwargs.get("max_new_tokens", 512)), + "temperature": kwargs.get("temperature", 0.7), + "top_p": kwargs.get("top_p", 0.9), + "do_sample": kwargs.get("temperature", 0.7) > 0, + "pad_token_id": self.tokenizer.eos_token_id, + "eos_token_id": self.tokenizer.eos_token_id, + } + + # Handle temperature=0 for greedy decoding + if generation_config["temperature"] == 0.0: + generation_config["do_sample"] = False + generation_config.pop("temperature") + + logger.info(f"Generating with HuggingFace model, config: {generation_config}") + + # Generate + import torch + + with torch.no_grad(): + outputs = self.model.generate(**inputs, **generation_config) + + # Decode response + generated_tokens = outputs[0][inputs["input_ids"].shape[1] :] + response = self.tokenizer.decode(generated_tokens, skip_special_tokens=True) + + return response.strip() + + +class GeminiChat(LLMInterface): + """LLM interface for Google Gemini models.""" + + def __init__(self, model: str = "gemini-2.5-flash", api_key: Optional[str] = None): + self.model = model + self.api_key = api_key or os.getenv("GEMINI_API_KEY") + + if not self.api_key: + raise ValueError( + "Gemini API key is required. Set GEMINI_API_KEY environment variable or pass api_key parameter." + ) + + logger.info(f"Initializing Gemini Chat with model='{model}'") + + try: + import google.genai as genai + + self.client = genai.Client(api_key=self.api_key) + except ImportError: + raise ImportError( + "The 'google-genai' library is required for Gemini models. Please install it with 'uv pip install google-genai'." + ) + + def ask(self, prompt: str, **kwargs) -> str: + logger.info(f"Sending request to Gemini with model {self.model}") + + try: + from google.genai.types import GenerateContentConfig + + generation_config = GenerateContentConfig( + temperature=kwargs.get("temperature", 0.7), + max_output_tokens=kwargs.get("max_tokens", 1000), + ) + + # Handle top_p parameter + if "top_p" in kwargs: + generation_config.top_p = kwargs["top_p"] + + response = self.client.models.generate_content( + model=self.model, + contents=prompt, + config=generation_config, + ) + # Handle potential None response text + response_text = response.text + if response_text is None: + logger.warning("Gemini returned None response text") + return "" + return response_text.strip() + except Exception as e: + logger.error(f"Error communicating with Gemini: {e}") + return f"Error: Could not get a response from Gemini. Details: {e}" + + +class OpenAIChat(LLMInterface): + """LLM interface for OpenAI models.""" + + def __init__( + self, + model: str = "gpt-4o", + api_key: Optional[str] = None, + base_url: Optional[str] = None, + ): + self.model = model + self.base_url = resolve_openai_base_url(base_url) + self.api_key = resolve_openai_api_key(api_key) + + if not self.api_key: + raise ValueError( + "OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass api_key parameter." + ) + + logger.info( + "Initializing OpenAI Chat with model='%s' and base_url='%s'", + model, + self.base_url, + ) + + try: + import openai + + self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url) + except ImportError: + raise ImportError( + "The 'openai' library is required for OpenAI models. Please install it with 'pip install openai'." + ) + + def ask(self, prompt: str, **kwargs) -> str: + # Default parameters for OpenAI + params = { + "model": self.model, + "messages": [{"role": "user", "content": prompt}], + "temperature": kwargs.get("temperature", 0.7), + } + + # Handle max_tokens vs max_completion_tokens based on model + max_tokens = kwargs.get("max_tokens", 1000) + if "o3" in self.model or "o4" in self.model or "o1" in self.model: + # o-series models use max_completion_tokens + params["max_completion_tokens"] = max_tokens + params["temperature"] = 1.0 + else: + # Other models use max_tokens + params["max_tokens"] = max_tokens + + # Handle thinking budget for reasoning models + thinking_budget = kwargs.get("thinking_budget") + if thinking_budget and thinking_budget in ["low", "medium", "high"]: + # Check if this is an o-series model (partial match for model names) + o_series_models = ["o3", "o3-mini", "o4-mini", "o1", "o3-pro", "o3-deep-research"] + if any(model in self.model for model in o_series_models): + # Use the correct OpenAI reasoning parameter format + params["reasoning_effort"] = thinking_budget + logger.info(f"Applied reasoning_effort={thinking_budget} to model {self.model}") + else: + logger.warning( + f"Thinking budget '{thinking_budget}' requested but model '{self.model}' may not support reasoning parameters. Proceeding without reasoning." + ) + + # Add other kwargs (excluding thinking_budget as it's handled above) + for k, v in kwargs.items(): + if k not in ["max_tokens", "temperature", "thinking_budget"]: + params[k] = v + + logger.info(f"Sending request to OpenAI with model {self.model}") + + try: + response = cast(Any, self.client.chat.completions).create(**params) + print( + f"Total tokens = {response.usage.total_tokens}, prompt tokens = {response.usage.prompt_tokens}, completion tokens = {response.usage.completion_tokens}" + ) + if response.choices[0].finish_reason == "length": + print("The query is exceeding the maximum allowed number of tokens") + return response.choices[0].message.content.strip() + except Exception as e: + logger.error(f"Error communicating with OpenAI: {e}") + return f"Error: Could not get a response from OpenAI. Details: {e}" + + +class AnthropicChat(LLMInterface): + """LLM interface for Anthropic Claude models.""" + + def __init__( + self, + model: str = "claude-haiku-4-5", + api_key: Optional[str] = None, + base_url: Optional[str] = None, + ): + self.model = model + self.base_url = resolve_anthropic_base_url(base_url) + self.api_key = resolve_anthropic_api_key(api_key) + + if not self.api_key: + raise ValueError( + "Anthropic API key is required. Set ANTHROPIC_API_KEY environment variable or pass api_key parameter." + ) + + logger.info( + "Initializing Anthropic Chat with model='%s' and base_url='%s'", + model, + self.base_url, + ) + + try: + import anthropic + + # Allow custom Anthropic-compatible endpoints via base_url + self.client = anthropic.Anthropic( + api_key=self.api_key, + base_url=self.base_url, + ) + except ImportError: + raise ImportError( + "The 'anthropic' library is required for Anthropic models. Please install it with 'pip install anthropic'." + ) + + def ask(self, prompt: str, **kwargs) -> str: + logger.info(f"Sending request to Anthropic with model {self.model}") + + try: + # Anthropic API parameters + params = { + "model": self.model, + "max_tokens": kwargs.get("max_tokens", 1000), + "messages": [{"role": "user", "content": prompt}], + } + + # Add optional parameters + if "temperature" in kwargs: + params["temperature"] = kwargs["temperature"] + if "top_p" in kwargs: + params["top_p"] = kwargs["top_p"] + + response = self.client.messages.create(**params) + + # Extract text from response + response_text = response.content[0].text + + # Log token usage + print( + f"Total tokens = {response.usage.input_tokens + response.usage.output_tokens}, " + f"input tokens = {response.usage.input_tokens}, " + f"output tokens = {response.usage.output_tokens}" + ) + + if response.stop_reason == "max_tokens": + print("The query is exceeding the maximum allowed number of tokens") + + return response_text.strip() + except Exception as e: + logger.error(f"Error communicating with Anthropic: {e}") + return f"Error: Could not get a response from Anthropic. Details: {e}" + + +class MiniMaxChat(LLMInterface): + """LLM interface for MiniMax models via the OpenAI-compatible API. + + Supported models: + - MiniMax-M2.5 (default): Peak Performance. Ultimate Value. + - MiniMax-M2.5-highspeed: Same performance, faster and more agile. + + Both models support a 204,800-token context window. + """ + + def __init__( + self, + model: str = "MiniMax-M2.5", + api_key: Optional[str] = None, + base_url: Optional[str] = None, + ): + self.model = model + self.base_url = resolve_minimax_base_url(base_url) + self.api_key = resolve_minimax_api_key(api_key) + + if not self.api_key: + raise ValueError( + "MiniMax API key is required. Set MINIMAX_API_KEY environment variable or pass api_key parameter." + ) + + logger.info( + "Initializing MiniMax Chat with model='%s' and base_url='%s'", + model, + self.base_url, + ) + + try: + import openai + + self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url) + except ImportError: + raise ImportError( + "The 'openai' library is required for MiniMax models. Please install it with 'pip install openai'." + ) + + def ask(self, prompt: str, **kwargs) -> str: + # Default parameters for MiniMax (OpenAI-compatible) + params = { + "model": self.model, + "messages": [{"role": "user", "content": prompt}], + "temperature": kwargs.get("temperature", 0.7), + "max_tokens": kwargs.get("max_tokens", 1000), + } + + # Add optional parameters + if "top_p" in kwargs: + params["top_p"] = kwargs["top_p"] + + logger.info(f"Sending request to MiniMax with model {self.model}") + + try: + response = cast(Any, self.client.chat.completions).create(**params) + print( + f"Total tokens = {response.usage.total_tokens}, prompt tokens = {response.usage.prompt_tokens}, completion tokens = {response.usage.completion_tokens}" + ) + if response.choices[0].finish_reason == "length": + print("The query is exceeding the maximum allowed number of tokens") + return response.choices[0].message.content.strip() + except Exception as e: + logger.error(f"Error communicating with MiniMax: {e}") + return f"Error: Could not get a response from MiniMax. Details: {e}" + + +class NovitaChat(LLMInterface): + """LLM interface for Novita AI models via the OpenAI-compatible API. + + Supported models include: + - moonshotai/kimi-k2.5 (default): 262K context, MoE with function calling, + structured output, reasoning, and vision support. + - zai-org/glm-5: 202K context, MoE with function calling, structured output, + reasoning support. + - minimax/minimax-m2.5: 204K context, MoE with function calling, structured + output, reasoning support. + + See CLAUDE.md for the full model catalog with pricing and features. + """ + + def __init__( + self, + model: str = "moonshotai/kimi-k2.5", + api_key: Optional[str] = None, + base_url: Optional[str] = None, + ): + self.model = model + self.base_url = resolve_novita_base_url(base_url) + self.api_key = resolve_novita_api_key(api_key) + + if not self.api_key: + raise ValueError( + "Novita API key is required. Set NOVITA_API_KEY environment variable or pass api_key parameter." + ) + + logger.info( + "Initializing Novita Chat with model='%s' and base_url='%s'", + model, + self.base_url, + ) + + try: + import openai + + self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url) + except ImportError: + raise ImportError( + "The 'openai' library is required for Novita models. Please install it with 'pip install openai'." + ) + + def ask(self, prompt: str, **kwargs) -> str: + params = { + "model": self.model, + "messages": [{"role": "user", "content": prompt}], + "temperature": kwargs.get("temperature", 0.7), + "max_tokens": kwargs.get("max_tokens", 1000), + } + + if "top_p" in kwargs: + params["top_p"] = kwargs["top_p"] + + logger.info(f"Sending request to Novita with model {self.model}") + + try: + response = cast(Any, self.client.chat.completions).create(**params) + logger.info( + f"Total tokens = {response.usage.total_tokens}, prompt tokens = {response.usage.prompt_tokens}, completion tokens = {response.usage.completion_tokens}" + ) + if response.choices[0].finish_reason == "length": + logger.warning("The query is exceeding the maximum allowed number of tokens") + return response.choices[0].message.content.strip() + except Exception as e: + logger.error(f"Error communicating with Novita: {e}") + return f"Error: Could not get a response from Novita. Details: {e}" + + +class SimulatedChat(LLMInterface): + """A simple simulated chat for testing and development.""" + + def ask(self, prompt: str, **kwargs) -> str: + logger.info("Simulating LLM call...") + print("Prompt sent to LLM (simulation):", prompt[:500] + "...") + return "This is a simulated answer from the LLM based on the retrieved context." + + +def get_llm(llm_config: Optional[dict[str, Any]] = None) -> LLMInterface: + """ + Factory function to get an LLM interface based on configuration. + + Args: + llm_config: A dictionary specifying the LLM type and its parameters. + Example: {"type": "ollama", "model": "llama3"} + {"type": "hf", "model": "distilgpt2"} + None (for simulation mode) + + Returns: + An instance of an LLMInterface subclass. + """ + if llm_config is None: + llm_config = { + "type": "openai", + "model": "gpt-4o", + "api_key": os.getenv("OPENAI_API_KEY"), + } + + llm_type = llm_config.get("type", "openai") + model = llm_config.get("model") + + logger.info(f"Attempting to create LLM of type='{llm_type}' with model='{model}'") + + if llm_type == "ollama": + return OllamaChat( + model=model or "llama3:8b", + host=llm_config.get("host"), + ) + elif llm_type == "hf": + return HFChat( + model_name=model or "deepseek-ai/deepseek-llm-7b-chat", + trust_remote_code=llm_config.get("trust_remote_code", False), + ) + elif llm_type == "openai": + return OpenAIChat( + model=model or "gpt-4o", + api_key=llm_config.get("api_key"), + base_url=llm_config.get("base_url"), + ) + elif llm_type == "gemini": + return GeminiChat(model=model or "gemini-2.5-flash", api_key=llm_config.get("api_key")) + elif llm_type == "anthropic": + return AnthropicChat( + model=model or "claude-3-5-sonnet-20241022", + api_key=llm_config.get("api_key"), + base_url=llm_config.get("base_url"), + ) + elif llm_type == "minimax": + return MiniMaxChat( + model=model or "MiniMax-M2.5", + api_key=llm_config.get("api_key"), + base_url=llm_config.get("base_url"), + ) + elif llm_type == "novita": + return NovitaChat( + model=model or "moonshotai/kimi-k2.5", + api_key=llm_config.get("api_key"), + base_url=llm_config.get("base_url"), + ) + elif llm_type == "simulated": + return SimulatedChat() + else: + raise ValueError(f"Unknown LLM type: '{llm_type}'") diff --git a/packages/leann-core/src/leann/chunking_utils.py b/packages/leann-core/src/leann/chunking_utils.py new file mode 100644 index 0000000..003e362 --- /dev/null +++ b/packages/leann-core/src/leann/chunking_utils.py @@ -0,0 +1,513 @@ +""" +Enhanced chunking utilities with AST-aware code chunking support. +Packaged within leann-core so installed wheels can import it reliably. +""" + +import logging +from pathlib import Path +from typing import Any, Optional + +from llama_index.core.node_parser import SentenceSplitter + +logger = logging.getLogger(__name__) + +# Flag to ensure AST token warning only shown once per session +_ast_token_warning_shown = False + + +def estimate_token_count(text: str) -> int: + """ + Estimate token count for a text string. + Uses conservative estimation: ~4 characters per token for natural text, + ~1.2 tokens per character for code (worse tokenization). + + Args: + text: Input text to estimate tokens for + + Returns: + Estimated token count + """ + try: + import tiktoken + + encoder = tiktoken.get_encoding("cl100k_base") + return len(encoder.encode(text)) + except ImportError: + # Fallback: Conservative character-based estimation + # Assume worst case for code: 1.2 tokens per character + return int(len(text) * 1.2) + + +def calculate_safe_chunk_size( + model_token_limit: int, + overlap_size: int, + chunking_mode: str = "traditional", + safety_factor: float = 0.9, +) -> int: + """ + Calculate safe chunk size accounting for overlap and safety margin. + + Args: + model_token_limit: Maximum tokens supported by embedding model + overlap_size: Overlap units (tokens for traditional, chars for AST) + chunking_mode: "traditional" (tokens) or "ast" (characters) + safety_factor: Safety margin (0.9 = 10% safety margin) + + Returns: + Safe chunk size: tokens for traditional, characters for AST + """ + safe_limit = int(model_token_limit * safety_factor) + + if chunking_mode == "traditional": + # Traditional chunking uses tokens + # Max chunk = chunk_size + overlap, so chunk_size = limit - overlap + return max(1, safe_limit - overlap_size) + else: # AST chunking + # AST uses characters, need to convert + # Conservative estimate: 1.2 tokens per char for code + overlap_chars = int(overlap_size * 3) # ~3 chars per token for code + safe_chars = int(safe_limit / 1.2) + return max(1, safe_chars - overlap_chars) + + +def validate_chunk_token_limits(chunks: list[str], max_tokens: int = 512) -> tuple[list[str], int]: + """ + Validate that chunks don't exceed token limits and truncate if necessary. + + Args: + chunks: List of text chunks to validate + max_tokens: Maximum tokens allowed per chunk + + Returns: + Tuple of (validated_chunks, num_truncated) + """ + validated_chunks = [] + num_truncated = 0 + + for i, chunk in enumerate(chunks): + estimated_tokens = estimate_token_count(chunk) + + if estimated_tokens > max_tokens: + # Truncate chunk to fit token limit + try: + import tiktoken + + encoder = tiktoken.get_encoding("cl100k_base") + tokens = encoder.encode(chunk) + if len(tokens) > max_tokens: + truncated_tokens = tokens[:max_tokens] + truncated_chunk = encoder.decode(truncated_tokens) + validated_chunks.append(truncated_chunk) + num_truncated += 1 + logger.warning( + f"Truncated chunk {i} from {len(tokens)} to {max_tokens} tokens " + f"(from {len(chunk)} to {len(truncated_chunk)} characters)" + ) + else: + validated_chunks.append(chunk) + except ImportError: + # Fallback: Conservative character truncation + char_limit = int(max_tokens / 1.2) # Conservative for code + if len(chunk) > char_limit: + truncated_chunk = chunk[:char_limit] + validated_chunks.append(truncated_chunk) + num_truncated += 1 + logger.warning( + f"Truncated chunk {i} from {len(chunk)} to {char_limit} characters " + f"(conservative estimate for {max_tokens} tokens)" + ) + else: + validated_chunks.append(chunk) + else: + validated_chunks.append(chunk) + + if num_truncated > 0: + logger.warning(f"Truncated {num_truncated}/{len(chunks)} chunks to fit token limits") + + return validated_chunks, num_truncated + + +# Code file extensions supported by astchunk +CODE_EXTENSIONS = { + ".py": "python", + ".java": "java", + ".cs": "csharp", + ".ts": "typescript", + ".tsx": "typescript", + ".js": "typescript", + ".jsx": "typescript", +} + + +def detect_code_files(documents, code_extensions=None) -> tuple[list, list]: + """Separate documents into code files and regular text files.""" + if code_extensions is None: + code_extensions = CODE_EXTENSIONS + + code_docs = [] + text_docs = [] + + for doc in documents: + file_path = doc.metadata.get("file_path", "") or doc.metadata.get("file_name", "") + if file_path: + file_ext = Path(file_path).suffix.lower() + if file_ext in code_extensions: + doc.metadata["language"] = code_extensions[file_ext] + doc.metadata["is_code"] = True + code_docs.append(doc) + else: + doc.metadata["is_code"] = False + text_docs.append(doc) + else: + doc.metadata["is_code"] = False + text_docs.append(doc) + + logger.info(f"Detected {len(code_docs)} code files and {len(text_docs)} text files") + return code_docs, text_docs + + +def get_language_from_extension(file_path: str) -> Optional[str]: + """Return language string from a filename/extension using CODE_EXTENSIONS.""" + ext = Path(file_path).suffix.lower() + return CODE_EXTENSIONS.get(ext) + + +def _parse_ast_chunk_output(chunk: Any) -> tuple[str | None, dict[str, Any]]: + """Normalize the various chunk output formats from ASTChunkBuilder. + + astchunk can return objects (with a ``.text`` attr), plain strings, or + dicts (``{"content": ..., "metadata": ...}``). This helper returns a + uniform ``(text, metadata)`` pair regardless of the input shape. + """ + if hasattr(chunk, "text"): + return (str(chunk.text) if chunk.text else None, {}) + if isinstance(chunk, str): + return (chunk, {}) + if isinstance(chunk, dict): + meta = chunk.get("metadata", {}) + if "content" in chunk: + return (chunk["content"], meta) + if "text" in chunk: + return (chunk["text"], meta) + return (str(chunk), {}) + return (str(chunk), {}) + + +def create_ast_chunks( + documents, + max_chunk_size: int = 512, + chunk_overlap: int = 64, + metadata_template: str = "default", +) -> list[dict[str, Any]]: + """Create AST-aware chunks from code documents using astchunk. + + Falls back to traditional chunking if astchunk is unavailable. + + Returns: + List of dicts with {"text": str, "metadata": dict} + """ + try: + from astchunk import ASTChunkBuilder # optional dependency + except ImportError as e: + logger.error(f"astchunk not available: {e}") + logger.info("Falling back to traditional chunking for code files") + return create_traditional_chunks(documents, max_chunk_size, chunk_overlap) + + all_chunks = [] + for doc in documents: + language = doc.metadata.get("language") + if not language: + logger.warning("No language detected; falling back to traditional chunking") + all_chunks.extend(create_traditional_chunks([doc], max_chunk_size, chunk_overlap)) + continue + + try: + # Warn once if AST chunk size + overlap might exceed common token limits + # Note: Actual truncation happens at embedding time with dynamic model limits + global _ast_token_warning_shown + estimated_max_tokens = int( + (max_chunk_size + chunk_overlap) * 1.2 + ) # Conservative estimate + if estimated_max_tokens > 512 and not _ast_token_warning_shown: + logger.warning( + f"AST chunk size ({max_chunk_size}) + overlap ({chunk_overlap}) = {max_chunk_size + chunk_overlap} chars " + f"may exceed 512 token limit (~{estimated_max_tokens} tokens estimated). " + f"Consider reducing --ast-chunk-size to {int(400 / 1.2)} or --ast-chunk-overlap to {int(50 / 1.2)}. " + f"Note: Chunks will be auto-truncated at embedding time based on your model's actual token limit." + ) + _ast_token_warning_shown = True + + configs = { + "max_chunk_size": max_chunk_size, + "language": language, + "metadata_template": metadata_template, + "chunk_overlap": chunk_overlap if chunk_overlap > 0 else 0, + } + + repo_metadata = { + "file_path": doc.metadata.get("file_path", ""), + "file_name": doc.metadata.get("file_name", ""), + "source": doc.metadata.get("source", ""), + "creation_date": doc.metadata.get("creation_date", ""), + "last_modified_date": doc.metadata.get("last_modified_date", ""), + } + configs["repo_level_metadata"] = repo_metadata + + chunk_builder = ASTChunkBuilder(**configs) + code_content = doc.get_content() + if not code_content or not code_content.strip(): + logger.warning("Empty code content, skipping") + continue + + chunks = chunk_builder.chunkify(code_content) + for chunk in chunks: + chunk_text, astchunk_metadata = _parse_ast_chunk_output(chunk) + + if chunk_text and chunk_text.strip(): + # Extract document-level metadata + doc_metadata = { + "file_path": doc.metadata.get("file_path", ""), + "file_name": doc.metadata.get("file_name", ""), + "source": doc.metadata.get("source", ""), + } + if "creation_date" in doc.metadata: + doc_metadata["creation_date"] = doc.metadata["creation_date"] + if "last_modified_date" in doc.metadata: + doc_metadata["last_modified_date"] = doc.metadata["last_modified_date"] + + # Merge document metadata + astchunk metadata + combined_metadata = {**doc_metadata, **astchunk_metadata} + + all_chunks.append({"text": chunk_text.strip(), "metadata": combined_metadata}) + + logger.info( + f"Created {len(chunks)} AST chunks from {language} file: {doc.metadata.get('file_name', 'unknown')}" + ) + except Exception as e: + logger.warning(f"AST chunking failed for {language} file: {e}") + logger.info("Falling back to traditional chunking") + all_chunks.extend(create_traditional_chunks([doc], max_chunk_size, chunk_overlap)) + + return all_chunks + + +def create_traditional_chunks( + documents, + chunk_size: int = 256, + chunk_overlap: int = 128, + max_tokens_per_chunk: int | None = None, +) -> list[dict[str, Any]]: + """Create traditional text chunks using LlamaIndex SentenceSplitter. + + Args: + documents: LlamaIndex Document list. + chunk_size: Target chunk size in **characters** (approximate tokens). + chunk_overlap: Overlap between adjacent chunks in characters. + max_tokens_per_chunk: If set, auto-scale ``chunk_size`` so each + chunk stays within the model's token budget. Uses + ``calculate_safe_chunk_size`` with a 10 % safety margin. + Additionaly runs ``validate_chunk_token_limits`` post-chunk. + + Returns: + List of dicts with ``{"text": str, "metadata": dict}``. + """ + if chunk_size <= 0: + logger.warning(f"Invalid chunk_size={chunk_size}, using default value of 256") + chunk_size = 256 + if chunk_overlap < 0: + chunk_overlap = 0 + + # ── Token-aware auto-scaling ────────────────────────────────────── + if max_tokens_per_chunk and max_tokens_per_chunk > 0: + chunk_size = calculate_safe_chunk_size( + max_tokens_per_chunk, chunk_overlap, chunking_mode="traditional" + ) + logger.info( + "Token-aware chunking: model limit=%d tokens β†’ safe chunk_size=%d chars " + "(overlap=%d, safety=0.9)", + max_tokens_per_chunk, + chunk_size, + chunk_overlap, + ) + + # Revalidate after scaling + if chunk_overlap >= chunk_size: + old_overlap = chunk_overlap + chunk_overlap = max(0, chunk_size // 2) + logger.warning( + "Token-aware scaling reduced chunk_size below chunk_overlap " + "(%d β†’ %d); overlap reduced %d β†’ %d", + chunk_size, + old_overlap, + old_overlap, + chunk_overlap, + ) + + node_parser = SentenceSplitter( + chunk_size=chunk_size, + chunk_overlap=chunk_overlap, + separator=" ", + paragraph_separator="\n\n", + ) + + result = [] + for doc in documents: + doc_metadata = dict(doc.metadata) if doc.metadata else {} + doc_metadata.setdefault("file_path", "") + doc_metadata.setdefault("file_name", "") + doc_metadata.setdefault("source", "") + + try: + nodes = node_parser.get_nodes_from_documents([doc]) + if nodes: + for node in nodes: + result.append({"text": node.get_content(), "metadata": doc_metadata}) + except Exception as e: + logger.error(f"Traditional chunking failed for document: {e}") + content = doc.get_content() + if content and content.strip(): + result.append({"text": content.strip(), "metadata": doc_metadata}) + + # ── Post-chunk validation ───────────────────────────────────────── + if max_tokens_per_chunk and max_tokens_per_chunk > 0 and result: + _texts = [c["text"] for c in result] + _validated, _n = validate_chunk_token_limits(_texts, max_tokens_per_chunk) + if _n > 0: + logger.warning( + "Token-aware chunking: %d/%d chunks truncated to fit %d-token limit", + _n, + len(result), + max_tokens_per_chunk, + ) + for i, chunk_dict in enumerate(result): + if i < len(_validated): + chunk_dict["text"] = _validated[i] + + return result + + +def _traditional_chunks_as_dicts( + documents, + chunk_size: int = 256, + chunk_overlap: int = 128, + max_tokens_per_chunk: int | None = None, +) -> list[dict[str, Any]]: + """Backward-compatible alias for create_traditional_chunks.""" + return create_traditional_chunks( + documents, + chunk_size, + chunk_overlap, + max_tokens_per_chunk=max_tokens_per_chunk, + ) + + +def create_text_chunks( + documents, + chunk_size: int = 256, + chunk_overlap: int = 128, + use_ast_chunking: bool = False, + ast_chunk_size: int = 512, + ast_chunk_overlap: int = 64, + code_file_extensions: Optional[list[str]] = None, + ast_fallback_traditional: bool = True, + max_tokens_per_chunk: int | None = None, +) -> list[dict[str, Any]]: + """Create text chunks from documents with optional AST support for code files. + + Args: + documents: LlamaIndex Document list. + chunk_size: Characters per traditional chunk. + chunk_overlap: Character overlap between traditional chunks. + max_tokens_per_chunk: If set, auto-scale chunk_size to keep each + chunk within the embedding model's token budget. Also runs + post-chunk validation and truncation when necessary. + + Returns: + List of dicts with ``{"text": str, "metadata": dict}``. + """ + if not documents: + logger.warning("No documents provided for chunking") + return [] + + local_code_extensions = CODE_EXTENSIONS.copy() + if code_file_extensions: + ext_mapping = { + ".py": "python", + ".java": "java", + ".cs": "c_sharp", + ".ts": "typescript", + ".tsx": "typescript", + } + for ext in code_file_extensions: + if ext.lower() not in local_code_extensions: + if ext.lower() in ext_mapping: + local_code_extensions[ext.lower()] = ext_mapping[ext.lower()] + else: + logger.warning(f"Unsupported extension {ext}, will use traditional chunking") + + all_chunks = [] + if use_ast_chunking: + code_docs, text_docs = detect_code_files(documents, local_code_extensions) + if code_docs: + try: + # AST chunking: auto-scale if token limit given + ast_size = ast_chunk_size + if max_tokens_per_chunk and max_tokens_per_chunk > 0: + ast_size = calculate_safe_chunk_size( + max_tokens_per_chunk, ast_chunk_overlap, chunking_mode="ast" + ) + logger.info( + "Token-aware AST chunking: limit=%d β†’ safe ast_chunk_size=%d chars " + "(overlap=%d, safety=0.9)", + max_tokens_per_chunk, + ast_size, + ast_chunk_overlap, + ) + + ast_chunks = create_ast_chunks( + code_docs, max_chunk_size=ast_size, chunk_overlap=ast_chunk_overlap + ) + # Prepend line numbers to code chunks for navigation + for chunk in ast_chunks: + start_line = chunk.get("metadata", {}).get("start_line_no") + if start_line is not None: + lines = chunk["text"].split("\n") + end_line = start_line + len(lines) - 1 + w = len(str(end_line)) + chunk["text"] = "\n".join( + f"{start_line + i:>{w}}|{line}" for i, line in enumerate(lines) + ) + all_chunks.extend(ast_chunks) + except Exception as e: + logger.error(f"AST chunking failed: {e}") + if ast_fallback_traditional: + all_chunks.extend( + create_traditional_chunks( + code_docs, + chunk_size, + chunk_overlap, + max_tokens_per_chunk=max_tokens_per_chunk, + ) + ) + else: + raise + if text_docs: + all_chunks.extend( + create_traditional_chunks( + text_docs, + chunk_size, + chunk_overlap, + max_tokens_per_chunk=max_tokens_per_chunk, + ) + ) + else: + all_chunks = create_traditional_chunks( + documents, + chunk_size, + chunk_overlap, + max_tokens_per_chunk=max_tokens_per_chunk, + ) + + logger.info(f"Total chunks created: {len(all_chunks)}") + + return all_chunks diff --git a/packages/leann-core/src/leann/cli.py b/packages/leann-core/src/leann/cli.py new file mode 100644 index 0000000..405cf2c --- /dev/null +++ b/packages/leann-core/src/leann/cli.py @@ -0,0 +1,3763 @@ +import argparse +import asyncio +import contextlib +import hashlib +import io +import json +import os +import pickle +import sys +import time +import uuid +from pathlib import Path +from typing import Any, Optional, Union + +from llama_index.core import SimpleDirectoryReader +from llama_index.core.node_parser import SentenceSplitter +from tqdm import tqdm + +from .api import Fts5BM25Index, LeannBuilder, LeannChat, LeannSearcher +from .embedding_server_manager import EmbeddingServerManager +from .interactive_utils import create_cli_session +from .registry import register_project_directory +from .settings import ( + resolve_anthropic_base_url, + resolve_minimax_api_key, + resolve_minimax_base_url, + resolve_ollama_host, + resolve_openai_api_key, + resolve_openai_base_url, +) +from .sync import DEFAULT_INDEX_EXTENSIONS, FileSynchronizer, parse_include_extensions + + +def _default_embedding_model() -> str: + """Pick a sensible default embedding model based on platform. + + | Platform | Default model | + |------------|------------------------------------------------| + | NVIDIA GPU | BAAI/bge-base-en-v1.5 | + | macOS | sentence-transformers/all-MiniLM-L6-v2 | + | Other/CPU | sentence-transformers/all-MiniLM-L6-v2 | + """ + + try: + import torch + + if torch.cuda.is_available(): + return "BAAI/bge-base-en-v1.5" + except ImportError: + pass + + # macOS (MPS or CPU) and all other platforms: lightweight model + return "sentence-transformers/all-MiniLM-L6-v2" + + +def _normalize_path(path: str) -> str: + """Return absolute path string for consistent keys.""" + if not path: + return path + return str(Path(path).resolve()) + + +def _cleanup_path(path: Path) -> None: + if path.is_dir(): + import shutil + + shutil.rmtree(path) + elif path.exists(): + path.unlink() + + +def _existing_index_artifacts(index_dir: Path) -> bool: + return ( + (index_dir / "documents.leann.meta.json").exists() + and (index_dir / "documents.leann.passages.jsonl").exists() + and (index_dir / "documents.leann.passages.idx").exists() + ) + + +def _publish_rebuilt_index(staging_dir: Path, index_dir: Path) -> None: + """Publish a staged full rebuild, restoring the previous directory if publish fails.""" + backup_dir = index_dir.with_name(f".{index_dir.name}.backup-{uuid.uuid4().hex}") + live_moved = False + published = False + try: + if index_dir.exists(): + os.replace(index_dir, backup_dir) + live_moved = True + os.replace(staging_dir, index_dir) + published = True + except Exception: + if published: + _cleanup_path(index_dir) + if live_moved and backup_dir.exists(): + os.replace(backup_dir, index_dir) + raise + else: + if backup_dir.exists(): + _cleanup_path(backup_dir) + + +@contextlib.contextmanager +def suppress_cpp_output(suppress: bool = True): + """Context manager to suppress C++ stdout/stderr output from FAISS/HNSW + while preserving Python print() output. + + C++ native code writes directly to OS file descriptors (fd 1 / fd 2). + Python print() goes through sys.stdout / sys.stderr, which are Python + file objects. We redirect the OS fds to /dev/null (silencing C++) but + point sys.stdout / sys.stderr at copies of the *original* fds so that + Python output still reaches the terminal. + """ + if not suppress: + yield + return + + # 1. Duplicate the original OS file descriptors. + # May fail in no-console environments (e.g. pythonw, Windows GUI apps) + # where fd 1/2 are not valid β€” in that case, skip suppression. + try: + saved_stdout_fd = os.dup(1) + saved_stderr_fd = os.dup(2) + except OSError: + yield + return + + # 2. Build Python file objects that write to the saved (real) fds. + # closefd=False so closing these wrappers won't close the duped fds. + py_stdout = io.TextIOWrapper( + io.FileIO(saved_stdout_fd, mode="w", closefd=False), encoding=sys.stdout.encoding or "utf-8" + ) + py_stderr = io.TextIOWrapper( + io.FileIO(saved_stderr_fd, mode="w", closefd=False), encoding=sys.stderr.encoding or "utf-8" + ) + + old_sys_stdout = sys.stdout + old_sys_stderr = sys.stderr + + try: + # 3. Redirect OS-level fds to /dev/null β†’ silences C++ output + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, 1) + os.dup2(devnull, 2) + os.close(devnull) + + # 4. Point Python's sys.stdout/stderr at the real terminal + sys.stdout = py_stdout + sys.stderr = py_stderr + + yield + finally: + # 5. Restore everything + # Flush wrappers first (they still need the saved fds to be open) + py_stdout.flush() + py_stderr.flush() + + sys.stdout = old_sys_stdout + sys.stderr = old_sys_stderr + + os.dup2(saved_stdout_fd, 1) + os.dup2(saved_stderr_fd, 2) + os.close(saved_stdout_fd) + os.close(saved_stderr_fd) + + +def extract_pdf_text_with_pymupdf(file_path: str) -> str | None: + """Extract text from PDF using PyMuPDF for better quality.""" + try: + import fitz # PyMuPDF + + doc = fitz.open(file_path) + text = "" + for page in doc: + text += page.get_text() + doc.close() + return text + except ImportError: + # Fallback to default reader + return None + + +def extract_pdf_text_with_pdfplumber(file_path: str) -> str | None: + """Extract text from PDF using pdfplumber for better quality.""" + try: + import pdfplumber + + text = "" + with pdfplumber.open(file_path) as pdf: + for page in pdf.pages: + text += page.extract_text() or "" + return text + except ImportError: + # Fallback to default reader + return None + + +class LeannCLI: + def __init__(self): + # Always use project-local .leann directory (like .git) + self.indexes_dir = Path.cwd() / ".leann" / "indexes" + self.indexes_dir.mkdir(parents=True, exist_ok=True) + + # Default parser for documents + self.node_parser = SentenceSplitter( + chunk_size=256, chunk_overlap=128, separator=" ", paragraph_separator="\n\n" + ) + + # Code-optimized parser + self.code_parser = SentenceSplitter( + chunk_size=512, # Larger chunks for code context + chunk_overlap=50, # Less overlap to preserve function boundaries + separator="\n", # Split by lines for code + paragraph_separator="\n\n", # Preserve logical code blocks + ) + + def get_index_path(self, index_name: str) -> str: + index_dir = self.indexes_dir / index_name + return str(index_dir / "documents.leann") + + def index_exists(self, index_name: str) -> bool: + index_dir = self.indexes_dir / index_name + meta_file = index_dir / "documents.leann.meta.json" + return meta_file.exists() + + def create_parser(self) -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="leann", + description="The smallest vector index in the world. RAG Everything with LEANN!", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + leann build my-docs --docs ./documents # Build index from directory + leann build my-code --docs ./src ./tests ./config # Build index from multiple directories + leann build my-files --docs ./file1.py ./file2.txt ./docs/ # Build index from files and directories + leann build my-mixed --docs ./readme.md ./src/ ./config.json # Build index from mixed files/dirs + leann build my-ppts --docs ./ --file-types .pptx,.pdf # Index only PowerPoint and PDF files + leann search my-docs "query" # Search in my-docs index + leann ask my-docs "question" # Ask my-docs index + leann react my-docs "complex question" # Use ReAct agent for multiturn retrieval + leann index-browser chrome # Index Chrome browser history + leann index-email # Index Apple Mail + leann index-imessage # Index iMessage conversations + leann index-chatgpt --export-path ~/chatgpt-export.zip # Index ChatGPT export + leann list # List all stored indexes + leann remove my-docs # Remove an index (local first, then global) + """, + ) + + # Global verbosity options + verbosity_group = parser.add_mutually_exclusive_group() + verbosity_group.add_argument( + "-v", + "--verbose", + action="store_true", + help="Show detailed output including C++ backend logs from FAISS/HNSW", + ) + verbosity_group.add_argument( + "-q", + "--quiet", + action="store_true", + help="Suppress all non-essential output (default behavior)", + ) + + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Build command + build_parser = subparsers.add_parser("build", help="Build document index") + build_parser.add_argument( + "index_name", nargs="?", help="Index name (default: current directory name)" + ) + build_parser.add_argument( + "--docs", + type=str, + nargs="+", + default=["."], + help="Documents directories and/or files (default: current directory)", + ) + build_parser.add_argument( + "--backend-name", + type=str, + default="hnsw", + choices=["hnsw", "diskann", "ivf"], + help="Backend to use (default: hnsw)", + ) + _default_model = _default_embedding_model() + build_parser.add_argument( + "--embedding-model", + type=str, + default=_default_model, + help=f"Embedding model (default: {_default_model})", + ) + build_parser.add_argument( + "--embedding-mode", + type=str, + default="sentence-transformers", + choices=["sentence-transformers", "openai", "mlx", "ollama"], + help="Embedding backend mode (default: sentence-transformers)", + ) + build_parser.add_argument( + "--embedding-host", + type=str, + default=None, + help="Override Ollama-compatible embedding host", + ) + build_parser.add_argument( + "--embedding-api-base", + type=str, + default=None, + help="Base URL for OpenAI-compatible embedding services", + ) + build_parser.add_argument( + "--embedding-api-key", + type=str, + default=None, + help="API key for embedding service (defaults to OPENAI_API_KEY)", + ) + build_parser.add_argument( + "--embedding-prompt-template", + type=str, + default=None, + help="Prompt template to prepend to all texts for embedding (e.g., 'query: ' for search)", + ) + build_parser.add_argument( + "--embedding-batch-size", + type=int, + default=None, + help="Embedding batch size for sentence-transformers (overrides device defaults; also set LEANN_CUDA_BATCH_SIZE env)", + ) + build_parser.add_argument( + "--query-prompt-template", + type=str, + default=None, + help="Prompt template for queries (different from build template for task-specific models)", + ) + build_parser.add_argument( + "--force", + "-f", + action="store_true", + help="Force full rebuild of existing index (without this, build does incremental update: add new files only)", + ) + build_parser.add_argument( + "--graph-degree", type=int, default=32, help="Graph degree (default: 32)" + ) + build_parser.add_argument( + "--complexity", type=int, default=64, help="Build complexity (default: 64)" + ) + build_parser.add_argument("--num-threads", type=int, default=1) + build_parser.add_argument( + "--compact", + action=argparse.BooleanOptionalAction, + default=False, + help="Use compact (CSR) graph storage. Compact indices are read-only and cannot be updated incrementally. Default: false (allows incremental updates while still pruning embeddings for 97%% compression).", + ) + build_parser.add_argument( + "--recompute", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable recomputation (default: true)", + ) + build_parser.add_argument( + "--file-types", + type=str, + help="Comma-separated list of file extensions to include (e.g., '.txt,.pdf,.pptx'). If not specified, uses default supported types.", + ) + build_parser.add_argument( + "--include-hidden", + action=argparse.BooleanOptionalAction, + default=False, + help="Include hidden files and directories (paths starting with '.') during indexing (default: false)", + ) + build_parser.add_argument( + "--doc-chunk-size", + type=int, + default=256, + help="Document chunk size in TOKENS (default: 256). Final chunks may be larger due to overlap. For 512 token models: recommended 350 tokens (350 + 128 overlap = 478 max)", + ) + build_parser.add_argument( + "--doc-chunk-overlap", + type=int, + default=128, + help="Document chunk overlap in TOKENS (default: 128). Added to chunk size, not included in it", + ) + build_parser.add_argument( + "--code-chunk-size", + type=int, + default=512, + help="Code chunk size in TOKENS (default: 512). Final chunks may be larger due to overlap. For 512 token models: recommended 400 tokens (400 + 50 overlap = 450 max)", + ) + build_parser.add_argument( + "--code-chunk-overlap", + type=int, + default=50, + help="Code chunk overlap in TOKENS (default: 50). Added to chunk size, not included in it", + ) + build_parser.add_argument( + "--use-ast-chunking", + action="store_true", + help="Enable AST-aware chunking for code files (requires astchunk)", + ) + build_parser.add_argument( + "--ast-chunk-size", + type=int, + default=300, + help="AST chunk size in CHARACTERS (non-whitespace) (default: 300). Final chunks may be larger due to overlap and expansion. For 512 token models: recommended 300 chars (300 + 64 overlap ~= 480 tokens)", + ) + build_parser.add_argument( + "--ast-chunk-overlap", + type=int, + default=64, + help="AST chunk overlap in CHARACTERS (default: 64). Added to chunk size, not included in it. ~1.2 tokens per character for code", + ) + build_parser.add_argument( + "--ast-fallback-traditional", + action="store_true", + default=True, + help="Fall back to traditional chunking if AST chunking fails (default: True)", + ) + build_parser.add_argument( + "--id-scheme", + choices=["sequential", "content-hash"], + default="sequential", + help=( + "How passage IDs are assigned. 'sequential' (default) keys by insertion " + "order; 'content-hash' uses sha256(text)[:16], stable across file moves " + "and reorderings. See #329." + ), + ) + + # Watch command + watch_parser = subparsers.add_parser( + "watch", + help="Monitor source files and auto-rebuild index when changes are detected", + ) + watch_parser.add_argument("index_name", help="Index name") + watch_parser.add_argument( + "--interval", + type=int, + default=30, + help="Poll interval in seconds (default: 30)", + ) + watch_parser.add_argument( + "--once", + action="store_true", + help="Check once for changes and exit (do not loop)", + ) + watch_parser.add_argument( + "--dry-run", + action="store_true", + help="Report changes without rebuilding (original watch behavior)", + ) + + migrate_parser = subparsers.add_parser( + "migrate-ids", + help=( + "Rewrite an existing index's passage IDs to content-hash form. " + "Irreversible; back up the index first." + ), + ) + migrate_parser.add_argument("index_name", help="Index name") + migrate_parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would change without writing anything.", + ) + migrate_parser.add_argument( + "-y", + "--yes", + action="store_true", + help="Skip the interactive confirmation prompt.", + ) + + rebuild_parser = subparsers.add_parser( + "rebuild", + help="Rebuild an existing index using its stored config (delta by default; --force for full rebuild)", + ) + rebuild_parser.add_argument("index_name", help="Index name") + rebuild_parser.add_argument( + "-f", + "--force", + action="store_true", + help="Full rebuild from scratch instead of incremental delta", + ) + + # Search command + search_parser = subparsers.add_parser("search", help="Search documents") + search_parser.add_argument("index_name", help="Index name") + search_parser.add_argument("query", help="Search query") + search_parser.add_argument( + "--top-k", type=int, default=5, help="Number of results (default: 5)" + ) + search_parser.add_argument( + "--complexity", type=int, default=64, help="Search complexity (default: 64)" + ) + search_parser.add_argument("--beam-width", type=int, default=1) + search_parser.add_argument("--prune-ratio", type=float, default=0.0) + search_parser.add_argument( + "--recompute", + dest="recompute_embeddings", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable embedding recomputation (default: enabled). Should not do a `no-recompute` search in a `recompute` build.", + ) + search_parser.add_argument( + "--pruning-strategy", + choices=["global", "local", "proportional"], + default="global", + help="Pruning strategy (default: global)", + ) + search_parser.add_argument( + "--json", + action="store_true", + help="Output results as JSON array (machine-readable)", + ) + search_parser.add_argument( + "--non-interactive", + action="store_true", + help="Non-interactive mode: automatically select index without prompting", + ) + search_parser.add_argument( + "--show-metadata", + action="store_true", + help="Display file paths and metadata in search results", + ) + search_parser.add_argument( + "--embedding-prompt-template", + type=str, + default=None, + help="Prompt template to prepend to query for embedding (e.g., 'query: ' for search)", + ) + search_parser.add_argument( + "--daemon", + dest="use_daemon", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable cross-process daemon reuse for embedding server (default: enabled)", + ) + search_parser.add_argument( + "--daemon-ttl", + type=int, + default=900, + help="Daemon idle TTL in seconds (default: 900, 0 = never expire)", + ) + search_parser.add_argument( + "--warmup", + dest="enable_warmup", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable warmup when starting embedding server (default: enabled)", + ) + search_parser.add_argument( + "--metadata-filters", + type=str, + default=None, + help=( + "Filter results by metadata fields (JSON string). " + 'Format: \'{"field": {"operator": value}}\'. ' + "Operators: ==, !=, <, <=, >, >=, in, not_in, contains, starts_with, ends_with. " + 'Example: \'{"chapter": {"<=": 5}, "genre": {"==": "fiction"}}\'' + ), + ) + + # Warmup command + warmup_parser = subparsers.add_parser("warmup", help="Warm up an index embedding server") + warmup_parser.add_argument("index_name", help="Index name") + warmup_parser.add_argument( + "--daemon", + dest="use_daemon", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable daemon mode for warmup (default: enabled)", + ) + warmup_parser.add_argument( + "--daemon-ttl", + type=int, + default=900, + help="Daemon idle TTL in seconds (default: 900, 0 = never expire)", + ) + warmup_parser.add_argument( + "--warmup", + dest="enable_warmup", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable warmup request itself (default: enabled)", + ) + + # Daemon command + daemon_parser = subparsers.add_parser("daemon", help="Manage embedding daemons") + daemon_subparsers = daemon_parser.add_subparsers(dest="daemon_command") + + daemon_start = daemon_subparsers.add_parser("start", help="Start daemon for an index") + daemon_start.add_argument("index_name", help="Index name") + daemon_start.add_argument( + "--daemon-ttl", + type=int, + default=900, + help="Daemon idle TTL in seconds (default: 900, 0 = never expire)", + ) + daemon_start.add_argument( + "--warmup", + dest="enable_warmup", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable startup warmup (default: enabled)", + ) + + daemon_stop = daemon_subparsers.add_parser("stop", help="Stop daemon(s)") + daemon_stop.add_argument("index_name", nargs="?", help="Index name to stop") + daemon_stop.add_argument( + "--all", + action="store_true", + help="Stop all LEANN embedding daemons", + ) + + daemon_status = daemon_subparsers.add_parser("status", help="Show daemon status") + daemon_status.add_argument("index_name", nargs="?", help="Optional index name filter") + + # Ask command + ask_parser = subparsers.add_parser("ask", help="Ask questions") + ask_parser.add_argument("index_name", help="Index name") + ask_parser.add_argument( + "query", + nargs="?", + help="Question to ask (omit for prompt or when using --interactive)", + ) + ask_parser.add_argument( + "--llm", + type=str, + default="ollama", + choices=["simulated", "ollama", "hf", "openai", "anthropic", "minimax", "novita"], + help="LLM provider (default: ollama)", + ) + ask_parser.add_argument( + "--model", type=str, default="qwen3:8b", help="Model name (default: qwen3:8b)" + ) + ask_parser.add_argument( + "--host", + type=str, + default=None, + help="Override Ollama-compatible host (defaults to LEANN_OLLAMA_HOST/OLLAMA_HOST)", + ) + ask_parser.add_argument( + "--interactive", "-i", action="store_true", help="Interactive chat mode" + ) + ask_parser.add_argument( + "--top-k", type=int, default=20, help="Retrieval count (default: 20)" + ) + ask_parser.add_argument("--complexity", type=int, default=32) + ask_parser.add_argument("--beam-width", type=int, default=1) + ask_parser.add_argument("--prune-ratio", type=float, default=0.0) + ask_parser.add_argument( + "--recompute", + dest="recompute_embeddings", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable embedding recomputation during ask (default: enabled)", + ) + ask_parser.add_argument( + "--pruning-strategy", + choices=["global", "local", "proportional"], + default="global", + ) + ask_parser.add_argument( + "--thinking-budget", + type=str, + choices=["low", "medium", "high"], + default=None, + help="Thinking budget for reasoning models (low/medium/high). Supported by GPT-Oss:20b and other reasoning models.", + ) + ask_parser.add_argument( + "--api-base", + type=str, + default=None, + help="Base URL for OpenAI-compatible APIs (e.g., http://localhost:10000/v1)", + ) + ask_parser.add_argument( + "--api-key", + type=str, + default=None, + help="API key for cloud LLM providers (OpenAI, Anthropic)", + ) + ask_parser.add_argument( + "--metadata-filters", + type=str, + default=None, + help=( + "Filter retrieved chunks by metadata fields before answering (JSON string). " + 'Format: \'{"field": {"operator": value}}\'. ' + "Operators: ==, !=, <, <=, >, >=, in, not_in, contains, starts_with, ends_with. " + 'Example: \'{"chapter": {"<=": 5}, "genre": {"==": "fiction"}}\'' + ), + ) + + # React command (multiturn retrieval agent) + react_parser = subparsers.add_parser( + "react", help="Use ReAct agent for multiturn retrieval and reasoning" + ) + react_parser.add_argument("index_name", help="Index name") + react_parser.add_argument("query", help="Question to research") + react_parser.add_argument( + "--llm", + type=str, + default="ollama", + choices=["simulated", "ollama", "hf", "openai", "anthropic", "minimax", "novita"], + help="LLM provider (default: ollama)", + ) + react_parser.add_argument( + "--model", type=str, default="qwen3:8b", help="Model name (default: qwen3:8b)" + ) + react_parser.add_argument( + "--host", + type=str, + default=None, + help="Override Ollama-compatible host (defaults to LEANN_OLLAMA_HOST/OLLAMA_HOST)", + ) + react_parser.add_argument( + "--top-k", type=int, default=5, help="Number of results per search (default: 5)" + ) + react_parser.add_argument( + "--max-iterations", + type=int, + default=5, + help="Maximum number of search iterations (default: 5)", + ) + react_parser.add_argument( + "--api-base", + type=str, + default=None, + help="Base URL for OpenAI-compatible APIs (e.g., http://localhost:10000/v1)", + ) + react_parser.add_argument( + "--api-key", + type=str, + default=None, + help="API key for cloud LLM providers (OpenAI, Anthropic)", + ) + react_parser.add_argument( + "--serper-api-key", + type=str, + default=None, + help="Serper API key for web search (or set SERPER_API_KEY env var)", + ) + react_parser.add_argument( + "--jina-api-key", + type=str, + default=None, + help="Jina API key for page content fetching (or set JINA_API_KEY env var)", + ) + + # ── index-* commands: data source indexing ────────────────────── + + def _add_index_args(p, default_name): + """Add common embedding and index args to an index-* subparser.""" + p.add_argument( + "--index-name", + type=str, + default=default_name, + help=f"Index name (default: {default_name})", + ) + p.add_argument( + "--embedding-model", type=str, default="facebook/contriever", help="Embedding model" + ) + p.add_argument( + "--embedding-mode", + type=str, + default="sentence-transformers", + choices=["sentence-transformers", "openai", "mlx", "ollama"], + help="Embedding backend", + ) + p.add_argument("--embedding-host", type=str, default=None, help="Ollama embedding host") + p.add_argument( + "--embedding-api-base", + type=str, + default=None, + help="OpenAI-compatible embedding base URL", + ) + p.add_argument("--embedding-api-key", type=str, default=None, help="Embedding API key") + p.add_argument( + "--embedding-batch-size", + type=int, + default=None, + help="Embedding batch size for sentence-transformers", + ) + p.add_argument( + "--max-count", type=int, default=1000, help="Max items to index (default: 1000)" + ) + p.add_argument( + "--no-recompute", + action="store_true", + help="Disable embedding recomputation (stores full embeddings)", + ) + + idx_browser = subparsers.add_parser( + "index-browser", help="Index browser history (Chrome/Brave)" + ) + _add_index_args(idx_browser, "browser_history") + idx_browser.add_argument( + "browser", + nargs="?", + default="chrome", + choices=["chrome", "brave"], + help="Browser to index (default: chrome)", + ) + + idx_email = subparsers.add_parser("index-email", help="Index Apple Mail") + _add_index_args(idx_email, "email") + + idx_calendar = subparsers.add_parser("index-calendar", help="Index Apple Calendar events") + _add_index_args(idx_calendar, "calendar") + + idx_imessage = subparsers.add_parser("index-imessage", help="Index iMessage conversations") + _add_index_args(idx_imessage, "imessage") + + idx_wechat = subparsers.add_parser("index-wechat", help="Index WeChat chat history") + _add_index_args(idx_wechat, "wechat") + idx_wechat.add_argument( + "--export-dir", type=str, required=True, help="Path to WeChat JSON export directory" + ) + + idx_chatgpt = subparsers.add_parser("index-chatgpt", help="Index ChatGPT export") + _add_index_args(idx_chatgpt, "chatgpt") + idx_chatgpt.add_argument( + "--export-path", + type=str, + required=True, + help="Path to ChatGPT export (chat.html or .zip)", + ) + + idx_claude = subparsers.add_parser("index-claude", help="Index Claude export") + _add_index_args(idx_claude, "claude") + idx_claude.add_argument( + "--export-path", type=str, required=True, help="Path to Claude export (.json or .zip)" + ) + + # List command + subparsers.add_parser("list", help="List all indexes") + + # Remove command + remove_parser = subparsers.add_parser("remove", help="Remove an index") + remove_parser.add_argument("index_name", help="Index name to remove") + remove_parser.add_argument( + "--force", "-f", action="store_true", help="Force removal without confirmation" + ) + + # Serve command (HTTP API server) + serve_parser = subparsers.add_parser( + "serve", help="Start HTTP API server for LEANN vector DB" + ) + serve_parser.add_argument( + "--host", type=str, default=None, help="Host to bind to (default: 127.0.0.1)" + ) + serve_parser.add_argument( + "--port", type=int, default=None, help="Port to bind to (default: 8000)" + ) + + return parser + + def register_project_dir(self): + """Register current project directory in global registry""" + register_project_directory() + + def _build_gitignore_parser(self, docs_dir: str): + """Build gitignore parser using gitignore-parser library.""" + from gitignore_parser import parse_gitignore + + # Try to parse the root .gitignore + gitignore_path = Path(docs_dir) / ".gitignore" + + if gitignore_path.exists(): + try: + # gitignore-parser automatically handles all subdirectory .gitignore files! + matches = parse_gitignore(str(gitignore_path)) + print(f"πŸ“‹ Loaded .gitignore from {docs_dir} (includes all subdirectories)") + return matches + except Exception as e: + print(f"Warning: Could not parse .gitignore: {e}") + else: + print("πŸ“‹ No .gitignore found") + + # Fallback: basic pattern matching for essential files + essential_patterns = {".git", ".DS_Store", "__pycache__", "node_modules", ".venv", "venv"} + + def basic_matches(file_path): + path_parts = Path(file_path).parts + return any(part in essential_patterns for part in path_parts) + + return basic_matches + + def _should_exclude_file(self, file_path: Path, gitignore_matches) -> bool: + """Check if a file should be excluded using gitignore parser. + + Always match against absolute, posix-style paths for consistency with + gitignore_parser expectations. + """ + try: + absolute_path = file_path.resolve() + except Exception: + absolute_path = Path(str(file_path)) + return gitignore_matches(absolute_path.as_posix()) + + def _is_git_submodule(self, path: Path) -> bool: + """Check if a path is a git submodule.""" + try: + # Find the git repo root + current_dir = Path.cwd() + while current_dir != current_dir.parent: + if (current_dir / ".git").exists(): + gitmodules_path = current_dir / ".gitmodules" + if gitmodules_path.exists(): + # Read .gitmodules to check if this path is a submodule + gitmodules_content = gitmodules_path.read_text() + # Convert path to relative to git root + try: + relative_path = path.resolve().relative_to(current_dir) + # Check if this path appears in .gitmodules + return f"path = {relative_path}" in gitmodules_content + except ValueError: + # Path is not under git root + return False + break + current_dir = current_dir.parent + return False + except Exception: + # If anything goes wrong, assume it's not a submodule + return False + + def list_indexes(self): + # Get all project directories with .leann + global_registry = Path.home() / ".leann" / "projects.json" + all_projects = [] + + if global_registry.exists(): + try: + import json + + with open(global_registry) as f: + all_projects = json.load(f) + except Exception: + pass + + # Filter to only existing directories with .leann + valid_projects = [] + for project_dir in all_projects: + project_path = Path(project_dir) + if project_path.exists() and (project_path / ".leann" / "indexes").exists(): + valid_projects.append(project_path) + + # Add current project if it has .leann but not in registry + current_path = Path.cwd() + if (current_path / ".leann" / "indexes").exists() and current_path not in valid_projects: + valid_projects.append(current_path) + + # Separate current and other projects + other_projects = [] + + for project_path in valid_projects: + if project_path != current_path: + other_projects.append(project_path) + + print("πŸ“š LEANN Indexes") + print("=" * 50) + + total_indexes = 0 + current_indexes_count = 0 + + # Show current project first (most important) + print("\n🏠 Current Project") + print(f" {current_path}") + print(" " + "─" * 45) + + current_indexes = self._discover_indexes_in_project( + current_path, exclude_dirs=other_projects + ) + if current_indexes: + for idx in current_indexes: + total_indexes += 1 + current_indexes_count += 1 + type_icon = "πŸ“" if idx["type"] == "cli" else "πŸ“„" + print(f" {current_indexes_count}. {type_icon} {idx['name']} {idx['status']}") + if idx["size_mb"] > 0: + print(f" πŸ“¦ Size: {idx['size_mb']:.1f} MB") + else: + print(" πŸ“­ No indexes in current project") + + # Show other projects (reference information) + if other_projects: + print("\n\nπŸ—‚οΈ Other Projects") + print(" " + "─" * 45) + + for project_path in other_projects: + project_indexes = self._discover_indexes_in_project(project_path) + if not project_indexes: + continue + + print(f"\n πŸ“‚ {project_path.name}") + print(f" {project_path}") + + for idx in project_indexes: + total_indexes += 1 + type_icon = "πŸ“" if idx["type"] == "cli" else "πŸ“„" + print(f" β€’ {type_icon} {idx['name']} {idx['status']}") + if idx["size_mb"] > 0: + print(f" πŸ“¦ {idx['size_mb']:.1f} MB") + + # Summary and usage info + print("\n" + "=" * 50) + if total_indexes == 0: + print("πŸ’‘ Get started:") + print(" leann build my-docs --docs ./documents") + else: + # Count only projects that have at least one discoverable index + projects_count = 0 + for p in valid_projects: + if p == current_path: + discovered = self._discover_indexes_in_project(p, exclude_dirs=other_projects) + else: + discovered = self._discover_indexes_in_project(p) + if len(discovered) > 0: + projects_count += 1 + print(f"πŸ“Š Total: {total_indexes} indexes across {projects_count} projects") + + if current_indexes_count > 0: + print("\nπŸ’« Quick start (current project):") + # Get first index from current project for example + current_indexes_dir = current_path / ".leann" / "indexes" + if current_indexes_dir.exists(): + current_index_dirs = [d for d in current_indexes_dir.iterdir() if d.is_dir()] + if current_index_dirs: + example_name = current_index_dirs[0].name + print(f' leann search {example_name} "your query"') + print(f" leann ask {example_name} --interactive") + else: + print("\nπŸ’‘ Create your first index:") + print(" leann build my-docs --docs ./documents") + + def _discover_indexes_in_project( + self, project_path: Path, exclude_dirs: Optional[list[Path]] = None + ): + """Discover all indexes in a project directory (both CLI and apps formats) + + exclude_dirs: when provided, skip any APP-format index files that are + located under these directories. This prevents duplicates when the + current project is a parent directory of other registered projects. + """ + indexes = [] + exclude_dirs = exclude_dirs or [] + # normalize to resolved paths once for comparison + try: + exclude_dirs_resolved = [p.resolve() for p in exclude_dirs] + except Exception: + exclude_dirs_resolved = exclude_dirs + + # 1. CLI format: .leann/indexes/index_name/ + cli_indexes_dir = project_path / ".leann" / "indexes" + if cli_indexes_dir.exists(): + for index_dir in cli_indexes_dir.iterdir(): + if index_dir.is_dir(): + meta_file = index_dir / "documents.leann.meta.json" + status = "βœ…" if meta_file.exists() else "❌" + + size_mb = 0 + if meta_file.exists(): + try: + size_mb = sum( + f.stat().st_size for f in index_dir.iterdir() if f.is_file() + ) / (1024 * 1024) + except (OSError, PermissionError): + pass + + indexes.append( + { + "name": index_dir.name, + "type": "cli", + "status": status, + "size_mb": size_mb, + "path": index_dir, + } + ) + + # 2. Apps format: *.leann.meta.json files anywhere in the project + cli_indexes_dir = project_path / ".leann" / "indexes" + for meta_file in project_path.rglob("*.leann.meta.json"): + if meta_file.is_file(): + # Skip CLI-built indexes (which store meta under .leann/indexes//) + try: + if cli_indexes_dir.exists() and cli_indexes_dir in meta_file.parents: + continue + except Exception: + pass + # Skip meta files that live under excluded directories + try: + meta_parent_resolved = meta_file.parent.resolve() + if any( + meta_parent_resolved.is_relative_to(ex_dir) + for ex_dir in exclude_dirs_resolved + ): + continue + except Exception: + # best effort; if resolve or comparison fails, do not exclude + pass + # Use the parent directory name as the app index display name + display_name = meta_file.parent.name + # Extract file base used to store files + file_base = meta_file.name.replace(".leann.meta.json", "") + + # Apps indexes are considered complete if the .leann.meta.json file exists + status = "βœ…" + + # Calculate total size of all related files (use file base) + size_mb = 0 + try: + index_dir = meta_file.parent + for related_file in index_dir.glob(f"{file_base}.leann*"): + size_mb += related_file.stat().st_size / (1024 * 1024) + except (OSError, PermissionError): + pass + + indexes.append( + { + "name": display_name, + "type": "app", + "status": status, + "size_mb": size_mb, + "path": meta_file, + } + ) + + return indexes + + def remove_index(self, index_name: str, force: bool = False): + """Safely remove an index - always show all matches for transparency""" + + # Always do a comprehensive search for safety + print(f"πŸ” Searching for all indexes named '{index_name}'...") + all_matches = self._find_all_matching_indexes(index_name) + + if not all_matches: + print(f"❌ Index '{index_name}' not found in any project.") + return False + + if len(all_matches) == 1: + return self._remove_single_match(all_matches[0], index_name, force) + else: + return self._remove_from_multiple_matches(all_matches, index_name, force) + + def _find_all_matching_indexes(self, index_name: str): + """Find all indexes with the given name across all projects""" + matches = [] + + # Get all registered projects + global_registry = Path.home() / ".leann" / "projects.json" + all_projects = [] + + if global_registry.exists(): + try: + import json + + with open(global_registry) as f: + all_projects = json.load(f) + except Exception: + pass + + # Always include current project + current_path = Path.cwd() + if str(current_path) not in all_projects: + all_projects.append(str(current_path)) + + # Search across all projects + for project_dir in all_projects: + project_path = Path(project_dir) + if not project_path.exists(): + continue + + # 1) CLI-format index under .leann/indexes/ + index_dir = project_path / ".leann" / "indexes" / index_name + if index_dir.exists(): + is_current = project_path == current_path + matches.append( + { + "project_path": project_path, + "index_dir": index_dir, + "is_current": is_current, + "kind": "cli", + } + ) + + # 2) App-format indexes + # We support two ways of addressing apps: + # a) by the file base (e.g., `pdf_documents`) + # b) by the parent directory name (e.g., `new_txt`) + seen_app_meta = set() + + # 2a) by file base + for meta_file in project_path.rglob(f"{index_name}.leann.meta.json"): + if meta_file.is_file(): + # Skip CLI-built indexes' meta under .leann/indexes + try: + cli_indexes_dir = project_path / ".leann" / "indexes" + if cli_indexes_dir.exists() and cli_indexes_dir in meta_file.parents: + continue + except Exception: + pass + is_current = project_path == current_path + key = (str(project_path), str(meta_file)) + if key in seen_app_meta: + continue + seen_app_meta.add(key) + matches.append( + { + "project_path": project_path, + "files_dir": meta_file.parent, + "meta_file": meta_file, + "is_current": is_current, + "kind": "app", + "display_name": meta_file.parent.name, + "file_base": meta_file.name.replace(".leann.meta.json", ""), + } + ) + + # 2b) by parent directory name + for meta_file in project_path.rglob("*.leann.meta.json"): + if meta_file.is_file() and meta_file.parent.name == index_name: + # Skip CLI-built indexes' meta under .leann/indexes + try: + cli_indexes_dir = project_path / ".leann" / "indexes" + if cli_indexes_dir.exists() and cli_indexes_dir in meta_file.parents: + continue + except Exception: + pass + is_current = project_path == current_path + key = (str(project_path), str(meta_file)) + if key in seen_app_meta: + continue + seen_app_meta.add(key) + matches.append( + { + "project_path": project_path, + "files_dir": meta_file.parent, + "meta_file": meta_file, + "is_current": is_current, + "kind": "app", + "display_name": meta_file.parent.name, + "file_base": meta_file.name.replace(".leann.meta.json", ""), + } + ) + + # Sort: current project first, then by project name + matches.sort(key=lambda x: (not x["is_current"], x["project_path"].name)) + return matches + + def _remove_single_match(self, match, index_name: str, force: bool): + """Handle removal when only one match is found""" + project_path = match["project_path"] + is_current = match["is_current"] + kind = match.get("kind", "cli") + + if is_current: + location_info = "current project" + emoji = "🏠" + else: + location_info = f"other project '{project_path.name}'" + emoji = "πŸ“‚" + + print(f"βœ… Found 1 index named '{index_name}':") + print(f" {emoji} Location: {location_info}") + if kind == "cli": + print(f" πŸ“ Path: {project_path / '.leann' / 'indexes' / index_name}") + else: + print(f" πŸ“ Meta: {match['meta_file']}") + + if not force: + if not is_current: + print("\n⚠️ CROSS-PROJECT REMOVAL!") + print(" This will delete the index from another project.") + + response = input(f" ❓ Confirm removal from {location_info}? (y/N): ").strip().lower() + if response not in ["y", "yes"]: + print(" ❌ Removal cancelled.") + return False + + if kind == "cli": + return self._delete_index_directory( + match["index_dir"], + index_name, + project_path if not is_current else None, + is_app=False, + ) + else: + return self._delete_index_directory( + match["files_dir"], + match.get("display_name", index_name), + project_path if not is_current else None, + is_app=True, + meta_file=match.get("meta_file"), + app_file_base=match.get("file_base"), + ) + + def _remove_from_multiple_matches(self, matches, index_name: str, force: bool): + """Handle removal when multiple matches are found""" + + print(f"⚠️ Found {len(matches)} indexes named '{index_name}':") + print(" " + "─" * 50) + + for i, match in enumerate(matches, 1): + project_path = match["project_path"] + is_current = match["is_current"] + kind = match.get("kind", "cli") + + if is_current: + print(f" {i}. 🏠 Current project ({'CLI' if kind == 'cli' else 'APP'})") + else: + print(f" {i}. πŸ“‚ {project_path.name} ({'CLI' if kind == 'cli' else 'APP'})") + + # Show path details + if kind == "cli": + print(f" πŸ“ {project_path / '.leann' / 'indexes' / index_name}") + else: + print(f" πŸ“ {match['meta_file']}") + + # Show size info + try: + if kind == "cli": + size_mb = sum( + f.stat().st_size for f in match["index_dir"].iterdir() if f.is_file() + ) / (1024 * 1024) + else: + file_base = match.get("file_base") + size_mb = 0.0 + if file_base: + size_mb = sum( + f.stat().st_size + for f in match["files_dir"].glob(f"{file_base}.leann*") + if f.is_file() + ) / (1024 * 1024) + print(f" πŸ“¦ Size: {size_mb:.1f} MB") + except (OSError, PermissionError): + pass + + print(" " + "─" * 50) + + if force: + print(" ❌ Multiple matches found, but --force specified.") + print(" Please run without --force to choose which one to remove.") + return False + + try: + choice = input( + f" ❓ Which one to remove? (1-{len(matches)}, or 'c' to cancel): " + ).strip() + if choice.lower() == "c": + print(" ❌ Removal cancelled.") + return False + + choice_idx = int(choice) - 1 + if 0 <= choice_idx < len(matches): + selected_match = matches[choice_idx] + project_path = selected_match["project_path"] + is_current = selected_match["is_current"] + kind = selected_match.get("kind", "cli") + + location = "current project" if is_current else f"'{project_path.name}' project" + print(f" 🎯 Selected: Remove from {location}") + + # Final confirmation for safety + confirm = input( + f" ❓ FINAL CONFIRMATION - Type '{index_name}' to proceed: " + ).strip() + if confirm != index_name: + print(" ❌ Confirmation failed. Removal cancelled.") + return False + + if kind == "cli": + return self._delete_index_directory( + selected_match["index_dir"], + index_name, + project_path if not is_current else None, + is_app=False, + ) + else: + return self._delete_index_directory( + selected_match["files_dir"], + selected_match.get("display_name", index_name), + project_path if not is_current else None, + is_app=True, + meta_file=selected_match.get("meta_file"), + app_file_base=selected_match.get("file_base"), + ) + else: + print(" ❌ Invalid choice. Removal cancelled.") + return False + + except (ValueError, KeyboardInterrupt): + print("\n ❌ Invalid input. Removal cancelled.") + return False + + def _delete_index_directory( + self, + index_dir: Path, + index_display_name: str, + project_path: Optional[Path] = None, + is_app: bool = False, + meta_file: Optional[Path] = None, + app_file_base: Optional[str] = None, + ): + """Delete a CLI index directory or APP index files safely.""" + try: + if is_app: + removed = 0 + errors = 0 + # Delete only files that belong to this app index (based on file base) + pattern_base = app_file_base or "" + for f in index_dir.glob(f"{pattern_base}.leann*"): + try: + f.unlink() + removed += 1 + except Exception: + errors += 1 + # Best-effort: also remove the meta file if specified and still exists + if meta_file and meta_file.exists(): + try: + meta_file.unlink() + removed += 1 + except Exception: + errors += 1 + + if removed > 0 and errors == 0: + if project_path: + print( + f"βœ… App index '{index_display_name}' removed from {project_path.name}" + ) + else: + print(f"βœ… App index '{index_display_name}' removed successfully") + return True + elif removed > 0 and errors > 0: + print( + f"⚠️ App index '{index_display_name}' partially removed (some files couldn't be deleted)" + ) + return True + else: + print( + f"❌ No files found to remove for app index '{index_display_name}' in {index_dir}" + ) + return False + else: + import shutil + + shutil.rmtree(index_dir) + + if project_path: + print(f"βœ… Index '{index_display_name}' removed from {project_path.name}") + else: + print(f"βœ… Index '{index_display_name}' removed successfully") + return True + except Exception as e: + print(f"❌ Error removing index '{index_display_name}': {e}") + return False + + def load_documents( + self, + docs_paths: Union[str, list], + custom_file_types: Union[str, None] = None, + include_hidden: bool = False, + args: Optional[dict[str, Any]] = None, + ): + # Handle both single path (string) and multiple paths (list) for backward compatibility + if isinstance(docs_paths, str): + docs_paths = [docs_paths] + + # Separate files and directories + files = [] + directories = [] + for path in docs_paths: + path_obj = Path(path) + if path_obj.is_file(): + files.append(str(path_obj)) + elif path_obj.is_dir(): + # Check if this is a git submodule - if so, skip it + if self._is_git_submodule(path_obj): + print(f"⚠️ Skipping git submodule: {path}") + continue + directories.append(str(path_obj)) + else: + print(f"⚠️ Warning: Path '{path}' does not exist, skipping...") + continue + + # Print summary of what we're processing + total_items = len(files) + len(directories) + items_desc = [] + if files: + items_desc.append(f"{len(files)} file{'s' if len(files) > 1 else ''}") + if directories: + items_desc.append( + f"{len(directories)} director{'ies' if len(directories) > 1 else 'y'}" + ) + + print(f"Loading documents from {' and '.join(items_desc)} ({total_items} total):") + if files: + print(f" πŸ“„ Files: {', '.join([Path(f).name for f in files])}") + if directories: + print(f" πŸ“ Directories: {', '.join(directories)}") + + if custom_file_types: + print(f"Using custom file types: {custom_file_types}") + + all_documents = [] + + # Helper to detect hidden path components + def _path_has_hidden_segment(p: Path) -> bool: + return any(part.startswith(".") and part not in [".", ".."] for part in p.parts) + + # First, process individual files if any + if files: + print(f"\nπŸ”„ Processing {len(files)} individual file{'s' if len(files) > 1 else ''}...") + + # Load individual files using SimpleDirectoryReader with input_files + # Note: We skip gitignore filtering for explicitly specified files + try: + # Group files by their parent directory for efficient loading + from collections import defaultdict + + files_by_dir = defaultdict(list) + for file_path in files: + file_path_obj = Path(file_path) + if not include_hidden and _path_has_hidden_segment(file_path_obj): + print(f" ⚠️ Skipping hidden file: {file_path}") + continue + parent_dir = str(file_path_obj.parent) + files_by_dir[parent_dir].append(str(file_path_obj)) + + # Load files from each parent directory + for parent_dir, file_list in files_by_dir.items(): + print( + f" Loading {len(file_list)} file{'s' if len(file_list) > 1 else ''} from {parent_dir}" + ) + try: + file_docs = SimpleDirectoryReader( + parent_dir, + input_files=file_list, + # exclude_hidden only affects directory scans; input_files are explicit + filename_as_id=True, + ).load_data() + for doc in file_docs: + if not doc.metadata.get("source"): + doc.metadata["source"] = doc.metadata.get("file_path", "") + all_documents.extend(file_docs) + print( + f" βœ… Loaded {len(file_docs)} document{'s' if len(file_docs) > 1 else ''}" + ) + except Exception as e: + print(f" ❌ Warning: Could not load files from {parent_dir}: {e}") + + except Exception as e: + print(f"❌ Error processing individual files: {e}") + + # Define file extensions to process + if custom_file_types: + # Parse custom file types from comma-separated string + code_extensions = [ext.strip() for ext in custom_file_types.split(",") if ext.strip()] + # Ensure extensions start with a dot + code_extensions = [ext if ext.startswith(".") else f".{ext}" for ext in code_extensions] + else: + code_extensions = list(DEFAULT_INDEX_EXTENSIONS) + + # Process each directory + if directories: + print( + f"\nπŸ”„ Processing {len(directories)} director{'ies' if len(directories) > 1 else 'y'}..." + ) + + for docs_dir in directories: + print(f"Processing directory: {docs_dir}") + # Build gitignore parser for each directory + gitignore_matches = self._build_gitignore_parser(docs_dir) + + # Try to use better PDF parsers first, but only if PDFs are requested + documents = [] + # Use resolved absolute paths to avoid mismatches (symlinks, relative vs absolute) + docs_path = Path(docs_dir).resolve() + + # Check if we should process PDFs + should_process_pdfs = custom_file_types is None or ".pdf" in custom_file_types + + if should_process_pdfs: + for file_path in docs_path.rglob("*.pdf"): + # Check if file matches any exclude pattern + try: + # Ensure both paths are resolved before computing relativity + file_path_resolved = file_path.resolve() + # Determine directory scope using the non-resolved path to avoid + # misclassifying symlinked entries as outside the docs directory + relative_path = file_path.relative_to(docs_path) + if not include_hidden and _path_has_hidden_segment(relative_path): + continue + # Use absolute path for gitignore matching + if self._should_exclude_file(file_path_resolved, gitignore_matches): + continue + except ValueError: + # Skip files that can't be made relative to docs_path + print(f"⚠️ Skipping file outside directory scope: {file_path}") + continue + + print(f"Processing PDF: {file_path}") + + # Try PyMuPDF first (best quality) + text = extract_pdf_text_with_pymupdf(str(file_path)) + if text is None: + # Try pdfplumber + text = extract_pdf_text_with_pdfplumber(str(file_path)) + + if text: + # Create a simple document structure + from llama_index.core import Document + + doc = Document(text=text, metadata={"source": str(file_path)}) + documents.append(doc) + else: + # Fallback to default reader + print(f"Using default reader for {file_path}") + try: + default_docs = SimpleDirectoryReader( + str(file_path.parent), + exclude_hidden=not include_hidden, + filename_as_id=True, + required_exts=[file_path.suffix], + ).load_data() + documents.extend(default_docs) + except Exception as e: + print(f"Warning: Could not process {file_path}: {e}") + + # Load other file types with default reader + # Exclude PDFs from code_extensions if they were already processed separately + other_file_extensions = code_extensions + if should_process_pdfs and ".pdf" in code_extensions: + other_file_extensions = [ext for ext in code_extensions if ext != ".pdf"] + + try: + # Create a custom file filter function using our PathSpec + def file_filter( + file_path: str, docs_dir=docs_dir, gitignore_matches=gitignore_matches + ) -> bool: + """Return True if file should be included (not excluded)""" + try: + docs_path_obj = Path(docs_dir).resolve() + file_path_obj = Path(file_path).resolve() + # Use absolute path for gitignore matching + _ = file_path_obj.relative_to(docs_path_obj) # validate scope + return not self._should_exclude_file(file_path_obj, gitignore_matches) + except (ValueError, OSError): + return True # Include files that can't be processed + + # Only load other file types if there are extensions to process + if other_file_extensions: + other_docs = SimpleDirectoryReader( + docs_dir, + recursive=True, + encoding="utf-8", + required_exts=other_file_extensions, + file_extractor={}, # Use default extractors + exclude_hidden=not include_hidden, + filename_as_id=True, + ).load_data(show_progress=True) + else: + other_docs = [] + + # Filter documents after loading based on gitignore rules + filtered_docs = [] + for doc in other_docs: + file_path = doc.metadata.get("file_path", "") + if file_filter(file_path): + doc.metadata["source"] = file_path + filtered_docs.append(doc) + + documents.extend(filtered_docs) + except ValueError as e: + if "No files found" in str(e): + print(f"No additional files found for other supported types in {docs_dir}.") + else: + raise e + + all_documents.extend(documents) + print(f"Loaded {len(documents)} documents from {docs_dir}") + + documents = all_documents + + all_texts = [] + + # Define code file extensions for intelligent chunking + code_file_exts = { + ".py", + ".js", + ".ts", + ".jsx", + ".tsx", + ".java", + ".cpp", + ".c", + ".h", + ".hpp", + ".cs", + ".go", + ".rs", + ".rb", + ".php", + ".swift", + ".kt", + ".scala", + ".r", + ".sql", + ".sh", + ".bash", + ".zsh", + ".fish", + ".ps1", + ".bat", + ".json", + ".yaml", + ".yml", + ".xml", + ".toml", + ".ini", + ".cfg", + ".conf", + ".html", + ".css", + ".scss", + ".less", + ".vue", + ".svelte", + ".ipynb", + ".R", + ".jl", + } + + print("start chunking documents") + + # Check if AST chunking is requested + use_ast = getattr(args, "use_ast_chunking", False) + + if use_ast: + print("🧠 Using AST-aware chunking for code files") + try: + # Import enhanced chunking utilities from packaged module + from .chunking_utils import create_text_chunks + + # Use enhanced chunking with AST support + chunk_texts = create_text_chunks( + documents, + chunk_size=self.node_parser.chunk_size, + chunk_overlap=self.node_parser.chunk_overlap, + use_ast_chunking=True, + ast_chunk_size=getattr(args, "ast_chunk_size", 768), + ast_chunk_overlap=getattr(args, "ast_chunk_overlap", 96), + code_file_extensions=None, # Use defaults + ast_fallback_traditional=getattr(args, "ast_fallback_traditional", True), + ) + + # create_text_chunks now returns list[dict] with metadata preserved + all_texts.extend(chunk_texts) + + except ImportError as e: + print( + f"⚠️ AST chunking utilities not available in package ({e}), falling back to traditional chunking" + ) + use_ast = False + + if not use_ast: + # Use traditional chunking logic + for doc in tqdm(documents, desc="Chunking documents", unit="doc"): + # Check if this is a code file based on source path + source_path = doc.metadata.get("source", "") + file_path = doc.metadata.get("file_path", "") + is_code_file = any( + (source_path or file_path).endswith(ext) for ext in code_file_exts + ) + + # For code files, prepend line numbers so chunks carry them + if is_code_file: + from llama_index.core.schema import MediaResource + + original_text = doc.get_content() + lines = original_text.split("\n") + width = len(str(len(lines))) + numbered = "\n".join(f"{i + 1:>{width}}|{line}" for i, line in enumerate(lines)) + doc.text_resource = MediaResource(text=numbered) + + # Extract metadata to preserve with chunks + chunk_metadata = { + "file_path": file_path or source_path, + "file_name": doc.metadata.get("file_name", ""), + "source": source_path, + } + + # Add optional metadata if available + if "creation_date" in doc.metadata: + chunk_metadata["creation_date"] = doc.metadata["creation_date"] + if "last_modified_date" in doc.metadata: + chunk_metadata["last_modified_date"] = doc.metadata["last_modified_date"] + + # Use appropriate parser based on file type + parser = self.code_parser if is_code_file else self.node_parser + nodes = parser.get_nodes_from_documents([doc]) + + for node in nodes: + text = node.get_content() + # For code chunks, trim a partial first line left by overlap + # (a valid line starts with digits followed by '|') + if is_code_file and text and not text[0].isdigit(): + first_nl = text.find("\n") + if first_nl != -1: + text = text[first_nl + 1 :] + all_texts.append({"text": text, "metadata": chunk_metadata.copy()}) + + print(f"Loaded {len(documents)} documents, {len(all_texts)} chunks") + return all_texts + + def _parse_file_types(self, custom_file_types: Optional[str]) -> list[str]: + return parse_include_extensions(custom_file_types) + + def _sync_ignore_patterns(self, include_hidden: bool) -> Optional[list[str]]: + if include_hidden: + return None + return ["**/.*"] + + def _build_embedding_options(self, args) -> dict[str, Any]: + """Build embedding provider options dict from CLI args.""" + opts: dict[str, Any] = {} + if args.embedding_mode == "ollama": + opts["host"] = resolve_ollama_host(args.embedding_host) + elif args.embedding_mode == "openai": + opts["base_url"] = resolve_openai_base_url(args.embedding_api_base) + resolved_key = resolve_openai_api_key(args.embedding_api_key) + if resolved_key: + opts["api_key"] = resolved_key + if args.query_prompt_template: + if args.embedding_prompt_template: + opts["build_prompt_template"] = args.embedding_prompt_template + opts["query_prompt_template"] = args.query_prompt_template + elif args.embedding_prompt_template: + opts["prompt_template"] = args.embedding_prompt_template + batch_size = getattr(args, "embedding_batch_size", None) + if batch_size is not None: + opts["batch_size"] = batch_size + return opts + + def _build_config_from_args( + self, + args, + docs_paths: list[str], + *, + doc_chunk_size: Optional[int] = None, + doc_chunk_overlap: Optional[int] = None, + code_chunk_size: Optional[int] = None, + code_chunk_overlap: Optional[int] = None, + ) -> dict[str, Any]: + """Capture the CLI build settings needed to replay `leann build`.""" + embedding_options = self._build_embedding_options(args) + config: dict[str, Any] = { + "docs": [str(Path(path).resolve()) for path in docs_paths], + "file_types": args.file_types, + "include_hidden": bool(args.include_hidden), + "doc_chunk_size": doc_chunk_size + if doc_chunk_size is not None + else int(args.doc_chunk_size), + "doc_chunk_overlap": doc_chunk_overlap + if doc_chunk_overlap is not None + else int(args.doc_chunk_overlap), + "code_chunk_size": code_chunk_size + if code_chunk_size is not None + else int(args.code_chunk_size), + "code_chunk_overlap": code_chunk_overlap + if code_chunk_overlap is not None + else int(args.code_chunk_overlap), + "use_ast_chunking": bool(args.use_ast_chunking), + "ast_chunk_size": int(args.ast_chunk_size), + "ast_chunk_overlap": int(args.ast_chunk_overlap), + "ast_fallback_traditional": bool(args.ast_fallback_traditional), + "graph_degree": int(args.graph_degree), + "complexity": int(args.complexity), + "num_threads": int(args.num_threads), + "compact": bool(args.compact), + "recompute": bool(args.recompute), + } + if "host" in embedding_options: + config["embedding_host"] = embedding_options["host"] + if "base_url" in embedding_options: + config["embedding_api_base"] = embedding_options["base_url"] + if args.embedding_prompt_template: + config["embedding_prompt_template"] = args.embedding_prompt_template + if args.query_prompt_template: + config["query_prompt_template"] = args.query_prompt_template + return config + + def _resolve_sync_scope(self, docs_paths: list[str]) -> tuple[list[str], list[str]]: + directories: list[str] = [] + files: list[str] = [] + for path in docs_paths: + path_obj = Path(path).resolve() + if path_obj.is_dir(): + directories.append(str(path_obj)) + elif path_obj.is_file(): + files.append(str(path_obj)) + return sorted(directories), sorted(files) + + def _resolve_sync_roots(self, docs_paths: list[str]) -> list[str]: + """Directory roots only (legacy). Prefer _resolve_sync_scope.""" + directories, _files = self._resolve_sync_scope(docs_paths) + return directories + + def _create_synchronizers( + self, + index_dir: Path, + directories: list[str], + explicit_files: list[str], + include_extensions: list[str], + include_hidden: bool = False, + ) -> list[FileSynchronizer]: + """Create FileSynchronizers with snapshots stored in the index dir. Shared by build and watch.""" + synchronizers: list[FileSynchronizer] = [] + for root in directories: + tag = hashlib.sha256(root.encode()).hexdigest()[:12] + snapshot_path = str(index_dir / f"sync_{tag}.pickle") + try: + fs = FileSynchronizer( + root_dir=root, + include_extensions=include_extensions, + include_hidden=include_hidden, + snapshot_path=snapshot_path, + ) + synchronizers.append(fs) + except Exception as exc: + print(f"Warning: Failed to init synchronizer for {root}: {exc}") + if explicit_files: + tag = hashlib.sha256("|".join(explicit_files).encode()).hexdigest()[:12] + snapshot_path = str(index_dir / f"sync_files_{tag}.pickle") + try: + fs = FileSynchronizer( + explicit_files=explicit_files, + include_extensions=include_extensions, + include_hidden=include_hidden, + snapshot_path=snapshot_path, + ) + synchronizers.append(fs) + except Exception as exc: + print(f"Warning: Failed to init synchronizer for explicit files: {exc}") + return synchronizers + + def _build_synchronizers( + self, + docs_paths: list[str], + index_dir: Path, + file_types: Optional[str] = None, + include_hidden: bool = False, + ) -> list[FileSynchronizer]: + """Create FileSynchronizers for build from docs_paths.""" + directories, files = self._resolve_sync_scope(docs_paths) + include_extensions = self._parse_file_types(file_types) + return self._create_synchronizers( + index_dir, directories, files, include_extensions, include_hidden + ) + + def _detect_build_changes( + self, + synchronizers: list[FileSynchronizer], + ) -> tuple[set[str], set[str], set[str]]: + """Detect added/removed/modified files across all source roots using content hashes.""" + all_added: set[str] = set() + all_removed: set[str] = set() + all_modified: set[str] = set() + for fs in synchronizers: + added, removed, modified = fs.detect_changes() + all_added.update(added) + all_removed.update(removed) + all_modified.update(modified) + return all_added, all_removed, all_modified + + def _commit_synchronizers(self, synchronizers: list[FileSynchronizer]) -> None: + """Persist all synchronizer snapshots after a successful build.""" + for fs in synchronizers: + fs.commit() + + @staticmethod + def _assign_chunk_ids(chunks: list[dict]) -> None: + """Assign stable IDs to chunks based on their file path and position.""" + from collections import defaultdict + + by_path: dict[str, list] = defaultdict(list) + for c in chunks: + p = c.get("metadata", {}).get("file_path") or c.get("metadata", {}).get("source") or "" + by_path[_normalize_path(p)].append(c) + for path_key, path_chunks in by_path.items(): + for idx, c in enumerate(path_chunks): + sid = hashlib.sha256(f"{path_key}:{idx}".encode()).hexdigest()[:16] + c.setdefault("metadata", {})["id"] = sid + c["id"] = sid + + @staticmethod + def _assign_unique_chunk_ids(chunks: list[dict]) -> None: + """Assign unique IDs for incremental (avoids collision when path lookup misses some old ids).""" + for c in chunks: + sid = uuid.uuid4().hex[:16] + c.setdefault("metadata", {})["id"] = sid + c["id"] = sid + + def _chunks_for_paths(self, all_texts: list[dict], paths: set[str]) -> list[dict]: + """Filter chunks belonging to the given file paths.""" + return [ + c + for c in all_texts + if _normalize_path( + c.get("metadata", {}).get("file_path") or c.get("metadata", {}).get("source") or "" + ) + in paths + ] + + def _existing_index_id_scheme(self, index_path: str) -> Optional[str]: + """Return the passage_id_scheme recorded in an existing index's meta.json. + + Returns None when the index doesn't exist yet. Older indexes pre-#330 + have a meta file without passage_id_scheme and must be treated as + sequential so incremental updates never mix ID schemes. + """ + meta_path = Path(index_path).with_suffix(".leann.meta.json") + if not meta_path.exists(): + return None + try: + with open(meta_path, encoding="utf-8") as f: + return json.load(f).get("passage_id_scheme", "sequential") + except Exception: + return None + + def _make_incremental_builder(self, args) -> "LeannBuilder": + # For incremental updates, the existing index's scheme wins. Otherwise + # IDs would mix schemes within one index, which breaks lookups. + existing_scheme = self._existing_index_id_scheme(self.get_index_path(args.index_name)) + scheme = existing_scheme or getattr(args, "id_scheme", "sequential") + if existing_scheme and getattr(args, "id_scheme", existing_scheme) != existing_scheme: + print( + f"Note: --id-scheme={args.id_scheme} ignored β€” index '{args.index_name}' " + f"was built with passage_id_scheme={existing_scheme!r}, keeping that." + ) + return LeannBuilder( + backend_name=args.backend_name, + embedding_model=args.embedding_model, + embedding_mode=args.embedding_mode, + embedding_options=self._build_embedding_options(args) or None, + graph_degree=args.graph_degree, + complexity=args.complexity, + is_compact=args.compact, + is_recompute=args.recompute, + num_threads=args.num_threads, + passage_id_scheme=scheme, + ) + + def _incremental_add_only( + self, + index_path: str, + all_texts: list[dict], + args, + new_paths: set[str], + ) -> bool: + """Add-only incremental update (works for HNSW and IVF).""" + new_chunks = self._chunks_for_paths(all_texts, new_paths) + if not new_chunks: + return False + self._assign_chunk_ids(new_chunks) + builder = self._make_incremental_builder(args) + for chunk in new_chunks: + builder.add_text(chunk["text"], metadata=chunk["metadata"]) + print( + f"Incremental update: adding {len(new_chunks)} chunks from {len(new_paths)} new file(s)..." + ) + builder.update_index(index_path) + print(f"Index updated at {index_path}") + return True + + def _incremental_ivf_remove_only( + self, index_path: str, index_dir: Path, removed_paths: set[str], args + ) -> bool: + """IVF remove-only fast path: remove chunk IDs without loading or chunking documents.""" + passages_file = index_dir / "documents.leann.passages.jsonl" + if not passages_file.exists(): + return False + offset_file = index_dir / "documents.leann.passages.idx" + live_ids: set[str] | None = None + if offset_file.exists(): + with open(offset_file, "rb") as f: + live_ids = set(pickle.load(f).keys()) + chunk_ids_by_file = self._load_chunk_ids_by_file(passages_file, live_ids=live_ids) + roots = self._load_sync_roots(index_dir) + ids_to_remove: list[str] = [] + seen_ids: set[str] = set() + for p in removed_paths: + for key in self._path_lookup_keys(p, roots): + for pid in chunk_ids_by_file.get(key, []): + if pid not in seen_ids: + seen_ids.add(pid) + ids_to_remove.append(pid) + if not ids_to_remove: + return False + print( + f"Incremental IVF update (-{len(removed_paths)} removed): removing {len(ids_to_remove)} old chunks..." + ) + builder = self._make_incremental_builder(args) + builder.update_index(index_path, remove_passage_ids=ids_to_remove) + print(f"Index updated at {index_path}") + return True + + def _path_lookup_keys(self, path: str, roots: list[str]) -> list[str]: + """Return path variations for lookup (sync paths may be relative to roots).""" + keys = [path, _normalize_path(path)] + for root in roots: + candidate = str((Path(root) / path).resolve()) + if candidate not in keys: + keys.append(candidate) + return keys + + def _incremental_ivf_update( + self, + index_path: str, + index_dir: Path, + all_texts: list[dict], + args, + new_paths: set[str], + removed_paths: set[str], + modified_paths: set[str], + sync_roots: list[str], + ) -> bool: + """IVF incremental update: remove old chunks for modified/removed files, add new chunks.""" + passages_file = index_dir / "documents.leann.passages.jsonl" + offset_file = index_dir / "documents.leann.passages.idx" + live_ids: set[str] | None = None + if offset_file.exists(): + with open(offset_file, "rb") as f: + live_ids = set(pickle.load(f).keys()) + chunk_ids_by_file = ( + self._load_chunk_ids_by_file(passages_file, live_ids=live_ids) + if passages_file.exists() + else {} + ) + + # Collect old chunk IDs to remove (modified + removed files) + # Try all path variations: same file can have different path formats in passages + ids_to_remove: list[str] = [] + seen_ids: set[str] = set() + for p in modified_paths | removed_paths: + for key in self._path_lookup_keys(p, sync_roots): + for pid in chunk_ids_by_file.get(key, []): + if pid not in seen_ids: + seen_ids.add(pid) + ids_to_remove.append(pid) + + # Collect new chunks to add (modified + new files) + # Build path set for matching: chunks may have paths in different formats + changed_paths = new_paths | modified_paths + path_set: set[str] = set() + for p in changed_paths: + path_set.update(self._path_lookup_keys(p, sync_roots)) + new_chunks = self._chunks_for_paths(all_texts, path_set) + # Use unique IDs: passages can have mixed path formats so we may miss some ids_to_remove + self._assign_unique_chunk_ids(new_chunks) + + if not ids_to_remove and not new_chunks: + return False + + builder = self._make_incremental_builder(args) + for chunk in new_chunks: + builder.add_text(chunk["text"], metadata=chunk["metadata"]) + + parts = [] + if ids_to_remove: + parts.append(f"removing {len(ids_to_remove)} old chunks") + if new_chunks: + parts.append(f"adding {len(new_chunks)} new chunks") + file_parts = [] + if new_paths: + file_parts.append(f"+{len(new_paths)} added") + if modified_paths: + file_parts.append(f"~{len(modified_paths)} modified") + if removed_paths: + file_parts.append(f"-{len(removed_paths)} removed") + print(f"Incremental IVF update ({', '.join(file_parts)}): {', '.join(parts)}...") + + builder.update_index( + index_path, remove_passage_ids=ids_to_remove if ids_to_remove else None + ) + print(f"Index updated at {index_path}") + return True + + @staticmethod + def _log_rebuild_reason( + meta: dict, args, new_paths: set, removed_paths: set, modified_paths: set + ) -> None: + """Print a human-readable explanation of why incremental update is not possible.""" + if removed_paths or modified_paths: + reasons = [] + if removed_paths: + reasons.append(f"{len(removed_paths)} file(s) removed") + if modified_paths: + reasons.append(f"{len(modified_paths)} file(s) modified") + print( + f"Incremental update not possible ({', '.join(reasons)}); falling back to full rebuild." + ) + return + + blockers = [] + if meta.get("backend_name") not in ("hnsw", "ivf"): + blockers.append( + f"backend '{meta.get('backend_name')}' does not support incremental updates" + ) + if meta.get("is_compact", meta.get("backend_kwargs", {}).get("is_compact", True)): + blockers.append("index is compact (read-only); rebuild with --no-compact to enable") + if meta.get("embedding_model") != args.embedding_model: + blockers.append( + f"embedding model changed ('{meta.get('embedding_model')}' -> '{args.embedding_model}')" + ) + if meta.get("embedding_mode") != args.embedding_mode: + blockers.append( + f"embedding mode changed ('{meta.get('embedding_mode')}' -> '{args.embedding_mode}')" + ) + if blockers: + print( + f"Incremental update not possible: {'; '.join(blockers)}. Falling back to full rebuild." + ) + else: + changes = [] + if new_paths: + changes.append(f"+{len(new_paths)} added") + summary = ", ".join(changes) if changes else "unknown reason" + print(f"Full rebuild starting ({summary})...") + + def _write_sync_config( + self, + index_dir: Path, + directories: list[str], + explicit_files: list[str], + include_extensions: list[str], + include_hidden: bool, + build_config: Optional[dict[str, Any]] = None, + ) -> None: + sync_config_path = index_dir / "sync_roots.json" + config = { + "directories": directories, + "files": explicit_files, + "roots": directories, + "include_extensions": include_extensions, + "ignore_patterns": self._sync_ignore_patterns(include_hidden), + } + if build_config is not None: + config["build_config"] = build_config + with open(sync_config_path, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2) + + def _write_sync_config_for_docs( + self, + index_dir: Path, + docs_paths: list[str], + args, + build_config: Optional[dict[str, Any]] = None, + ) -> None: + directories, files = self._resolve_sync_scope(docs_paths) + self._write_sync_config( + index_dir, + directories, + files, + self._parse_file_types(args.file_types), + args.include_hidden, + build_config, + ) + + def _load_sync_scope(self, index_dir: Path) -> tuple[list[str], list[str], list[str], bool]: + """Load sync scope from index dir (directories, files, extensions, include_hidden).""" + sync_config_path = index_dir / "sync_roots.json" + if not sync_config_path.exists(): + return [], [], list(DEFAULT_INDEX_EXTENSIONS), False + try: + with open(sync_config_path, encoding="utf-8") as f: + config = json.load(f) + except (json.JSONDecodeError, OSError): + return [], [], list(DEFAULT_INDEX_EXTENSIONS), False + directories = config.get("directories") or config.get("roots") or [] + files = config.get("files") or [] + include_extensions = config.get("include_extensions") or list(DEFAULT_INDEX_EXTENSIONS) + include_hidden = config.get("ignore_patterns") is None + return directories, files, include_extensions, include_hidden + + def _load_sync_roots(self, index_dir: Path) -> list[str]: + """Load directory + explicit file paths for chunk ID lookup.""" + directories, files, _, _ = self._load_sync_scope(index_dir) + return directories + files + + def _resolve_index_for_watch(self, index_name: str) -> Optional[dict[str, Path]]: + if self.index_exists(index_name): + index_dir = self.indexes_dir / index_name + passages_file = index_dir / "documents.leann.passages.jsonl" + return {"index_dir": index_dir, "passages_file": passages_file} + + all_matches = self._find_all_matching_indexes(index_name) + if not all_matches: + print( + f"Index '{index_name}' not found. Use 'leann build {index_name} --docs [ ...]' to create it." + ) + return None + + if len(all_matches) == 1: + match = all_matches[0] + else: + current_matches = [m for m in all_matches if m.get("is_current")] + match = current_matches[0] if current_matches else all_matches[0] + location_desc = ( + "current project" + if match.get("is_current") + else f"project '{match['project_path'].name}'" + ) + print( + f"Found {len(all_matches)} indexes named '{index_name}', using index from {location_desc}" + ) + + if match.get("kind") == "cli": + index_dir = match["index_dir"] + passages_file = index_dir / "documents.leann.passages.jsonl" + else: + index_dir = match["meta_file"].parent + file_base = match["file_base"] + passages_file = index_dir / f"{file_base}.passages.jsonl" + + return {"index_dir": index_dir, "passages_file": passages_file} + + def _load_chunk_ids_by_file( + self, passages_file: Path, live_ids: set[str] | None = None + ) -> dict[str, list[str]]: + """Load chunk IDs grouped by file path from passages.jsonl. + + If *live_ids* is provided, skip entries whose ID is not in the set + (filters out stale entries left by prior incremental updates). + """ + chunk_ids_by_file: dict[str, list[str]] = {} + with open(passages_file, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + data = json.loads(line) + except json.JSONDecodeError: + continue + metadata = data.get("metadata") or {} + file_path = metadata.get("file_path") or metadata.get("source") + if not file_path: + continue + chunk_id = data.get("id") + if chunk_id is None: + continue + if live_ids is not None and str(chunk_id) not in live_ids: + continue + normalized_path = str(Path(file_path).resolve()) + chunk_ids_by_file.setdefault(normalized_path, []).append(str(chunk_id)) + if file_path != normalized_path: + chunk_ids_by_file.setdefault(file_path, []).append(str(chunk_id)) + return chunk_ids_by_file + + async def build_index(self, args): + docs_paths = args.docs + # Use current directory name if index_name not provided + if args.index_name: + index_name = args.index_name + else: + index_name = Path.cwd().name + print(f"Using current directory name as index: '{index_name}'") + + index_dir = self.indexes_dir / index_name + index_path = self.get_index_path(index_name) + + # Display all paths being indexed with file/directory distinction + files = [p for p in docs_paths if Path(p).is_file()] + directories = [p for p in docs_paths if Path(p).is_dir()] + + print(f"πŸ“‚ Indexing {len(docs_paths)} path{'s' if len(docs_paths) > 1 else ''}:") + if files: + print(f" πŸ“„ Files ({len(files)}):") + for i, file_path in enumerate(files, 1): + print(f" {i}. {Path(file_path).resolve()}") + if directories: + print(f" πŸ“ Directories ({len(directories)}):") + for i, dir_path in enumerate(directories, 1): + print(f" {i}. {Path(dir_path).resolve()}") + + # Configure chunking based on CLI args before loading documents + # Guard against invalid configurations + doc_chunk_size = max(1, int(args.doc_chunk_size)) + doc_chunk_overlap = max(0, int(args.doc_chunk_overlap)) + if doc_chunk_overlap >= doc_chunk_size: + print( + f"⚠️ Adjusting doc chunk overlap from {doc_chunk_overlap} to {doc_chunk_size - 1} (must be < chunk size)" + ) + doc_chunk_overlap = doc_chunk_size - 1 + + code_chunk_size = max(1, int(args.code_chunk_size)) + code_chunk_overlap = max(0, int(args.code_chunk_overlap)) + if code_chunk_overlap >= code_chunk_size: + print( + f"⚠️ Adjusting code chunk overlap from {code_chunk_overlap} to {code_chunk_size - 1} (must be < chunk size)" + ) + code_chunk_overlap = code_chunk_size - 1 + + self.node_parser = SentenceSplitter( + chunk_size=doc_chunk_size, + chunk_overlap=doc_chunk_overlap, + separator=" ", + paragraph_separator="\n\n", + ) + self.code_parser = SentenceSplitter( + chunk_size=code_chunk_size, + chunk_overlap=code_chunk_overlap, + separator="\n", + paragraph_separator="\n\n", + ) + build_config = self._build_config_from_args( + args, + docs_paths, + doc_chunk_size=doc_chunk_size, + doc_chunk_overlap=doc_chunk_overlap, + code_chunk_size=code_chunk_size, + code_chunk_overlap=code_chunk_overlap, + ) + + # Detect changes first so we can skip load_documents for remove-only + index_dir.mkdir(parents=True, exist_ok=True) + synchronizers = self._build_synchronizers( + docs_paths, index_dir, file_types=args.file_types, include_hidden=args.include_hidden + ) + + if index_dir.exists() and not args.force and synchronizers: + meta_path = index_dir / "documents.leann.meta.json" + new_paths, removed_paths, modified_paths = self._detect_build_changes(synchronizers) + + if not new_paths and not removed_paths and not modified_paths: + print("Index up to date.") + return + + # Show change summary (add / modify / delete) before build + change_parts = [] + if new_paths: + change_parts.append(f"+{len(new_paths)} added") + if modified_paths: + change_parts.append(f"~{len(modified_paths)} modified") + if removed_paths: + change_parts.append(f"-{len(removed_paths)} removed") + if change_parts: + print(f"Changes: {', '.join(change_parts)}") + + if meta_path.exists(): + with open(meta_path, encoding="utf-8") as f: + meta = json.load(f) + + backend_name = meta.get("backend_name") + is_compact = meta.get( + "is_compact", meta.get("backend_kwargs", {}).get("is_compact", True) + ) + + # Normalize model names for comparison β€” e.g. "all-MiniLM-L6-v2" + # should match "sentence-transformers/all-MiniLM-L6-v2" + def _normalize_model_name(name: str) -> str: + return name.rsplit("/", 1)[-1] if name else name + + same_embedding = ( + _normalize_model_name(meta.get("embedding_model", "")) + == _normalize_model_name(args.embedding_model) + and meta.get("embedding_mode") == args.embedding_mode + ) + + # IVF supports remove+add, so it can handle modified and removed files incrementally + can_ivf_update = backend_name == "ivf" and not is_compact and same_embedding + # HNSW only supports add (no remove), so it needs add-only changes + can_add_only = ( + not removed_paths + and not modified_paths + and backend_name in ("hnsw", "ivf") + and not is_compact + and same_embedding + ) + + # Remove-only fast path: no load/chunk, just remove IDs from index + if can_ivf_update and removed_paths and not new_paths and not modified_paths: + result = self._incremental_ivf_remove_only( + index_path, index_dir, removed_paths, args + ) + if result: + self._commit_synchronizers(synchronizers) + self._write_sync_config_for_docs(index_dir, docs_paths, args, build_config) + self.register_project_dir() + return + + # Resolve paths relative to sync scope (directories + explicit files). + sync_dirs, sync_files = self._resolve_sync_scope(docs_paths) + scope_roots = sync_dirs + sync_files + paths_to_load = new_paths | modified_paths + resolved_paths: list[str] = [] + for p in paths_to_load: + path_obj = Path(p) + if path_obj.is_absolute() and path_obj.exists(): + resolved_paths.append(p) + else: + for root in scope_roots: + candidate = Path(root) / p + if candidate.exists(): + resolved_paths.append(str(candidate.resolve())) + break + else: + resolved_paths.append(p) # fallback: pass as-is + all_texts = self.load_documents( + resolved_paths, + args.file_types, + include_hidden=args.include_hidden, + args=args, + ) + # Proceed even when all_texts is empty (e.g. file emptied): we still need to remove old chunks + if not all_texts and not (can_ivf_update and (modified_paths or removed_paths)): + print("No documents found") + return + + if can_ivf_update and (new_paths or modified_paths or removed_paths): + result = self._incremental_ivf_update( + index_path, + index_dir, + all_texts, + args, + new_paths, + removed_paths, + modified_paths, + scope_roots, + ) + if result: + self._commit_synchronizers(synchronizers) + self._write_sync_config_for_docs(index_dir, docs_paths, args, build_config) + self.register_project_dir() + return + + elif can_add_only and new_paths: + result = self._incremental_add_only( + index_path, + all_texts, + args, + new_paths, + ) + if result: + self._commit_synchronizers(synchronizers) + self._write_sync_config_for_docs(index_dir, docs_paths, args, build_config) + self.register_project_dir() + return + + else: + self._log_rebuild_reason(meta, args, new_paths, removed_paths, modified_paths) + # Force full reload β€” the partial all_texts from incremental + # path only contains changed files, not the full corpus. + all_texts = self.load_documents( + docs_paths, args.file_types, include_hidden=args.include_hidden, args=args + ) + + # Full rebuild: load documents if not already loaded (first build or force) + try: + _ = all_texts + except NameError: + all_texts = self.load_documents( + docs_paths, args.file_types, include_hidden=args.include_hidden, args=args + ) + if not all_texts: + print("No documents found") + return + + print(f"Building index '{index_name}' with {args.backend_name} backend...") + publish_from_staging = _existing_index_artifacts(index_dir) + target_index_dir = index_dir + target_index_path = index_path + if publish_from_staging: + target_index_dir = index_dir.with_name(f".{index_dir.name}.rebuild-{uuid.uuid4().hex}") + target_index_path = str(target_index_dir / "documents.leann") + + builder = LeannBuilder( + backend_name=args.backend_name, + embedding_model=args.embedding_model, + embedding_mode=args.embedding_mode, + embedding_options=self._build_embedding_options(args) or None, + graph_degree=args.graph_degree, + complexity=args.complexity, + is_compact=args.compact, + is_recompute=args.recompute, + num_threads=args.num_threads, + passage_id_scheme=getattr(args, "id_scheme", "sequential"), + ) + + for chunk in all_texts: + builder.add_text(chunk["text"], metadata=chunk["metadata"]) + + try: + builder.build_index(target_index_path) + target_synchronizers = ( + self._build_synchronizers( + docs_paths, + target_index_dir, + file_types=args.file_types, + include_hidden=args.include_hidden, + ) + if publish_from_staging + else synchronizers + ) + for fs in target_synchronizers: + fs.create_snapshot() + self._write_sync_config_for_docs(target_index_dir, docs_paths, args, build_config) + if publish_from_staging: + _publish_rebuilt_index(target_index_dir, index_dir) + except Exception: + if publish_from_staging and target_index_dir.exists(): + import shutil + + shutil.rmtree(target_index_dir) + raise + print(f"Index built at {index_path}") + self.register_project_dir() + + def _watch_check_changes(self, index_name: str) -> tuple[set[str], set[str], set[str]]: + """Check for file changes using the same snapshots as build (index_dir).""" + resolved = self._resolve_index_for_watch(index_name) + if not resolved: + return set(), set(), set() + + index_dir = resolved["index_dir"] + sync_config_path = index_dir / "sync_roots.json" + if not sync_config_path.exists(): + return set(), set(), set() + + directories, files, include_extensions, include_hidden = self._load_sync_scope(index_dir) + if not directories and not files: + return set(), set(), set() + + synchronizers = self._create_synchronizers( + index_dir, + directories, + files, + include_extensions, + include_hidden, + ) + return self._detect_build_changes(synchronizers) + + def _watch_report_changes( + self, + index_name: str, + added: set[str], + removed: set[str], + modified: set[str], + ) -> None: + """Print a summary of detected file changes.""" + resolved = self._resolve_index_for_watch(index_name) + passages_file = resolved["passages_file"] if resolved else None + + chunk_ids_by_file: dict[str, list[str]] = {} + if passages_file and passages_file.exists(): + chunk_ids_by_file = self._load_chunk_ids_by_file(passages_file) + + print("\n=== Changes detected ===") + for label, paths in ( + ("added", sorted(added)), + ("removed", sorted(removed)), + ("modified", sorted(modified)), + ): + if not paths: + continue + print(f"\n{label} ({len(paths)}):") + for file_path in paths: + normalized_path = str(Path(file_path).resolve()) + chunk_ids = chunk_ids_by_file.get(normalized_path) or chunk_ids_by_file.get( + file_path, [] + ) + chunk_display = ", ".join(chunk_ids) if chunk_ids else "(not in index)" + print(f" - {file_path}") + print(f" chunks: {chunk_display}") + + def _reconstruct_build_args( + self, index_name: str, *, force: bool = False, verbose: bool = False + ) -> Optional[list[str]]: + """Reconstruct the `leann build` CLI args for an existing index from its + stored config (.meta.json + sync_roots.json). + + Returns None when the index can't be rebuilt this way (no sync config, + missing metadata, etc.). With verbose=True the reason gets printed (for + the user-driven `leann rebuild` path); with verbose=False the failures + are silent (for the watch loop, which polls and shouldn't spam logs). + """ + resolved = self._resolve_index_for_watch(index_name) + if not resolved: + return None + index_dir = resolved["index_dir"] + sync_config_path = index_dir / "sync_roots.json" + if not sync_config_path.exists(): + if verbose: + print( + f"Cannot rebuild '{index_name}': no sync config at {sync_config_path}. " + f"This usually means the index was built via the Python API rather than " + f"`leann build --docs `, so the original docs roots aren't recorded." + ) + return None + with open(sync_config_path, encoding="utf-8") as f: + config = json.load(f) + roots = config.get("roots") or [] + if not roots: + if verbose: + print(f"Cannot rebuild '{index_name}': sync config has no document roots.") + return None + + meta_path = index_dir / "documents.leann.meta.json" + if not meta_path.exists(): + if verbose: + print(f"Cannot rebuild '{index_name}': index metadata missing at {meta_path}.") + else: + print(f"Index metadata missing for '{index_name}', cannot rebuild.") + return None + with open(meta_path, encoding="utf-8") as f: + meta = json.load(f) + + build_config = config.get("build_config") or meta.get("build_config") or {} + docs = build_config.get("docs") or roots + build_args_list = [ + "build", + index_name, + "--docs", + *[str(doc) for doc in docs], + "--backend-name", + meta.get("backend_name", "hnsw"), + "--embedding-model", + meta.get("embedding_model", "all-MiniLM-L6-v2"), + "--embedding-mode", + meta.get("embedding_mode", "sentence-transformers"), + ] + bkw = meta.get("backend_kwargs", {}) + + def add_option(flag: str, value: Any) -> None: + if value is not None: + build_args_list.extend([flag, str(value)]) + + def add_bool(true_flag: str, false_flag: str, value: Any) -> None: + if value is None: + return + build_args_list.append(true_flag if bool(value) else false_flag) + + def config_or_backend(config_key: str, backend_key: str) -> Any: + if config_key in build_config: + return build_config[config_key] + return bkw.get(backend_key) + + add_option("--graph-degree", config_or_backend("graph_degree", "graph_degree")) + add_option("--complexity", config_or_backend("complexity", "complexity")) + add_option("--num-threads", config_or_backend("num_threads", "num_threads")) + + compact = ( + build_config["compact"] + if "compact" in build_config + else meta.get("is_compact", bkw.get("is_compact")) + ) + recompute = ( + build_config["recompute"] + if "recompute" in build_config + else meta.get("is_recompute", bkw.get("is_recompute")) + ) + add_bool("--compact", "--no-compact", compact) + add_bool("--recompute", "--no-recompute", recompute) + + file_types = build_config.get("file_types") + if file_types is None: + include_extensions = config.get("include_extensions") + if include_extensions: + file_types = ",".join(include_extensions) + add_option("--file-types", file_types) + + include_hidden = build_config.get("include_hidden") + if include_hidden is None: + include_hidden = config.get("ignore_patterns") is None + if include_hidden: + build_args_list.append("--include-hidden") + + add_option("--doc-chunk-size", build_config.get("doc_chunk_size")) + add_option("--doc-chunk-overlap", build_config.get("doc_chunk_overlap")) + add_option("--code-chunk-size", build_config.get("code_chunk_size")) + add_option("--code-chunk-overlap", build_config.get("code_chunk_overlap")) + if build_config.get("use_ast_chunking"): + build_args_list.append("--use-ast-chunking") + add_option("--ast-chunk-size", build_config.get("ast_chunk_size")) + add_option("--ast-chunk-overlap", build_config.get("ast_chunk_overlap")) + if build_config.get("ast_fallback_traditional"): + build_args_list.append("--ast-fallback-traditional") + + embedding_options = meta.get("embedding_options") or {} + embedding_host = build_config.get("embedding_host", embedding_options.get("host")) + embedding_api_base = build_config.get( + "embedding_api_base", embedding_options.get("base_url") + ) + embedding_prompt_template = build_config.get("embedding_prompt_template") + if embedding_prompt_template is None: + embedding_prompt_template = embedding_options.get( + "build_prompt_template", embedding_options.get("prompt_template") + ) + query_prompt_template = build_config.get( + "query_prompt_template", embedding_options.get("query_prompt_template") + ) + add_option("--embedding-host", embedding_host) + add_option("--embedding-api-base", embedding_api_base) + add_option("--embedding-prompt-template", embedding_prompt_template) + add_option("--query-prompt-template", query_prompt_template) + if force: + build_args_list.append("--force") + return build_args_list + + async def rebuild_index(self, args) -> None: + """Rebuild an existing index using its stored config. + + By default this is an incremental delta (leann build is idempotent and + only re-indexes added/changed/removed files). Pass --force to do a full + rebuild from scratch. + """ + build_args_list = self._reconstruct_build_args( + args.index_name, force=getattr(args, "force", False), verbose=True + ) + if build_args_list is None: + return + parser = self.create_parser() + build_args = parser.parse_args(build_args_list) + await self.build_index(build_args) + + async def _watch_trigger_build(self, index_name: str) -> None: + """Trigger an idempotent build for the given index, reusing its stored config.""" + build_args_list = self._reconstruct_build_args(index_name, verbose=False) + if build_args_list is None: + return + parser = self.create_parser() + build_args = parser.parse_args(build_args_list) + await self.build_index(build_args) + + def migrate_ids(self, args) -> None: + """Rewrite an existing index's passage IDs to content-hash form. + + Migration is purely a Python-side rewrite β€” the vector graph isn't + touched, so FAISS labels stay valid. What gets rewritten: + - .passages.jsonl : new IDs in each line's "id" field + - .passages.idx : new offset map keyed by new IDs + - .ids.txt : new label β†’ ID mapping for FAISS + - .meta.json : passage_id_scheme = "content-hash" + - .bm25.sqlite : BM25 IDs, when an FTS5 artifact is configured + + Identical-text chunks collide on the same sha256 prefix; the later + occurrence wins in the offset map (dedup). A `--preserve-duplicates` + knob to suffix collisions can land separately. + + Irreversible. Prompts for confirmation unless --yes is passed. + """ + import hashlib + import shutil + + index_name = args.index_name + index_path = self._resolve_index_path(index_name, purpose="migrate") + if not index_path: + return + meta_path = Path(index_path).with_suffix(".leann.meta.json") + if not meta_path.exists(): + print(f"Cannot migrate '{index_name}': metadata missing at {meta_path}.") + return + with open(meta_path, encoding="utf-8") as f: + meta = json.load(f) + current_scheme = meta.get("passage_id_scheme", "sequential") + if current_scheme == "content-hash": + print(f"Index '{index_name}' already uses content-hash IDs. Nothing to do.") + return + + # Locate the sibling artifacts using the same conventions as build_index. + index_dir = Path(index_path).parent + index_base = Path(index_path).name + passages_file = index_dir / f"{index_base}.passages.jsonl" + offset_file = index_dir / f"{index_base}.passages.idx" + base_no_leann = ( + index_base[: -len(".leann")] if index_base.endswith(".leann") else index_base + ) + idmap_file = index_dir / f"{base_no_leann}.ids.txt" + + for p in (passages_file, offset_file): + if not p.exists(): + print(f"Cannot migrate: required artifact missing at {p}.") + return + + # Stream the passages to plan the rewrite and surface collision count + # before committing to anything irreversible. + old_ids: list[str] = [] + new_ids: list[str] = [] + with open(passages_file, encoding="utf-8") as f: + for line in f: + if not line.strip(): + continue + data = json.loads(line) + old_ids.append(data["id"]) + new_ids.append(hashlib.sha256(data["text"].encode("utf-8")).hexdigest()[:16]) + + unique_new = len(set(new_ids)) + collisions = len(new_ids) - unique_new + print( + f"Migrating '{index_name}': {len(new_ids)} passages β†’ {unique_new} unique " + f"content-hash IDs ({collisions} collision(s) will dedup)." + ) + + if args.dry_run: + print("(dry-run; not writing anything)") + return + if not args.yes: + confirm = input( + "Proceed? This rewrites passages.jsonl, .idx, .ids.txt, .meta.json. [y/N] " + ) + if confirm.strip().lower() not in ("y", "yes"): + print("Aborted.") + return + + # Stage writes into siblings, then atomically rename. + new_passages = passages_file.with_suffix(passages_file.suffix + ".migrate") + new_offsets: dict[str, int] = {} + rewritten_passages: list[dict[str, Any]] = [] + with ( + open(passages_file, encoding="utf-8") as src, + open(new_passages, "w", encoding="utf-8") as dst, + ): + idx = 0 + for line in src: + if not line.strip(): + continue + data = json.loads(line) + data["id"] = new_ids[idx] + offset = dst.tell() + json.dump(data, dst, ensure_ascii=False) + dst.write("\n") + new_offsets[new_ids[idx]] = offset + rewritten_passages.append(data) + idx += 1 + + new_idx = offset_file.with_suffix(offset_file.suffix + ".migrate") + with open(new_idx, "wb") as f: + pickle.dump(new_offsets, f) + + new_idmap: Optional[Path] = None + if idmap_file.exists(): + new_idmap = idmap_file.with_suffix(idmap_file.suffix + ".migrate") + with open(new_idmap, "w", encoding="utf-8") as f: + for nid in new_ids: + f.write(nid + "\n") + + bm25_db_name = meta.get("bm25_db") + should_rebuild_bm25 = meta.get("bm25_backend") == "fts5" + if should_rebuild_bm25 and not bm25_db_name: + bm25_db_name = f"{index_base}.bm25.sqlite" + new_bm25: Optional[Path] = None + bm25_file: Optional[Path] = None + if should_rebuild_bm25 and bm25_db_name: + bm25_file = index_dir / bm25_db_name + new_bm25 = bm25_file.with_suffix(bm25_file.suffix + ".migrate") + bm25_index = Fts5BM25Index(str(new_bm25)) + bm25_index.fit(rewritten_passages) + bm25_index.close() + + shutil.move(str(new_passages), str(passages_file)) + shutil.move(str(new_idx), str(offset_file)) + if new_idmap is not None: + shutil.move(str(new_idmap), str(idmap_file)) + if new_bm25 is not None and bm25_file is not None: + shutil.move(str(new_bm25), str(bm25_file)) + + meta["passage_id_scheme"] = "content-hash" + meta["version"] = "1.1" + if should_rebuild_bm25 and bm25_db_name: + meta["bm25_backend"] = "fts5" + meta["bm25_db"] = bm25_db_name + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, indent=2) + + print( + f"βœ“ Migrated '{index_name}' to content-hash IDs. {collisions} collisions were deduped." + ) + + async def watch_index(self, args): + index_name = args.index_name + resolved = self._resolve_index_for_watch(index_name) + if not resolved: + return + + index_dir = resolved["index_dir"] + sync_config_path = index_dir / "sync_roots.json" + if not sync_config_path.exists(): + print( + f"Sync config not found for index '{index_name}'. " + f"Run 'leann build {index_name} --docs ' first." + ) + return + + dry_run = getattr(args, "dry_run", False) + once = getattr(args, "once", False) + interval = getattr(args, "interval", 5) + + if once: + added, removed, modified = self._watch_check_changes(index_name) + if not added and not removed and not modified: + print("No changes detected.") + return + self._watch_report_changes(index_name, added, removed, modified) + if not dry_run: + await self._watch_trigger_build(index_name) + return + + print(f"Watching index '{index_name}' (interval={interval}s, ctrl-c to stop)...") + try: + while True: + added, removed, modified = self._watch_check_changes(index_name) + if added or removed or modified: + self._watch_report_changes(index_name, added, removed, modified) + if not dry_run: + await self._watch_trigger_build(index_name) + await asyncio.sleep(interval) + except KeyboardInterrupt: + print("\nWatch stopped.") + + def _resolve_index_path( + self, + index_name: str, + *, + non_interactive: bool = True, + purpose: str = "use", + quiet: bool = False, + ) -> Optional[str]: + """Resolve index path from current project or registered projects.""" + _print = (lambda *a, **kw: print(*a, file=sys.stderr, **kw)) if quiet else print + + if self.index_exists(index_name): + return self.get_index_path(index_name) + + all_matches = self._find_all_matching_indexes(index_name) + if not all_matches: + _print( + f"Index '{index_name}' not found. Use 'leann build {index_name} --docs [ ...]' to create it." + ) + return None + + def _match_to_path(match: dict[str, Any]) -> str: + if match["kind"] == "cli": + return str(match["index_dir"] / "documents.leann") + meta_file = match["meta_file"] + file_base = match["file_base"] + return str(meta_file.parent / f"{file_base}.leann") + + if len(all_matches) == 1: + match = all_matches[0] + project_info = ( + "current project" + if match["is_current"] + else f"project '{match['project_path'].name}'" + ) + _print(f"Using index '{index_name}' from {project_info}") + return _match_to_path(match) + + if non_interactive: + current_matches = [m for m in all_matches if m["is_current"]] + match = current_matches[0] if current_matches else all_matches[0] + location_desc = ( + "current project" + if match["is_current"] + else f"project '{match['project_path'].name}'" + ) + _print( + f"Found {len(all_matches)} indexes named '{index_name}', using index from {location_desc}" + ) + return _match_to_path(match) + + print(f"Found {len(all_matches)} indexes named '{index_name}':") + for i, match in enumerate(all_matches, 1): + project_path = match["project_path"] + is_current = match["is_current"] + kind = match.get("kind", "cli") + if is_current: + print(f" {i}. 🏠 Current project ({'CLI' if kind == 'cli' else 'APP'})") + else: + print(f" {i}. πŸ“‚ {project_path.name} ({'CLI' if kind == 'cli' else 'APP'})") + + try: + choice = input(f"Which index to {purpose}? (1-{len(all_matches)}): ").strip() + choice_idx = int(choice) - 1 + if 0 <= choice_idx < len(all_matches): + match = all_matches[choice_idx] + project_info = ( + "current project" + if match["is_current"] + else f"project '{match['project_path'].name}'" + ) + print(f"Using index '{index_name}' from {project_info}") + return _match_to_path(match) + print("Invalid choice. Aborting.") + return None + except (ValueError, KeyboardInterrupt): + print("Invalid input. Aborting.") + return None + + async def search_documents(self, args): + index_name = args.index_name + query = args.query + json_mode = getattr(args, "json", False) + + index_path = self._resolve_index_path( + index_name, + non_interactive=args.non_interactive, + purpose="search", + quiet=json_mode, + ) + if not index_path: + return + + # Build provider_options for runtime override + provider_options = {} + if args.embedding_prompt_template: + provider_options["prompt_template"] = args.embedding_prompt_template + + # Parse --metadata-filters JSON string + metadata_filters = None + raw_filters = getattr(args, "metadata_filters", None) + if raw_filters: + try: + metadata_filters = json.loads(raw_filters) + if not isinstance(metadata_filters, dict): + print( + "Error: --metadata-filters must be a JSON object, " + 'e.g. \'{"chapter": {"<=": 5}}\'' + ) + return + except json.JSONDecodeError as e: + print(f"Error: --metadata-filters is not valid JSON: {e}") + return + + saved_fd = None + if json_mode: + sys.stdout.flush() + try: + saved_fd = os.dup(1) + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, 1) + os.close(devnull) + except OSError: + saved_fd = None + + try: + searcher = LeannSearcher( + index_path=index_path, + enable_warmup=args.enable_warmup, + use_daemon=args.use_daemon, + daemon_ttl_seconds=args.daemon_ttl, + ) + results = searcher.search( + query, + top_k=args.top_k, + complexity=args.complexity, + beam_width=args.beam_width, + prune_ratio=args.prune_ratio, + recompute_embeddings=args.recompute_embeddings, + pruning_strategy=args.pruning_strategy, + provider_options=provider_options if provider_options else None, + metadata_filters=metadata_filters, + ) + finally: + if saved_fd is not None: + sys.stdout.flush() + os.dup2(saved_fd, 1) + os.close(saved_fd) + + if json_mode: + json_results = [ + { + "id": r.id, + "score": float(r.score), + "text": r.text, + "metadata": r.metadata, + } + for r in results + ] + print(json.dumps(json_results, ensure_ascii=False, indent=2)) + return + + print(f"Search results for '{query}' (top {len(results)}):") + for i, result in enumerate(results, 1): + print(f"{i}. Score: {result.score:.3f}") + + if args.show_metadata and result.metadata: + file_path = result.metadata.get("file_path", "") + if file_path: + print(f" File: {file_path}") + + file_name = result.metadata.get("file_name", "") + if file_name and file_name != file_path: + print(f" Name: {file_name}") + + if "creation_date" in result.metadata: + print(f" Created: {result.metadata['creation_date']}") + if "last_modified_date" in result.metadata: + print(f" Modified: {result.metadata['last_modified_date']}") + + print(f" {result.text}") + print(f" Source: {result.metadata.get('source', '')}") + print() + + async def warmup_index(self, args): + index_path = self._resolve_index_path( + args.index_name, + non_interactive=True, + purpose="warm up", + ) + if not index_path: + return + + searcher = LeannSearcher( + index_path=index_path, + recompute_embeddings=True, + enable_warmup=args.enable_warmup, + use_daemon=args.use_daemon, + daemon_ttl_seconds=args.daemon_ttl, + ) + if args.enable_warmup: + searcher.warmup() + print( + f"Warmed index '{args.index_name}' (daemon={'on' if args.use_daemon else 'off'}, ttl={args.daemon_ttl}s)" + ) + + async def daemon_command(self, args): + if not args.daemon_command: + print("Please specify one of: start, stop, status") + return + + if args.daemon_command == "status": + records = EmbeddingServerManager.list_daemons() + if args.index_name: + index_path = self._resolve_index_path( + args.index_name, + non_interactive=True, + purpose="check daemon status for", + ) + if not index_path: + return + meta_path = str(Path(f"{index_path}.meta.json").resolve()) + records = [ + r + for r in records + if r.get("config_signature", {}).get("passages_file") == meta_path + ] + + if not records: + print("No active embedding daemons.") + return + + print(f"Active embedding daemons: {len(records)}") + for record in records: + cfg = record.get("config_signature", {}) + print( + f"- pid={record.get('pid')} port={record.get('port')} backend={record.get('backend_module_name')} model={cfg.get('model_name')}" + ) + return + + if args.daemon_command == "start": + index_path = self._resolve_index_path( + args.index_name, + non_interactive=True, + purpose="start daemon for", + ) + if not index_path: + return + + searcher = LeannSearcher( + index_path=index_path, + recompute_embeddings=True, + enable_warmup=args.enable_warmup, + use_daemon=True, + daemon_ttl_seconds=args.daemon_ttl, + ) + searcher.warmup() + print( + f"Daemon started for '{args.index_name}' (ttl={args.daemon_ttl}s, warmup={'on' if args.enable_warmup else 'off'})" + ) + return + + if args.daemon_command == "stop": + if args.all: + stopped = EmbeddingServerManager.stop_daemons() + print(f"Stopped {stopped} daemon(s).") + return + + if not args.index_name: + print("Provide an index name or pass --all.") + return + + index_path = self._resolve_index_path( + args.index_name, + non_interactive=True, + purpose="stop daemon for", + ) + if not index_path: + return + meta_path = str(Path(f"{index_path}.meta.json").resolve()) + stopped = EmbeddingServerManager.stop_daemons(passages_file=meta_path) + print(f"Stopped {stopped} daemon(s) for index '{args.index_name}'.") + + async def ask_questions(self, args): + index_name = args.index_name + index_path = self.get_index_path(index_name) + + if not self.index_exists(index_name): + print( + f"Index '{index_name}' not found. Use 'leann build {index_name} --docs [ ...]' to create it." + ) + return + + print(f"Starting chat with index '{index_name}'...") + print(f"Using {args.model} ({args.llm})") + + llm_config = {"type": args.llm, "model": args.model} + if args.llm == "ollama": + llm_config["host"] = resolve_ollama_host(args.host) + elif args.llm == "openai": + llm_config["base_url"] = resolve_openai_base_url(args.api_base) + resolved_api_key = resolve_openai_api_key(args.api_key) + if resolved_api_key: + llm_config["api_key"] = resolved_api_key + elif args.llm == "anthropic": + # For Anthropic, pass base_url and API key if provided + if args.api_base: + llm_config["base_url"] = resolve_anthropic_base_url(args.api_base) + if args.api_key: + llm_config["api_key"] = args.api_key + elif args.llm == "minimax": + llm_config["base_url"] = resolve_minimax_base_url(args.api_base) + resolved_api_key = resolve_minimax_api_key(args.api_key) + if resolved_api_key: + llm_config["api_key"] = resolved_api_key + + # Parse --metadata-filters JSON string (mirrors `leann search`). + # Run before constructing LeannChat so invalid filters fail fast without + # spinning up an embedding server. + metadata_filters = None + raw_filters = getattr(args, "metadata_filters", None) + if raw_filters: + try: + metadata_filters = json.loads(raw_filters) + if not isinstance(metadata_filters, dict): + print( + "Error: --metadata-filters must be a JSON object, " + 'e.g. \'{"chapter": {"<=": 5}}\'' + ) + return + except json.JSONDecodeError as e: + print(f"Error: --metadata-filters is not valid JSON: {e}") + return + + chat = LeannChat(index_path=index_path, llm_config=llm_config) + + llm_kwargs: dict[str, Any] = {} + if args.thinking_budget: + llm_kwargs["thinking_budget"] = args.thinking_budget + + def _ask_once(prompt: str) -> None: + query_start_time = time.time() + response = chat.ask( + prompt, + top_k=args.top_k, + complexity=args.complexity, + beam_width=args.beam_width, + prune_ratio=args.prune_ratio, + recompute_embeddings=args.recompute_embeddings, + pruning_strategy=args.pruning_strategy, + metadata_filters=metadata_filters, + llm_kwargs=llm_kwargs, + ) + query_completion_time = time.time() - query_start_time + print(f"LEANN: {response}") + print(f"The query took {query_completion_time:.3f} seconds to finish") + + initial_query = (args.query or "").strip() + + if args.interactive: + # Create interactive session + session = create_cli_session(index_name) + + if initial_query: + _ask_once(initial_query) + + session.run_interactive_loop(_ask_once) + else: + query = initial_query or input("Enter your question: ").strip() + if not query: + print("No question provided. Exiting.") + return + + _ask_once(query) + + # ── index-* shared handler ────────────────────────────────────── + + async def _build_index_from_documents(self, args, documents: list): + """Shared builder for all index-* commands. + + Accepts a list of llama-index Document objects (or dicts with 'text' and + 'metadata' keys), builds a LEANN index with proper embedding and + recomputation settings. + """ + if not documents: + print("No documents loaded β€” nothing to index.") + return + + index_name = args.index_name + index_path = self.get_index_path(index_name) + is_recompute = not getattr(args, "no_recompute", False) + + print(f"Building index '{index_name}' with {len(documents)} documents...") + print(f" Embedding: {args.embedding_model} ({args.embedding_mode})") + print(f" Recompute: {is_recompute}") + + for attr in ( + "embedding_prompt_template", + "query_prompt_template", + "embedding_host", + "embedding_api_base", + "embedding_api_key", + ): + if not hasattr(args, attr): + setattr(args, attr, None) + + embedding_options = self._build_embedding_options(args) or None + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model=args.embedding_model, + embedding_mode=args.embedding_mode, + embedding_options=embedding_options, + is_recompute=is_recompute, + ) + + for doc in documents: + if hasattr(doc, "text"): + builder.add_text( + doc.text, metadata=doc.metadata if hasattr(doc, "metadata") else {} + ) + elif isinstance(doc, dict): + builder.add_text(doc["text"], metadata=doc.get("metadata", {})) + + builder.build_index(index_path) + self.register_project_dir() + print(f"Index '{index_name}' built at {index_path}") + + async def index_browser(self, args): + """Index browser history (Chrome or Brave).""" + from apps.history_data.history import ChromeHistoryReader + + browser = getattr(args, "browser", "chrome") + profile_paths = { + "chrome": "~/Library/Application Support/Google/Chrome/Default", + "brave": "~/Library/Application Support/BraveSoftware/Brave-Browser/Default", + } + profile = os.path.expanduser(profile_paths.get(browser, profile_paths["chrome"])) + reader = ChromeHistoryReader() + docs = reader.load_data(chrome_profile_path=profile, max_count=args.max_count) + print(f"Loaded {len(docs)} {browser} history entries") + await self._build_index_from_documents(args, docs) + + async def index_email(self, args): + """Index Apple Mail.""" + from apps.email_data.LEANN_email_reader import EmlxReader, find_all_messages_directories + + msg_dirs = find_all_messages_directories() + if not msg_dirs: + print("No Apple Mail Messages directories found.") + return + reader = EmlxReader() + docs = [] + for msg_dir in msg_dirs: + docs.extend(reader.load_data(str(msg_dir), max_count=args.max_count)) + await self._build_index_from_documents(args, docs) + + async def index_calendar(self, args): + """Index Apple Calendar events.""" + import shutil + import sqlite3 + + from llama_index.core import Document + + docs = [] + calendar_cache = Path.home() / "Library/Calendars/Calendar Cache" + if not calendar_cache.exists(): + print("Apple Calendar Cache not found.") + return + + temp_db = "/tmp/leann_calendar_index_copy" + try: + shutil.copy2(calendar_cache, temp_db) + conn = sqlite3.connect(temp_db) + cursor = conn.cursor() + cursor.execute( + """ + SELECT summary, description, location, + datetime(start_date + 978307200, 'unixepoch', 'localtime') as start, + datetime(end_date + 978307200, 'unixepoch', 'localtime') as end_time + FROM CI_EVENT ORDER BY start_date DESC LIMIT ? + """, + (args.max_count,), + ) + for summary, description, location, start, end_time in cursor.fetchall(): + if not summary: + continue + text = ( + f"Event: {summary}\nStart: {start}\nEnd: {end_time}\n" + f"Location: {location or ''}\nDescription: {description or ''}" + ) + docs.append(Document(text=text, metadata={"event": summary, "start": start})) + conn.close() + except Exception as e: + print(f"Error reading Apple Calendar: {e}") + finally: + if os.path.exists(temp_db): + os.remove(temp_db) + await self._build_index_from_documents(args, docs) + + async def index_imessage(self, args): + """Index iMessage conversations.""" + from apps.imessage_data.imessage_reader import IMessageReader + + reader = IMessageReader(concatenate_conversations=True) + docs = reader.load_data() + print(f"Loaded {len(docs)} iMessage conversations") + await self._build_index_from_documents(args, docs) + + async def index_wechat(self, args): + """Index WeChat chat history from exported JSON.""" + from apps.history_data.wechat_history import WeChatHistoryReader + + reader = WeChatHistoryReader() + docs = reader.load_data( + input_dir=args.export_dir, + max_count=args.max_count, + concatenate_messages=True, + ) + print(f"Loaded {len(docs)} WeChat conversations") + await self._build_index_from_documents(args, docs) + + async def index_chatgpt(self, args): + """Index ChatGPT export data.""" + from apps.chatgpt_data.chatgpt_reader import ChatGPTReader + + reader = ChatGPTReader(concatenate_conversations=True) + docs = reader.load_data( + input_dir=args.export_path, + max_count=args.max_count, + ) + print(f"Loaded {len(docs)} ChatGPT conversations") + await self._build_index_from_documents(args, docs) + + async def index_claude(self, args): + """Index Claude export data.""" + from apps.claude_data.claude_reader import ClaudeReader + + reader = ClaudeReader(concatenate_conversations=True) + docs = reader.load_data( + input_dir=args.export_path, + max_count=args.max_count, + ) + print(f"Loaded {len(docs)} Claude conversations") + await self._build_index_from_documents(args, docs) + + async def react_agent(self, args): + """Run ReAct agent for multiturn retrieval.""" + index_name = args.index_name + query = args.query + + # Find the index (similar to search_documents) + index_path = self.get_index_path(index_name) + if self.index_exists(index_name): + pass + else: + all_matches = self._find_all_matching_indexes(index_name) + if not all_matches: + print( + f"Index '{index_name}' not found. Use 'leann build {index_name} --docs [ ...]' to create it." + ) + return + elif len(all_matches) == 1: + match = all_matches[0] + if match["kind"] == "cli": + index_path = str(match["index_dir"] / "documents.leann") + else: + meta_file = match["meta_file"] + file_base = match["file_base"] + index_path = str(meta_file.parent / f"{file_base}.leann") + else: + # Multiple matches - use first one for now + match = all_matches[0] + if match["kind"] == "cli": + index_path = str(match["index_dir"] / "documents.leann") + else: + meta_file = match["meta_file"] + file_base = match["file_base"] + index_path = str(meta_file.parent / f"{file_base}.leann") + print(f"Found {len(all_matches)} indexes named '{index_name}', using first match") + + print(f"πŸ€– Starting ReAct agent with index '{index_name}'...") + print(f"Using {args.model} ({args.llm})") + + llm_config = {"type": args.llm, "model": args.model} + if args.llm == "ollama": + llm_config["host"] = resolve_ollama_host(args.host) + elif args.llm == "openai": + llm_config["base_url"] = resolve_openai_base_url(args.api_base) + resolved_api_key = resolve_openai_api_key(args.api_key) + if resolved_api_key: + llm_config["api_key"] = resolved_api_key + elif args.llm == "anthropic": + if args.api_base: + llm_config["base_url"] = resolve_anthropic_base_url(args.api_base) + if args.api_key: + llm_config["api_key"] = args.api_key + elif args.llm == "minimax": + llm_config["base_url"] = resolve_minimax_base_url(args.api_base) + resolved_api_key = resolve_minimax_api_key(args.api_key) + if resolved_api_key: + llm_config["api_key"] = resolved_api_key + + from .react_agent import create_react_agent + + agent = create_react_agent( + index_path=index_path, + llm_config=llm_config, + max_iterations=args.max_iterations, + serper_api_key=getattr(args, "serper_api_key", None), + jina_api_key=getattr(args, "jina_api_key", None), + ) + + if agent.web_search_available: + print("🌐 Web search enabled (Serper API key detected)") + else: + print("πŸ“š Local search only (no Serper API key)") + + print(f"\nπŸ” Question: {query}\n") + answer = agent.run(query, top_k=args.top_k) + print(f"\nβœ… Final Answer:\n{answer}\n") + + if agent.search_history: + print(f"\nπŸ“Š Search History ({len(agent.search_history)} iterations):") + for entry in agent.search_history: + source_tag = f"[{entry.get('source', 'local')}]" + print( + f" {entry['iteration']}. {source_tag} {entry['action']} " + f"({entry['results_count']} results)" + ) + + async def serve_api(self, args): + """Start the HTTP API server.""" + import os + + try: + from .server import serve_async + + # Override host/port if provided via CLI args + if args.host: + os.environ["LEANN_SERVER_HOST"] = args.host + if args.port: + os.environ["LEANN_SERVER_PORT"] = str(args.port) + + # Must await uvicorn on this loop: sync uvicorn.run() starts a nested loop + # and fails with "Cannot run the event loop while another loop is running". + await serve_async() + except ImportError as e: + print( + "❌ HTTP server dependencies not installed.\n" + "Install them with:\n" + " uv pip install 'leann-core[server]'\n" + "or:\n" + " uv pip install 'fastapi>=0.115' 'pydantic>=2' 'uvicorn[standard]'\n" + ) + raise SystemExit(1) from e + except Exception as e: + print(f"❌ Error starting server: {e}") + raise SystemExit(1) from e + + async def run(self, args=None): + parser = self.create_parser() + + if args is None: + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + # Determine whether to suppress C++ output + # Default is to suppress (quiet mode), unless --verbose is specified + suppress = not getattr(args, "verbose", False) + + if args.command == "list": + self.list_indexes() + elif args.command == "remove": + self.remove_index(args.index_name, args.force) + elif args.command == "build": + with suppress_cpp_output(suppress): + await self.build_index(args) + elif args.command == "watch": + await self.watch_index(args) + elif args.command == "migrate-ids": + self.migrate_ids(args) + elif args.command == "rebuild": + with suppress_cpp_output(suppress): + await self.rebuild_index(args) + elif args.command == "search": + with suppress_cpp_output(suppress): + await self.search_documents(args) + elif args.command == "warmup": + with suppress_cpp_output(suppress): + await self.warmup_index(args) + elif args.command == "daemon": + await self.daemon_command(args) + elif args.command == "ask": + with suppress_cpp_output(suppress): + await self.ask_questions(args) + elif args.command == "react": + with suppress_cpp_output(suppress): + await self.react_agent(args) + elif args.command == "serve": + await self.serve_api(args) + elif args.command in ( + "index-browser", + "index-email", + "index-calendar", + "index-imessage", + "index-wechat", + "index-chatgpt", + "index-claude", + ): + handler = { + "index-browser": self.index_browser, + "index-email": self.index_email, + "index-calendar": self.index_calendar, + "index-imessage": self.index_imessage, + "index-wechat": self.index_wechat, + "index-chatgpt": self.index_chatgpt, + "index-claude": self.index_claude, + }[args.command] + with suppress_cpp_output(suppress): + await handler(args) + else: + parser.print_help() + + +def main(): + import logging + + import dotenv + + dotenv.load_dotenv() + + # Set clean logging for CLI usage + logging.getLogger().setLevel(logging.WARNING) # Only show warnings and errors + + cli = LeannCLI() + asyncio.run(cli.run()) + + +if __name__ == "__main__": + main() diff --git a/packages/leann-core/src/leann/embedding_compute.py b/packages/leann-core/src/leann/embedding_compute.py new file mode 100644 index 0000000..6ff5d8b --- /dev/null +++ b/packages/leann-core/src/leann/embedding_compute.py @@ -0,0 +1,1451 @@ +""" +Unified embedding computation module +Consolidates all embedding computation logic using SentenceTransformer +Preserves all optimization parameters to ensure performance +""" + +import json +import logging +import os +import subprocess +import time +from typing import Any, Optional, Protocol, cast + +import numpy as np + +from .settings import resolve_ollama_host, resolve_openai_api_key, resolve_openai_base_url + +# torch and tiktoken are imported lazily inside the functions that use them, so +# `import leann` (e.g. for MCP search over an existing index, BM25-only flows, +# or non-embedding utilities) doesn't pull torch's ~1 GB of state into memory. + +# Set up logger with proper level +logger = logging.getLogger(__name__) +LOG_LEVEL = os.getenv("LEANN_LOG_LEVEL", "WARNING").upper() +log_level = getattr(logging, LOG_LEVEL, logging.WARNING) +logger.setLevel(log_level) + + +class _SentenceTransformerLike(Protocol): + def eval(self) -> Any: ... + def parameters(self) -> Any: ... + def encode(self, *args: Any, **kwargs: Any) -> Any: ... + def half(self) -> Any: ... + + +# Token limit registry for embedding models +# Used as fallback when dynamic discovery fails (e.g., LM Studio, OpenAI) +# Ollama models use dynamic discovery via /api/show +EMBEDDING_MODEL_LIMITS = { + # Nomic models (common across servers) + "nomic-embed-text": 2048, # Corrected from 512 - verified via /api/show + "nomic-embed-text-v1.5": 2048, + "nomic-embed-text-v2": 512, + # Other embedding models + "mxbai-embed-large": 512, + "all-minilm": 512, + "bge-m3": 8192, + "snowflake-arctic-embed": 512, + # OpenAI models + "text-embedding-3-small": 8192, + "text-embedding-3-large": 8192, + "text-embedding-ada-002": 8192, +} + +# Runtime cache for dynamically discovered token limits +# Key: (model_name, base_url), Value: token_limit +# Prevents repeated SDK/API calls for the same model +_token_limit_cache: dict[tuple[str, str], int] = {} + + +def get_model_token_limit( + model_name: str, + base_url: Optional[str] = None, + default: int = 2048, +) -> int: + """ + Get token limit for a given embedding model. + Uses hybrid approach: dynamic discovery for Ollama, registry fallback for others. + Caches discovered limits to prevent repeated API/SDK calls. + + Args: + model_name: Name of the embedding model + base_url: Base URL of the embedding server (for dynamic discovery) + default: Default token limit if model not found + + Returns: + Token limit for the model in tokens + """ + # Check cache first to avoid repeated SDK/API calls + cache_key = (model_name, base_url or "") + if cache_key in _token_limit_cache: + cached_limit = _token_limit_cache[cache_key] + logger.debug(f"Using cached token limit for {model_name}: {cached_limit}") + return cached_limit + + # Try Ollama dynamic discovery if base_url provided + if base_url: + # Detect Ollama servers by port or "ollama" in URL + if "11434" in base_url or "ollama" in base_url.lower(): + limit = _query_ollama_context_limit(model_name, base_url) + if limit: + _token_limit_cache[cache_key] = limit + return limit + + # Try LM Studio SDK discovery + if "1234" in base_url or "lmstudio" in base_url.lower() or "lm.studio" in base_url.lower(): + # Convert HTTP to WebSocket URL + ws_url = base_url.replace("https://", "wss://").replace("http://", "ws://") + # Remove /v1 suffix if present + if ws_url.endswith("/v1"): + ws_url = ws_url[:-3] + + limit = _query_lmstudio_context_limit(model_name, ws_url) + if limit: + _token_limit_cache[cache_key] = limit + return limit + + # Fallback to known model registry with version handling (from PR #154) + # Handle versioned model names (e.g., "nomic-embed-text:latest" -> "nomic-embed-text") + base_model_name = model_name.split(":")[0] + + # Check exact match first + if model_name in EMBEDDING_MODEL_LIMITS: + limit = EMBEDDING_MODEL_LIMITS[model_name] + _token_limit_cache[cache_key] = limit + return limit + + # Check base name match + if base_model_name in EMBEDDING_MODEL_LIMITS: + limit = EMBEDDING_MODEL_LIMITS[base_model_name] + _token_limit_cache[cache_key] = limit + return limit + + # Check partial matches for common patterns + for known_model, registry_limit in EMBEDDING_MODEL_LIMITS.items(): + if known_model in base_model_name or base_model_name in known_model: + _token_limit_cache[cache_key] = registry_limit + return registry_limit + + # Default fallback + logger.warning(f"Unknown model '{model_name}', using default {default} token limit") + _token_limit_cache[cache_key] = default + return default + + +def truncate_to_token_limit(texts: list[str], token_limit: int) -> list[str]: + """ + Truncate texts to fit within token limit using tiktoken. + + Args: + texts: List of text strings to truncate + token_limit: Maximum number of tokens allowed + + Returns: + List of truncated texts (same length as input) + """ + if not texts: + return [] + + import tiktoken + + # Use tiktoken with cl100k_base encoding + enc = tiktoken.get_encoding("cl100k_base") + + truncated_texts = [] + truncation_count = 0 + total_tokens_removed = 0 + max_original_length = 0 + + for i, text in enumerate(texts): + tokens = enc.encode(text) + original_length = len(tokens) + + if original_length <= token_limit: + # Text is within limit, keep as is + truncated_texts.append(text) + else: + # Truncate to token_limit + truncated_tokens = tokens[:token_limit] + truncated_text = enc.decode(truncated_tokens) + truncated_texts.append(truncated_text) + + # Track truncation statistics + truncation_count += 1 + tokens_removed = original_length - token_limit + total_tokens_removed += tokens_removed + max_original_length = max(max_original_length, original_length) + + # Log individual truncation at WARNING level (first few only) + if truncation_count <= 3: + logger.warning( + f"Text {i + 1} truncated: {original_length} β†’ {token_limit} tokens " + f"({tokens_removed} tokens removed)" + ) + elif truncation_count == 4: + logger.warning("Further truncation warnings suppressed...") + + # Log summary at INFO level + if truncation_count > 0: + logger.warning( + f"Truncation summary: {truncation_count}/{len(texts)} texts truncated " + f"(removed {total_tokens_removed} tokens total, longest was {max_original_length} tokens)" + ) + else: + logger.debug( + f"No truncation needed - all {len(texts)} texts within {token_limit} token limit" + ) + + return truncated_texts + + +def _query_ollama_context_limit(model_name: str, base_url: str) -> Optional[int]: + """ + Query Ollama /api/show for model context limit. + + Args: + model_name: Name of the Ollama model + base_url: Base URL of the Ollama server + + Returns: + Context limit in tokens if found, None otherwise + """ + try: + import requests + + response = requests.post( + f"{base_url}/api/show", + json={"name": model_name}, + timeout=5, + ) + if response.status_code == 200: + data = response.json() + if "model_info" in data: + # Look for *.context_length in model_info + for key, value in data["model_info"].items(): + if "context_length" in key and isinstance(value, int): + logger.info(f"Detected {model_name} context limit: {value} tokens") + return value + except Exception as e: + logger.debug(f"Failed to query Ollama context limit: {e}") + + return None + + +def _query_lmstudio_context_limit(model_name: str, base_url: str) -> Optional[int]: + """ + Query LM Studio SDK for model context length via Node.js subprocess. + + Args: + model_name: Name of the LM Studio model + base_url: Base URL of the LM Studio server (WebSocket format, e.g., "ws://localhost:1234") + + Returns: + Context limit in tokens if found, None otherwise + """ + # Inline JavaScript using @lmstudio/sdk + # Note: Load model temporarily for metadata, then unload to respect JIT auto-evict + js_code = f""" + const {{ LMStudioClient }} = require('@lmstudio/sdk'); + (async () => {{ + try {{ + const client = new LMStudioClient({{ baseUrl: '{base_url}' }}); + const model = await client.embedding.load('{model_name}', {{ verbose: false }}); + const contextLength = await model.getContextLength(); + await model.unload(); // Unload immediately to respect JIT auto-evict settings + console.log(JSON.stringify({{ contextLength, identifier: '{model_name}' }})); + }} catch (error) {{ + console.error(JSON.stringify({{ error: error.message }})); + process.exit(1); + }} + }})(); + """ + + try: + # Set NODE_PATH to include global modules for @lmstudio/sdk resolution + env = os.environ.copy() + + # Try to get npm global root (works with nvm, brew node, etc.) + try: + npm_root = subprocess.run( + ["npm", "root", "-g"], + capture_output=True, + text=True, + timeout=5, + ) + if npm_root.returncode == 0: + global_modules = npm_root.stdout.strip() + # Append to existing NODE_PATH if present + existing_node_path = env.get("NODE_PATH", "") + env["NODE_PATH"] = ( + f"{global_modules}:{existing_node_path}" + if existing_node_path + else global_modules + ) + except Exception: + # If npm not available, continue with existing NODE_PATH + pass + + result = subprocess.run( + ["node", "-e", js_code], + capture_output=True, + text=True, + timeout=10, + env=env, + ) + + if result.returncode != 0: + logger.debug(f"LM Studio SDK error: {result.stderr}") + return None + + data = json.loads(result.stdout) + context_length = data.get("contextLength") + + if context_length and context_length > 0: + logger.info(f"LM Studio SDK detected {model_name} context length: {context_length}") + return context_length + + except FileNotFoundError: + logger.debug("Node.js not found - install Node.js for LM Studio SDK features") + except subprocess.TimeoutExpired: + logger.debug("LM Studio SDK query timeout") + except json.JSONDecodeError: + logger.debug("LM Studio SDK returned invalid JSON") + except Exception as e: + logger.debug(f"LM Studio SDK query failed: {e}") + + return None + + +# Global model cache to avoid repeated loading +_model_cache: dict[str, Any] = {} + +_DEFAULT_CUDA_BATCH_SIZE = 256 +_DEFAULT_MPS_BATCH_SIZE = 128 + + +def _parse_positive_int_env(name: str, default: int) -> int: + raw = os.getenv(name) + if raw is None or raw.strip() == "": + return default + try: + value = int(raw) + except ValueError: + logger.warning("Invalid %s=%r; using default %d", name, raw, default) + return default + if value < 1: + logger.warning("%s must be >= 1; using default %d", name, default) + return default + return value + + +def _resolve_cpu_thread_count() -> int: + return _parse_positive_int_env("LEANN_CPU_THREADS", min(8, os.cpu_count() or 4)) + + +def _resolve_adaptive_batch_size(device: str, model_name: str) -> int: + if device == "cuda": + return _parse_positive_int_env("LEANN_CUDA_BATCH_SIZE", _DEFAULT_CUDA_BATCH_SIZE) + if device == "mps": + default = 32 if model_name == "Qwen/Qwen3-Embedding-0.6B" else _DEFAULT_MPS_BATCH_SIZE + return _parse_positive_int_env("LEANN_MPS_BATCH_SIZE", default) + return 32 + + +def _cap_cuda_batch_by_vram(requested: int, max_length: int = 512) -> int: + auto = os.getenv("LEANN_CUDA_AUTO_BATCH", "1").lower() + if auto in ("0", "false", "no"): + return requested + import torch + + if not torch.cuda.is_available(): + return requested + try: + free_bytes, _total = torch.cuda.mem_get_info() + except Exception: + return requested + + # Eager-attention peak memory scales ~O(seq^2) per sequence in the batch. + bytes_per_seq = max(8_000_000, max_length * max_length * 32) + budget = int(free_bytes * 0.2) + max_by_vram = max(1, budget // bytes_per_seq) + capped = min(requested, max_by_vram) + if capped < requested: + logger.info( + "Capping CUDA embedding batch size %d -> %d (%.2f GiB free VRAM)", + requested, + capped, + free_bytes / (1024**3), + ) + return capped + + +def _encode_with_oom_retry( + model: _SentenceTransformerLike, + texts: list[str], + batch_size: int, + *, + is_build: bool, + device: str, +) -> Any: + import torch + + current = batch_size + while True: + try: + with torch.inference_mode(): + return model.encode( + texts, + batch_size=current, + show_progress_bar=is_build, + convert_to_numpy=True, + normalize_embeddings=False, + device=device, + ) + except RuntimeError as exc: + oom = "out of memory" in str(exc).lower() + if torch.cuda.is_available(): + oom = oom or isinstance(exc, torch.cuda.OutOfMemoryError) + if not oom or current <= 1: + raise + if torch.cuda.is_available(): + torch.cuda.empty_cache() + next_batch = max(1, current // 2) + logger.warning( + "CUDA OOM at embedding batch_size=%d; retrying with batch_size=%d", + current, + next_batch, + ) + current = next_batch + + +def compute_embeddings( + texts: list[str], + model_name: str, + mode: str = "sentence-transformers", + is_build: bool = False, + batch_size: int = 32, + adaptive_optimization: bool = True, + manual_tokenize: bool = False, + max_length: int = 512, + provider_options: Optional[dict[str, Any]] = None, +) -> np.ndarray: + """ + Unified embedding computation entry point + + Args: + texts: List of texts to compute embeddings for + model_name: Model name + mode: Computation mode ('sentence-transformers', 'openai', 'mlx', 'ollama') + is_build: Whether this is a build operation (shows progress bar) + batch_size: Batch size for processing + adaptive_optimization: Whether to use adaptive optimization based on batch size + + Returns: + Normalized embeddings array, shape: (len(texts), embedding_dim) + """ + provider_options = provider_options or {} + wrapper_start_time = time.time() + logger.debug( + f"[compute_embeddings] entry: mode={mode}, model='{model_name}', text_count={len(texts)}" + ) + + # Allow batch_size override from provider_options (disables adaptive_optimization) + if "batch_size" in provider_options: + batch_size = provider_options["batch_size"] + adaptive_optimization = False # User-specified batch_size takes precedence + + if mode == "sentence-transformers": + inner_start_time = time.time() + result = compute_embeddings_sentence_transformers( + texts, + model_name, + is_build=is_build, + batch_size=batch_size, + adaptive_optimization=adaptive_optimization, + manual_tokenize=manual_tokenize, + max_length=max_length, + ) + inner_end_time = time.time() + wrapper_end_time = time.time() + logger.debug( + "[compute_embeddings] sentence-transformers timings: " + f"inner={inner_end_time - inner_start_time:.6f}s, " + f"wrapper_total={wrapper_end_time - wrapper_start_time:.6f}s" + ) + return result + elif mode == "openai": + return compute_embeddings_openai( + texts, + model_name, + base_url=provider_options.get("base_url"), + api_key=provider_options.get("api_key"), + provider_options=provider_options, + is_build=is_build, + ) + elif mode == "mlx": + return compute_embeddings_mlx(texts, model_name, is_build=is_build) + elif mode == "ollama": + return compute_embeddings_ollama( + texts, + model_name, + is_build=is_build, + host=provider_options.get("host"), + provider_options=provider_options, + ) + elif mode == "gemini": + return compute_embeddings_gemini(texts, model_name, is_build=is_build) + else: + raise ValueError(f"Unsupported embedding mode: {mode}") + + +def compute_embeddings_sentence_transformers( + texts: list[str], + model_name: str, + use_fp16: bool = True, + device: str = "auto", + batch_size: int = 32, + is_build: bool = False, + adaptive_optimization: bool = True, + manual_tokenize: bool = False, + max_length: int = 512, +) -> np.ndarray: + """ + Compute embeddings using SentenceTransformer with model caching and adaptive optimization + + Args: + texts: List of texts to compute embeddings for + model_name: Model name + use_fp16: Whether to use FP16 precision + device: Device to use ('auto', 'cuda', 'mps', 'cpu') + batch_size: Batch size for processing + is_build: Whether this is a build operation (shows progress bar) + adaptive_optimization: Whether to use adaptive optimization based on batch size + """ + import torch + + outer_start_time = time.time() + # Handle empty input + if not texts: + raise ValueError("Cannot compute embeddings for empty text list") + logger.info( + f"Computing embeddings for {len(texts)} texts using SentenceTransformer, model: '{model_name}'" + ) + + # Auto-detect device + if device == "auto": + # Check environment variable first + env_device = os.getenv("LEANN_EMBEDDING_DEVICE") + if env_device: + device = env_device + logger.info(f"Using device from LEANN_EMBEDDING_DEVICE: {device}") + elif torch.cuda.is_available(): + device = "cuda" + elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + device = "mps" + else: + device = "cpu" + + # Apply optimizations based on benchmark results + if adaptive_optimization: + batch_size = _resolve_adaptive_batch_size(device, model_name) + + # Create cache key + cache_key = f"sentence_transformers_{model_name}_{device}_{use_fp16}_optimized" + + pre_model_init_end_time = time.time() + logger.debug( + "compute_embeddings_sentence_transformers pre-model-init time " + f"(device/batch selection etc.): {pre_model_init_end_time - outer_start_time:.6f}s" + ) + + # Check if model is already cached + start_time = time.time() + if cache_key in _model_cache: + logger.info(f"Using cached optimized model: {model_name}") + model = cast(_SentenceTransformerLike, _model_cache[cache_key]) + else: + logger.info(f"Loading and caching optimized SentenceTransformer model: {model_name}") + from sentence_transformers import SentenceTransformer + + logger.info(f"Using device: {device}") + + # Apply hardware optimizations + if device == "cuda": + # TODO: Haven't tested this yet + torch.backends.cuda.matmul.allow_tf32 = True + torch.backends.cudnn.allow_tf32 = True + torch.backends.cudnn.benchmark = True + torch.backends.cudnn.deterministic = False + torch.cuda.set_per_process_memory_fraction(0.9) + elif device == "mps": + # No device-level init for MPS. set_per_process_memory_fraction causes + # greedy allocation; torch.compile causes graph buffer bloat. Cache + # clearing is handled per-batch in the compute loop below. + pass + elif device == "cpu": + # TODO: Haven't tested this yet + torch.set_num_threads(_resolve_cpu_thread_count()) + try: + torch.backends.mkldnn.enabled = True + except AttributeError: + pass + + # Prepare optimized model and tokenizer parameters + model_kwargs = { + "torch_dtype": torch.float16 if use_fp16 else torch.float32, + "low_cpu_mem_usage": True, + "_fast_init": True, + "attn_implementation": "eager", # Use eager attention for speed + } + + tokenizer_kwargs = { + "use_fast": True, + "padding": True, + "truncation": True, + } + + try: + # Try loading with advanced parameters first (newer versions) + local_model_kwargs = model_kwargs.copy() + local_tokenizer_kwargs = tokenizer_kwargs.copy() + local_model_kwargs["local_files_only"] = True + local_tokenizer_kwargs["local_files_only"] = True + + model = SentenceTransformer( + model_name, + device=device, + model_kwargs=local_model_kwargs, + tokenizer_kwargs=local_tokenizer_kwargs, + local_files_only=True, + ) + logger.info("Model loaded successfully! (local + optimized)") + except TypeError as e: + if "model_kwargs" in str(e) or "tokenizer_kwargs" in str(e): + logger.warning( + f"Advanced parameters not supported ({e}), using basic initialization..." + ) + # Fallback to basic initialization for older versions + try: + model = SentenceTransformer( + model_name, + device=device, + local_files_only=True, + ) + logger.info("Model loaded successfully! (local + basic)") + except Exception as e2: + logger.warning(f"Local loading failed ({e2}), trying network download...") + model = SentenceTransformer( + model_name, + device=device, + local_files_only=False, + ) + logger.info("Model loaded successfully! (network + basic)") + else: + raise + except Exception as e: + logger.warning(f"Local loading failed ({e}), trying network download...") + # Fallback to network loading with advanced parameters + try: + network_model_kwargs = model_kwargs.copy() + network_tokenizer_kwargs = tokenizer_kwargs.copy() + network_model_kwargs["local_files_only"] = False + network_tokenizer_kwargs["local_files_only"] = False + + model = SentenceTransformer( + model_name, + device=device, + model_kwargs=network_model_kwargs, + tokenizer_kwargs=network_tokenizer_kwargs, + local_files_only=False, + ) + logger.info("Model loaded successfully! (network + optimized)") + except TypeError as e2: + if "model_kwargs" in str(e2) or "tokenizer_kwargs" in str(e2): + logger.warning( + f"Advanced parameters not supported ({e2}), using basic network loading..." + ) + model = SentenceTransformer( + model_name, + device=device, + local_files_only=False, + ) + logger.info("Model loaded successfully! (network + basic)") + else: + raise + + # Apply additional optimizations based on mode + if use_fp16 and device in ["cuda", "mps"]: + try: + model = model.half() + logger.info(f"Applied FP16 precision: {model_name}") + except Exception as e: + logger.warning(f"FP16 optimization failed: {e}") + + # Apply torch.compile optimization + if device == "cuda": + try: + model = torch.compile(model, mode="reduce-overhead", dynamic=True) + logger.info(f"Applied torch.compile optimization: {model_name}") + except Exception as e: + logger.warning(f"torch.compile optimization failed: {e}") + + model = cast(_SentenceTransformerLike, model) + + # Set model to eval mode and disable gradients for inference + model.eval() + for param in model.parameters(): + param.requires_grad_(False) + + # Cache the model + _model_cache[cache_key] = model + logger.info(f"Model cached: {cache_key}") + + end_time = time.time() + + # Compute embeddings with optimized inference mode + logger.info( + f"Starting embedding computation... (batch_size: {batch_size}, manual_tokenize={manual_tokenize})" + ) + logger.info(f"start sentence transformers {model} takes {end_time - start_time}") + + if device == "cuda" and adaptive_optimization: + batch_size = _cap_cuda_batch_by_vram(batch_size, max_length=max_length) + + start_time = time.time() + if not manual_tokenize: + # Use SentenceTransformer's optimized encode path (default) + embeddings = _encode_with_oom_retry( + model, + texts, + batch_size, + is_build=is_build, + device=device, + ) + # Synchronize if CUDA to measure accurate wall time + try: + if torch.cuda.is_available(): + torch.cuda.synchronize() + except Exception: + pass + else: + # Manual tokenization + forward pass using HF AutoTokenizer/AutoModel. + # This path is reserved for an aggressively optimized FP pipeline + # (no quantization), mainly for experimentation. + try: + from transformers import AutoModel, AutoTokenizer + except Exception as e: + raise ImportError(f"transformers is required for manual_tokenize=True: {e}") + + tok_cache_key = f"hf_tokenizer_{model_name}" + mdl_cache_key = f"hf_model_{model_name}_{device}_{use_fp16}_fp" + + if tok_cache_key in _model_cache and mdl_cache_key in _model_cache: + hf_tokenizer = _model_cache[tok_cache_key] + hf_model = _model_cache[mdl_cache_key] + logger.info("Using cached HF tokenizer/model for manual FP path") + else: + logger.info("Loading HF tokenizer/model for manual FP path") + hf_tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True) + + torch_dtype = torch.float16 if (use_fp16 and device == "cuda") else torch.float32 + hf_model = AutoModel.from_pretrained( + model_name, + torch_dtype=torch_dtype, + ) + hf_model.to(device) + + hf_model.eval() + # Optional compile on supported devices + if device == "cuda": + try: + hf_model = torch.compile(hf_model, mode="reduce-overhead", dynamic=True) + logger.info( + f"Applied torch.compile to HF model for {model_name} " + f"(device={device}, dtype={torch_dtype})" + ) + except Exception as exc: + logger.warning(f"torch.compile optimization failed: {exc}") + + _model_cache[tok_cache_key] = hf_tokenizer + _model_cache[mdl_cache_key] = hf_model + + all_embeddings: list[np.ndarray] = [] + # Progress bar when building or for large inputs + show_progress = is_build or len(texts) > 32 + try: + if show_progress: + from tqdm import tqdm + + batch_iter = tqdm( + range(0, len(texts), batch_size), + desc="Embedding (manual)", + unit="batch", + ) + else: + batch_iter = range(0, len(texts), batch_size) + except Exception: + batch_iter = range(0, len(texts), batch_size) + + start_time_manual = time.time() + with torch.inference_mode(): + for start_index in batch_iter: + end_index = min(start_index + batch_size, len(texts)) + batch_texts = texts[start_index:end_index] + inputs = hf_tokenizer( + batch_texts, + padding=True, + truncation=True, + max_length=max_length, + return_tensors="pt", + ) + inputs = {k: v.to(device) for k, v in inputs.items()} + outputs = hf_model(**inputs) + last_hidden_state = outputs.last_hidden_state # (B, L, H) + attention_mask = inputs.get("attention_mask") + if attention_mask is None: + pooled = last_hidden_state.mean(dim=1) + else: + mask = attention_mask.unsqueeze(-1).to(last_hidden_state.dtype) + masked = last_hidden_state * mask + lengths = mask.sum(dim=1).clamp(min=1) + pooled = masked.sum(dim=1) / lengths + batch_embeddings = pooled.detach().to("cpu").float().numpy() + all_embeddings.append(batch_embeddings) + if device == "mps": + try: + torch.mps.empty_cache() + except (RuntimeError, AttributeError): + pass + + embeddings = np.vstack(all_embeddings).astype(np.float32, copy=False) + try: + if torch.cuda.is_available(): + torch.cuda.synchronize() + except Exception: + pass + end_time = time.time() + logger.info(f"Manual tokenize time taken: {end_time - start_time_manual} seconds") + end_time = time.time() + logger.info(f"Generated {len(embeddings)} embeddings, dimension: {embeddings.shape[1]}") + logger.info(f"Time taken: {end_time - start_time} seconds") + + # Validate results + if np.isnan(embeddings).any() or np.isinf(embeddings).any(): + raise RuntimeError(f"Detected NaN or Inf values in embeddings, model: {model_name}") + + outer_end_time = time.time() + logger.debug( + "compute_embeddings_sentence_transformers total time " + f"(function entry -> return): {outer_end_time - outer_start_time:.6f}s" + ) + + return embeddings + + +def compute_embeddings_openai( + texts: list[str], + model_name: str, + base_url: Optional[str] = None, + api_key: Optional[str] = None, + provider_options: Optional[dict[str, Any]] = None, + is_build: bool = False, +) -> np.ndarray: + """Compute embeddings using OpenAI API""" + try: + import openai + except ImportError as e: + raise ImportError(f"OpenAI package not installed: {e}") + + # Validate input list + if not texts: + raise ValueError("Cannot compute embeddings for empty text list") + # Extra validation: abort early if any item is empty/whitespace + invalid_count = sum(1 for t in texts if not isinstance(t, str) or not t.strip()) + if invalid_count > 0: + raise ValueError( + f"Found {invalid_count} empty/invalid text(s) in input. Upstream should filter before calling OpenAI." + ) + + # Extract base_url and api_key from provider_options if not provided directly + provider_options = provider_options or {} + effective_base_url = base_url or provider_options.get("base_url") + effective_api_key = api_key or provider_options.get("api_key") + + resolved_base_url = resolve_openai_base_url(effective_base_url) + resolved_api_key = resolve_openai_api_key(effective_api_key) + + if not resolved_api_key: + raise RuntimeError("OPENAI_API_KEY environment variable not set") + + # Create OpenAI client + client = openai.OpenAI(api_key=resolved_api_key, base_url=resolved_base_url) + + logger.info( + f"Computing embeddings for {len(texts)} texts using OpenAI API, model: '{model_name}'" + ) + + # Apply prompt template if provided + # Priority: build_prompt_template (new format) > prompt_template (old format) + prompt_template = provider_options.get("build_prompt_template") or provider_options.get( + "prompt_template" + ) + + if prompt_template: + logger.warning(f"Applying prompt template: '{prompt_template}'") + texts = [f"{prompt_template}{text}" for text in texts] + + # Query token limit and apply truncation + token_limit = get_model_token_limit(model_name, base_url=effective_base_url) + logger.info(f"Using token limit: {token_limit} for model '{model_name}'") + texts = truncate_to_token_limit(texts, token_limit) + + # OpenAI has limits on batch size and input length + max_batch_size = 800 # Conservative batch size because the token limit is 300K + all_embeddings = [] + # get the avg len of texts + avg_len = sum(len(text) for text in texts) / len(texts) + # if avg len is less than 1000, use the max batch size + if avg_len > 300: + max_batch_size = 500 + + # Gemini's OpenAI-compatible endpoint hard-limits embedding batches to 100 inputs per request. + # If we exceed this, the API returns: + # "BatchEmbedContentsRequest.requests: at most 100 requests can be in one batch" + if "generativelanguage.googleapis.com" in (resolved_base_url or ""): + max_batch_size = min(max_batch_size, 100) + logger.info( + "Detected Gemini OpenAI-compatible base_url; capping embedding batch_size to %d.", + max_batch_size, + ) + + # Alibaba Cloud DashScope's OpenAI-compatible endpoint hard-limits embedding batches + # to 10 inputs per request (e.g. text-embedding-v4). Exceeding this returns HTTP 400: + # "InternalError.Algo.InvalidParameter: Value error, batch size is invalid, + # it should not be larger than 10.: input.contents" + if "dashscope" in (resolved_base_url or ""): + max_batch_size = min(max_batch_size, 10) + logger.info( + "Detected DashScope OpenAI-compatible base_url; capping embedding batch_size to %d.", + max_batch_size, + ) + + # if avg len is less than 1000, use the max batch size + + try: + from tqdm import tqdm + + total_batches = (len(texts) + max_batch_size - 1) // max_batch_size + batch_range = range(0, len(texts), max_batch_size) + if is_build: + batch_iterator = tqdm( + batch_range, desc="Computing embeddings", unit="batch", total=total_batches + ) + else: + batch_iterator = batch_range + except ImportError: + # Fallback when tqdm is not available + batch_iterator = range(0, len(texts), max_batch_size) + + for i in batch_iterator: + batch_texts = texts[i : i + max_batch_size] + + try: + response = client.embeddings.create(model=model_name, input=batch_texts) + batch_embeddings = [embedding.embedding for embedding in response.data] + + # Verify we got the expected number of embeddings + if len(batch_embeddings) != len(batch_texts): + logger.warning( + f"Expected {len(batch_texts)} embeddings but got {len(batch_embeddings)}" + ) + + # Only take the number of embeddings that match the batch size + all_embeddings.extend(batch_embeddings[: len(batch_texts)]) + except Exception as e: + logger.error(f"Batch {i} failed: {e}") + raise + + embeddings = np.array(all_embeddings, dtype=np.float32) + logger.info(f"Generated {len(embeddings)} embeddings, dimension: {embeddings.shape[1]}") + return embeddings + + +def compute_embeddings_mlx( + chunks: list[str], model_name: str, batch_size: int = 16, is_build: bool = False +) -> np.ndarray: + """Computes embeddings using an MLX model.""" + try: + import mlx.core as mx + from mlx_lm.utils import load + except ImportError as e: + raise RuntimeError( + "MLX or related libraries not available. Install with: uv pip install mlx mlx-lm" + ) from e + + logger.info( + f"Computing embeddings for {len(chunks)} chunks using MLX model '{model_name}' with batch_size={batch_size}..." + ) + + # Cache MLX model and tokenizer + cache_key = f"mlx_{model_name}" + if cache_key in _model_cache: + logger.info(f"Using cached MLX model: {model_name}") + model, tokenizer = _model_cache[cache_key] + else: + logger.info(f"Loading and caching MLX model: {model_name}") + model, tokenizer = load(model_name) + _model_cache[cache_key] = (model, tokenizer) + logger.info(f"MLX model cached: {cache_key}") + + # Process chunks in batches with progress bar + all_embeddings = [] + + try: + from tqdm import tqdm + + batch_range = range(0, len(chunks), batch_size) + if is_build: + batch_iterator = tqdm(batch_range, desc="Computing embeddings", unit="batch") + else: + batch_iterator = batch_range + except ImportError: + batch_iterator = range(0, len(chunks), batch_size) + + for i in batch_iterator: + batch_chunks = chunks[i : i + batch_size] + + # Tokenize all chunks in the batch + batch_token_ids = [] + for chunk in batch_chunks: + token_ids = tokenizer.encode(chunk) + batch_token_ids.append(token_ids) + + # Pad sequences to the same length for batch processing + max_length = max(len(ids) for ids in batch_token_ids) + padded_token_ids = [] + for token_ids in batch_token_ids: + # Pad with tokenizer.pad_token_id or 0 + padded = token_ids + [0] * (max_length - len(token_ids)) + padded_token_ids.append(padded) + + # Convert to MLX array with batch dimension + input_ids = mx.array(padded_token_ids) + + # Get embeddings for the batch + embeddings = model(input_ids) + + # Mean pooling for each sequence in the batch + pooled = embeddings.mean(axis=1) # Shape: (batch_size, hidden_size) + + # Convert batch embeddings to numpy + for j in range(len(batch_chunks)): + pooled_list = pooled[j].tolist() # Convert to list + pooled_numpy = np.array(pooled_list, dtype=np.float32) + all_embeddings.append(pooled_numpy) + + # Stack numpy arrays + return np.stack(all_embeddings) + + +def compute_embeddings_ollama( + texts: list[str], + model_name: str, + is_build: bool = False, + host: Optional[str] = None, + provider_options: Optional[dict[str, Any]] = None, +) -> np.ndarray: + """ + Compute embeddings using Ollama API with true batch processing. + + Uses the /api/embed endpoint which supports batch inputs. + Batch size: 32 for MPS/CPU, 128 for CUDA to optimize performance. + + Args: + texts: List of texts to compute embeddings for + model_name: Ollama model name (e.g., "nomic-embed-text", "mxbai-embed-large") + is_build: Whether this is a build operation (shows progress bar) + host: Ollama host URL (defaults to environment or http://localhost:11434) + provider_options: Optional provider-specific options (e.g., prompt_template) + + Returns: + Normalized embeddings array, shape: (len(texts), embedding_dim) + """ + try: + import requests + except ImportError: + raise ImportError( + "The 'requests' library is required for Ollama embeddings. Install with: uv pip install requests" + ) + + if not texts: + raise ValueError("Cannot compute embeddings for empty text list") + + resolved_host = resolve_ollama_host(host) + + logger.info( + f"Computing embeddings for {len(texts)} texts using Ollama API, model: '{model_name}', host: '{resolved_host}'" + ) + + # Check if Ollama is running + try: + response = requests.get(f"{resolved_host}/api/version", timeout=5) + response.raise_for_status() + except requests.exceptions.ConnectionError: + error_msg = ( + f"❌ Could not connect to Ollama at {resolved_host}.\n\n" + "Please ensure Ollama is running:\n" + " β€’ macOS/Linux: ollama serve\n" + " β€’ Windows: Make sure Ollama is running in the system tray\n\n" + "Installation: https://ollama.com/download" + ) + raise RuntimeError(error_msg) + except Exception as e: + raise RuntimeError(f"Unexpected error connecting to Ollama: {e}") + + # Check if model exists and provide helpful suggestions + try: + response = requests.get(f"{resolved_host}/api/tags", timeout=5) + response.raise_for_status() + models = response.json() + model_names = [model["name"] for model in models.get("models", [])] + + # Filter for embedding models (models that support embeddings) + embedding_models = [] + suggested_embedding_models = [ + "nomic-embed-text", + "mxbai-embed-large", + "bge-m3", + "all-minilm", + "snowflake-arctic-embed", + ] + + for model in model_names: + # Check if it's an embedding model (by name patterns or known models) + base_name = model.split(":")[0] + if any(emb in base_name for emb in ["embed", "bge", "minilm", "e5"]): + embedding_models.append(model) + + # Check if model exists (handle versioned names) and resolve to full name + resolved_model_name = None + for name in model_names: + # Exact match + if model_name == name: + resolved_model_name = name + break + # Match without version tag (use the versioned name) + elif model_name == name.split(":")[0]: + resolved_model_name = name + break + + if not resolved_model_name: + error_msg = f"❌ Model '{model_name}' not found in local Ollama.\n\n" + + # Suggest pulling the model + error_msg += "πŸ“¦ To install this embedding model:\n" + error_msg += f" ollama pull {model_name}\n\n" + + # Show available embedding models + if embedding_models: + error_msg += "βœ… Available embedding models:\n" + for model in embedding_models[:5]: + error_msg += f" β€’ {model}\n" + if len(embedding_models) > 5: + error_msg += f" ... and {len(embedding_models) - 5} more\n" + else: + error_msg += "πŸ’‘ Popular embedding models to install:\n" + for model in suggested_embedding_models[:3]: + error_msg += f" β€’ ollama pull {model}\n" + + error_msg += "\nπŸ“š Browse more: https://ollama.com/library" + raise ValueError(error_msg) + + # Use the resolved model name for all subsequent operations + if resolved_model_name != model_name: + logger.info(f"Resolved model name '{model_name}' to '{resolved_model_name}'") + model_name = resolved_model_name + + # Verify the model supports embeddings by testing it with /api/embed + try: + test_response = requests.post( + f"{resolved_host}/api/embed", + json={"model": model_name, "input": "test"}, + timeout=10, + ) + if test_response.status_code != 200: + error_msg = ( + f"⚠️ Model '{model_name}' exists but may not support embeddings.\n\n" + f"Please use an embedding model like:\n" + ) + for model in suggested_embedding_models[:3]: + error_msg += f" β€’ {model}\n" + raise ValueError(error_msg) + except requests.exceptions.RequestException: + # If test fails, continue anyway - model might still work + pass + + except requests.exceptions.RequestException as e: + logger.warning(f"Could not verify model existence: {e}") + + # Determine batch size based on device availability + # Check for CUDA/MPS availability using torch if available + batch_size = 32 # Default for MPS/CPU + try: + import torch + + if torch.cuda.is_available(): + batch_size = 128 # CUDA gets larger batch size + elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + batch_size = 32 # MPS gets smaller batch size + except ImportError: + # If torch is not available, use conservative batch size + batch_size = 32 + + logger.info(f"Using batch size: {batch_size} for true batch processing") + + # Apply prompt template if provided + provider_options = provider_options or {} + # Priority: build_prompt_template (new format) > prompt_template (old format) + prompt_template = provider_options.get("build_prompt_template") or provider_options.get( + "prompt_template" + ) + + if prompt_template: + logger.warning(f"Applying prompt template: '{prompt_template}'") + texts = [f"{prompt_template}{text}" for text in texts] + + # Get model token limit and apply truncation before batching + token_limit = get_model_token_limit(model_name, base_url=resolved_host) + logger.info(f"Model '{model_name}' token limit: {token_limit}") + + # Apply truncation to all texts before batch processing + # Function logs truncation details internally + texts = truncate_to_token_limit(texts, token_limit) + + def get_batch_embeddings(batch_texts): + """Get embeddings for a batch of texts using /api/embed endpoint.""" + max_retries = 3 + retry_count = 0 + + # Texts are already truncated to token limit by the outer function + while retry_count < max_retries: + try: + # Use /api/embed endpoint with "input" parameter for batch processing + response = requests.post( + f"{resolved_host}/api/embed", + json={"model": model_name, "input": batch_texts}, + timeout=60, # Increased timeout for batch processing + ) + response.raise_for_status() + + result = response.json() + batch_embeddings = result.get("embeddings") + + if batch_embeddings is None: + raise ValueError("No embeddings returned from API") + + if not isinstance(batch_embeddings, list): + raise ValueError(f"Invalid embeddings format: {type(batch_embeddings)}") + + if len(batch_embeddings) != len(batch_texts): + raise ValueError( + f"Mismatch: requested {len(batch_texts)} embeddings, got {len(batch_embeddings)}" + ) + + return batch_embeddings, [] + + except requests.exceptions.Timeout: + retry_count += 1 + if retry_count >= max_retries: + logger.warning(f"Timeout for batch after {max_retries} retries") + return None, list(range(len(batch_texts))) + + except Exception as e: + retry_count += 1 + if retry_count >= max_retries: + # Enhanced error detection for token limit violations + error_msg = str(e).lower() + if "token" in error_msg and ( + "limit" in error_msg or "exceed" in error_msg or "length" in error_msg + ): + logger.error( + f"Token limit exceeded for batch. Error: {e}. " + f"Consider reducing chunk sizes or check token truncation." + ) + else: + logger.error(f"Failed to get embeddings for batch: {e}") + return None, list(range(len(batch_texts))) + + return None, list(range(len(batch_texts))) + + # Process texts in batches + all_embeddings = [] + all_failed_indices = [] + + # Setup progress bar if needed + show_progress = is_build or len(texts) > 10 + try: + if show_progress: + from tqdm import tqdm + except ImportError: + show_progress = False + + # Process batches + num_batches = (len(texts) + batch_size - 1) // batch_size + + if show_progress: + batch_iterator = tqdm(range(num_batches), desc="Computing Ollama embeddings (batched)") + else: + batch_iterator = range(num_batches) + + for batch_idx in batch_iterator: + start_idx = batch_idx * batch_size + end_idx = min(start_idx + batch_size, len(texts)) + batch_texts = texts[start_idx:end_idx] + + batch_embeddings, batch_failed = get_batch_embeddings(batch_texts) + + if batch_embeddings is not None: + all_embeddings.extend(batch_embeddings) + else: + # Entire batch failed, add None placeholders + all_embeddings.extend([None] * len(batch_texts)) + # Adjust failed indices to global indices + global_failed = [start_idx + idx for idx in batch_failed] + all_failed_indices.extend(global_failed) + + # Handle failed embeddings + if all_failed_indices: + if len(all_failed_indices) == len(texts): + raise RuntimeError("Failed to compute any embeddings") + + logger.warning( + f"Failed to compute embeddings for {len(all_failed_indices)}/{len(texts)} texts" + ) + + # Use zero embeddings as fallback for failed ones + valid_embedding = next((e for e in all_embeddings if e is not None), None) + if valid_embedding: + embedding_dim = len(valid_embedding) + for i, embedding in enumerate(all_embeddings): + if embedding is None: + all_embeddings[i] = [0.0] * embedding_dim + + # Remove None values + all_embeddings = [e for e in all_embeddings if e is not None] + + if not all_embeddings: + raise RuntimeError("No valid embeddings were computed") + + # Validate embedding dimensions + expected_dim = len(all_embeddings[0]) + inconsistent_dims = [] + for i, embedding in enumerate(all_embeddings): + if len(embedding) != expected_dim: + inconsistent_dims.append((i, len(embedding))) + + if inconsistent_dims: + error_msg = f"Ollama returned inconsistent embedding dimensions. Expected {expected_dim}, but got:\n" + for idx, dim in inconsistent_dims[:10]: # Show first 10 inconsistent ones + error_msg += f" - Text {idx}: {dim} dimensions\n" + if len(inconsistent_dims) > 10: + error_msg += f" ... and {len(inconsistent_dims) - 10} more\n" + error_msg += f"\nThis is likely an Ollama API bug with model '{model_name}'. Please try:\n" + error_msg += "1. Restart Ollama service: 'ollama serve'\n" + error_msg += f"2. Re-pull the model: 'ollama pull {model_name}'\n" + error_msg += ( + "3. Use sentence-transformers instead: --embedding-mode sentence-transformers\n" + ) + error_msg += "4. Report this issue to Ollama: https://github.com/ollama/ollama/issues" + raise ValueError(error_msg) + + # Convert to numpy array and normalize + embeddings = np.array(all_embeddings, dtype=np.float32) + + # Normalize embeddings (L2 normalization) + norms = np.linalg.norm(embeddings, axis=1, keepdims=True) + embeddings = embeddings / (norms + 1e-8) # Add small epsilon to avoid division by zero + + logger.info(f"Generated {len(embeddings)} embeddings, dimension: {embeddings.shape[1]}") + + return embeddings + + +def compute_embeddings_gemini( + texts: list[str], model_name: str = "text-embedding-004", is_build: bool = False +) -> np.ndarray: + """ + Compute embeddings using Google Gemini API. + + Args: + texts: List of texts to compute embeddings for + model_name: Gemini model name (default: "text-embedding-004") + is_build: Whether this is a build operation (shows progress bar) + + Returns: + Embeddings array, shape: (len(texts), embedding_dim) + """ + try: + import os + + import google.genai as genai + except ImportError as e: + raise ImportError(f"Google GenAI package not installed: {e}") + + api_key = os.getenv("GEMINI_API_KEY") + if not api_key: + raise RuntimeError("GEMINI_API_KEY environment variable not set") + + # Cache Gemini client + cache_key = "gemini_client" + if cache_key in _model_cache: + client = _model_cache[cache_key] + else: + client = genai.Client(api_key=api_key) + _model_cache[cache_key] = client + logger.info("Gemini client cached") + + logger.info( + f"Computing embeddings for {len(texts)} texts using Gemini API, model: '{model_name}'" + ) + + # Gemini supports batch embedding + max_batch_size = 100 # Conservative batch size for Gemini + all_embeddings = [] + + try: + from tqdm import tqdm + + total_batches = (len(texts) + max_batch_size - 1) // max_batch_size + batch_range = range(0, len(texts), max_batch_size) + batch_iterator = tqdm( + batch_range, desc="Computing embeddings", unit="batch", total=total_batches + ) + except ImportError: + # Fallback when tqdm is not available + batch_iterator = range(0, len(texts), max_batch_size) + + for i in batch_iterator: + batch_texts = texts[i : i + max_batch_size] + + try: + # Use the embed_content method from the new Google GenAI SDK + response = client.models.embed_content( + model=model_name, + contents=batch_texts, + config=genai.types.EmbedContentConfig( + task_type="RETRIEVAL_DOCUMENT" # For document embedding + ), + ) + + # Extract embeddings from response + for embedding_data in response.embeddings: + all_embeddings.append(embedding_data.values) + except Exception as e: + logger.error(f"Batch {i} failed: {e}") + raise + + embeddings = np.array(all_embeddings, dtype=np.float32) + logger.info(f"Generated {len(embeddings)} embeddings, dimension: {embeddings.shape[1]}") + + return embeddings diff --git a/packages/leann-core/src/leann/embedding_server_manager.py b/packages/leann-core/src/leann/embedding_server_manager.py new file mode 100644 index 0000000..b00f0ca --- /dev/null +++ b/packages/leann-core/src/leann/embedding_server_manager.py @@ -0,0 +1,908 @@ +import atexit +import contextlib +import hashlib +import json +import logging +import os +import socket +import subprocess +import sys +import threading +import time +from pathlib import Path +from typing import Any, Optional + +from .settings import encode_provider_options + +# Lightweight, self-contained server manager with no cross-process inspection + +# Set up logging based on environment variable +LOG_LEVEL = os.getenv("LEANN_LOG_LEVEL", "WARNING").upper() +logging.basicConfig( + level=getattr(logging, LOG_LEVEL, logging.INFO), + format="%(levelname)s - %(name)s - %(message)s", +) +logger = logging.getLogger(__name__) +_LOCK_STALE_SECONDS = 600 +_FLOCK_TIMEOUT_SECONDS = 300 +_REGISTRY_LOCKS_GUARD = threading.Lock() +_REGISTRY_LOCKS: dict[str, threading.Lock] = {} + + +def _flock_acquire(lock_file) -> None: # type: ignore[type-arg] + """Acquire an exclusive file lock for cross-process synchronisation. + + Uses ``fcntl.flock`` on POSIX and ``msvcrt.locking`` on Windows. Both are + auto-released when the file descriptor is closed or the owning process + exits, so a holder crash does not permanently block other waiters. + """ + try: + import fcntl + + fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX) + return + except ImportError: + pass + except OSError: + return + + if sys.platform != "win32": + return + import msvcrt + + # msvcrt.locking operates on byte ranges; ensure the file has content. + lock_file.seek(0, 2) + if lock_file.tell() == 0: + lock_file.write("\n") + lock_file.flush() + lock_file.seek(0) + + deadline = time.monotonic() + _FLOCK_TIMEOUT_SECONDS + while time.monotonic() < deadline: + try: + msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1) + return + except OSError: + time.sleep(0.5) + logger.warning( + "Cross-process file lock timed out after %ds; proceeding without lock", + _FLOCK_TIMEOUT_SECONDS, + ) + + +def _flock_release(lock_file) -> None: # type: ignore[type-arg] + """Release the file lock acquired by :func:`_flock_acquire`.""" + try: + import fcntl + + fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) + return + except ImportError: + pass + except OSError: + return + + if sys.platform != "win32": + return + try: + import msvcrt + + lock_file.seek(0) + msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1) + except (ImportError, OSError): + pass + + +def _is_colab_environment() -> bool: + """Check if we're running in Google Colab environment.""" + return "COLAB_GPU" in os.environ or "COLAB_TPU" in os.environ + + +def _get_available_port(start_port: int = 5557) -> int: + """Get an available port starting from start_port.""" + port = start_port + while port < start_port + 100: # Try up to 100 ports + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("localhost", port)) + return port + except OSError: + port += 1 + raise RuntimeError(f"No available ports found in range {start_port}-{start_port + 100}") + + +def _check_port(port: int) -> bool: + """Check if a port is in use""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(("localhost", port)) == 0 + + +def _pid_is_alive(pid: int) -> bool: + """Best-effort liveness check for a process id.""" + if pid <= 0: + return False + try: + os.kill(pid, 0) + return True + except OSError: + return False + + +# Note: All cross-process scanning helpers removed for simplicity + + +def _safe_resolve(path: Path) -> str: + """Resolve paths safely even if the target does not yet exist.""" + try: + return str(path.resolve(strict=False)) + except Exception: + return str(path) + + +def _safe_stat_signature(path: Path) -> dict: + """Return a lightweight signature describing the current state of a path.""" + signature: dict[str, object] = {"path": _safe_resolve(path)} + try: + stat = path.stat() + except FileNotFoundError: + signature["missing"] = True + except Exception as exc: # pragma: no cover - unexpected filesystem errors + signature["error"] = str(exc) + else: + signature["mtime_ns"] = stat.st_mtime_ns + signature["size"] = stat.st_size + return signature + + +def _build_passages_signature(passages_file: Optional[str]) -> Optional[dict]: + """Collect modification signatures for metadata and referenced passage files.""" + if not passages_file: + return None + + meta_path = Path(passages_file) + signature: dict[str, object] = {"meta": _safe_stat_signature(meta_path)} + + try: + with meta_path.open(encoding="utf-8") as fh: + meta = json.load(fh) + except FileNotFoundError: + signature["meta_missing"] = True + signature["sources"] = [] + return signature + except json.JSONDecodeError as exc: + signature["meta_error"] = f"json_error:{exc}" + signature["sources"] = [] + return signature + except Exception as exc: # pragma: no cover - unexpected errors + signature["meta_error"] = str(exc) + signature["sources"] = [] + return signature + + base_dir = meta_path.parent + seen_paths: set[str] = set() + source_signatures: list[dict[str, object]] = [] + + for source in meta.get("passage_sources", []): + for key, kind in ( + ("path", "passages"), + ("path_relative", "passages"), + ("index_path", "index"), + ("index_path_relative", "index"), + ): + raw_path = source.get(key) + if not raw_path: + continue + candidate = Path(raw_path) + if not candidate.is_absolute(): + candidate = base_dir / candidate + resolved = _safe_resolve(candidate) + if resolved in seen_paths: + continue + seen_paths.add(resolved) + sig = _safe_stat_signature(candidate) + sig["kind"] = kind + source_signatures.append(sig) + + signature["sources"] = source_signatures + return signature + + +# Note: All cross-process scanning helpers removed for simplicity + + +class EmbeddingServerManager: + """ + A simplified manager for embedding server processes that avoids complex update mechanisms. + """ + + def __init__(self, backend_module_name: str): + """ + Initializes the manager for a specific backend. + + Args: + backend_module_name (str): The full module name of the backend's server script. + e.g., "leann_backend_diskann.embedding_server" + """ + self.backend_module_name = backend_module_name + self.server_process: Optional[subprocess.Popen] = None + self.server_port: Optional[int] = None + # Track last-started config for in-process reuse only + self._server_config: Optional[dict] = None + self._daemon_mode = False + self._registry_path: Optional[Path] = None + self._atexit_registered = False + # Also register a weakref finalizer to ensure cleanup when manager is GC'ed + try: + import weakref + + self._finalizer = weakref.finalize(self, self._finalize_process) + except Exception: + self._finalizer = None + + def start_server( + self, + port: int, + model_name: str, + embedding_mode: str = "sentence-transformers", + **kwargs, + ) -> tuple[bool, int]: + """Start the embedding server.""" + # passages_file may be present in kwargs for server CLI, but we don't need it here + provider_options = kwargs.pop("provider_options", None) + passages_file = kwargs.get("passages_file", "") + distance_metric = kwargs.get("distance_metric", "") + use_daemon = bool(kwargs.get("use_daemon", True)) + daemon_ttl_seconds = int(kwargs.get("daemon_ttl_seconds", 900)) + + config_signature = self._build_config_signature( + model_name=model_name, + embedding_mode=embedding_mode, + provider_options=provider_options, + passages_file=passages_file, + distance_metric=distance_metric, + ) + + # If this manager already has a live server, just reuse it + if ( + self.server_process + and self.server_process.poll() is None + and self.server_port + and self._server_config == config_signature + ): + logger.info("Reusing in-process server") + return True, self.server_port + + # Configuration changed, stop existing ephemeral server before starting a new one + if self.server_process and self.server_process.poll() is None and not self._daemon_mode: + logger.info("Existing server configuration differs; restarting embedding server") + self.stop_server() + + # Reuse an already-running daemon from registry if possible. + if use_daemon and not _is_colab_environment(): + with self._registry_lock(config_signature): + adopted = self._adopt_registered_server(config_signature) + if adopted is not None: + self.server_process = None + self.server_port = adopted + self._server_config = config_signature + self._daemon_mode = True + return True, adopted + + # For Colab environment, use a different strategy + if _is_colab_environment(): + logger.info("Detected Colab environment, using alternative startup strategy") + return self._start_server_colab( + port, + model_name, + embedding_mode, + config_signature=config_signature, + provider_options=provider_options, + **kwargs, + ) + + # Always pick a fresh available port + try: + actual_port = _get_available_port(port) + except RuntimeError: + logger.error("No available ports found") + return False, port + + # Start a new server (guarded by lock in daemon mode to avoid race). + if use_daemon and not _is_colab_environment(): + with self._registry_lock(config_signature): + adopted = self._adopt_registered_server(config_signature) + if adopted is not None: + self.server_process = None + self.server_port = adopted + self._server_config = config_signature + self._daemon_mode = True + return True, adopted + started, actual_port = self._start_new_server( + actual_port, + model_name, + embedding_mode, + provider_options=provider_options, + config_signature=config_signature, + **kwargs, + ) + if started: + self._daemon_mode = True + self._registry_path = self._write_registry_record( + port=actual_port, + config_signature=config_signature, + daemon_ttl_seconds=daemon_ttl_seconds, + ) + return started, actual_port + + started, actual_port = self._start_new_server( + actual_port, + model_name, + embedding_mode, + provider_options=provider_options, + config_signature=config_signature, + **kwargs, + ) + if started: + self._daemon_mode = False + return started, actual_port + + def _build_config_signature( + self, + *, + model_name: str, + embedding_mode: str, + provider_options: Optional[dict], + passages_file: Optional[str], + distance_metric: Optional[str], + ) -> dict: + """Create a signature describing the current server configuration.""" + passages_path = "" + if passages_file: + passages_path = _safe_resolve(Path(passages_file)) + return { + "model_name": model_name, + "passages_file": passages_path, + "embedding_mode": embedding_mode, + "distance_metric": distance_metric or "", + "provider_options": provider_options or {}, + "passages_signature": _build_passages_signature(passages_file), + } + + def _start_server_colab( + self, + port: int, + model_name: str, + embedding_mode: str = "sentence-transformers", + *, + config_signature: Optional[dict] = None, + provider_options: Optional[dict] = None, + **kwargs, + ) -> tuple[bool, int]: + """Start server with Colab-specific configuration.""" + # Try to find an available port + try: + actual_port = _get_available_port(port) + except RuntimeError: + logger.error("No available ports found") + return False, port + + logger.info(f"Starting server on port {actual_port} for Colab environment") + + # Use a simpler startup strategy for Colab + command = self._build_server_command(actual_port, model_name, embedding_mode, **kwargs) + + try: + # In Colab, we'll use a more direct approach + self._launch_server_process_colab( + command, + actual_port, + provider_options=provider_options, + config_signature=config_signature, + ) + started, ready_port = self._wait_for_server_ready_colab(actual_port) + if started: + self._server_config = config_signature or { + "model_name": model_name, + "passages_file": kwargs.get("passages_file", ""), + "embedding_mode": embedding_mode, + "provider_options": provider_options or {}, + } + return started, ready_port + except Exception as e: + logger.error(f"Failed to start embedding server in Colab: {e}") + return False, actual_port + + # Note: No compatibility check needed; manager is per-searcher and configs are stable per instance + + def _start_new_server( + self, + port: int, + model_name: str, + embedding_mode: str, + provider_options: Optional[dict] = None, + config_signature: Optional[dict] = None, + **kwargs, + ) -> tuple[bool, int]: + """Start a new embedding server on the given port.""" + logger.info(f"Starting embedding server on port {port}...") + + command = self._build_server_command(port, model_name, embedding_mode, **kwargs) + + try: + self._launch_server_process( + command, + port, + provider_options=provider_options, + config_signature=config_signature, + ) + started, ready_port = self._wait_for_server_ready(port) + if started: + self._server_config = config_signature or { + "model_name": model_name, + "passages_file": kwargs.get("passages_file", ""), + "embedding_mode": embedding_mode, + "provider_options": provider_options or {}, + } + return started, ready_port + except Exception as e: + logger.error(f"Failed to start embedding server: {e}") + return False, port + + def _build_server_command( + self, port: int, model_name: str, embedding_mode: str, **kwargs + ) -> list: + """Build the command to start the embedding server.""" + command = [ + sys.executable, + "-m", + self.backend_module_name, + "--zmq-port", + str(port), + "--model-name", + model_name, + ] + + if kwargs.get("passages_file"): + # Convert to absolute path to ensure subprocess can find the file + passages_file = Path(kwargs["passages_file"]).resolve() + command.extend(["--passages-file", str(passages_file)]) + if embedding_mode != "sentence-transformers": + command.extend(["--embedding-mode", embedding_mode]) + if kwargs.get("distance_metric"): + command.extend(["--distance-metric", kwargs["distance_metric"]]) + if kwargs.get("enable_warmup", True): + command.append("--enable-warmup") + if kwargs.get("use_daemon", True): + command.append("--daemon-mode") + ttl = int(kwargs.get("daemon_ttl_seconds", 900)) + command.extend(["--daemon-ttl", str(ttl)]) + + return command + + def _launch_server_process( + self, + command: list, + port: int, + *, + provider_options: Optional[dict] = None, + config_signature: Optional[dict] = None, + ) -> None: + """Launch the server process.""" + project_root = Path(__file__).parent.parent.parent.parent.parent + logger.info(f"Command: {' '.join(command)}") + + # In CI environment, redirect stdout to avoid buffer deadlock but keep stderr for debugging + # Embedding servers use many print statements that can fill stdout buffers + is_ci = os.environ.get("CI") == "true" + if is_ci: + stdout_target = subprocess.DEVNULL + stderr_target = None # Keep stderr for error debugging in CI + logger.info( + "CI environment detected, redirecting embedding server stdout to DEVNULL, keeping stderr" + ) + else: + stdout_target = None # Direct to console for visible logs + stderr_target = None # Direct to console for visible logs + + # Start embedding server subprocess + logger.info(f"Starting server process with command: {' '.join(command)}") + env = os.environ.copy() + encoded_options = encode_provider_options(provider_options) + if encoded_options: + env["LEANN_EMBEDDING_OPTIONS"] = encoded_options + + self.server_process = subprocess.Popen( + command, + cwd=project_root, + stdout=stdout_target, + stderr=stderr_target, + env=env, + ) + self.server_port = port + # Record config for in-process reuse (best effort; refined later when ready) + if config_signature is not None: + self._server_config = config_signature + else: # Fallback for unexpected code paths + try: + self._server_config = { + "model_name": command[command.index("--model-name") + 1] + if "--model-name" in command + else "", + "passages_file": command[command.index("--passages-file") + 1] + if "--passages-file" in command + else "", + "embedding_mode": command[command.index("--embedding-mode") + 1] + if "--embedding-mode" in command + else "sentence-transformers", + "provider_options": provider_options or {}, + } + except Exception: + self._server_config = { + "model_name": "", + "passages_file": "", + "embedding_mode": "sentence-transformers", + "provider_options": provider_options or {}, + } + logger.info(f"Server process started with PID: {self.server_process.pid}") + + # Register atexit callback only when we actually start a process + if not self._atexit_registered: + # Always attempt best-effort finalize at interpreter exit + atexit.register(self._finalize_process) + self._atexit_registered = True + # Touch finalizer so it knows there is a live process + finalizer = getattr(self, "_finalizer", None) + if finalizer is not None and getattr(finalizer, "alive", False) is False: + try: + import weakref + + self._finalizer = weakref.finalize(self, self._finalize_process) + except Exception: + pass + + def _wait_for_server_ready(self, port: int) -> tuple[bool, int]: + """Wait for the server to be ready.""" + max_wait, wait_interval = 120, 0.5 + for _ in range(int(max_wait / wait_interval)): + if _check_port(port): + logger.info("Embedding server is ready!") + return True, port + + if self.server_process and self.server_process.poll() is not None: + logger.error("Server terminated during startup.") + return False, port + + time.sleep(wait_interval) + + logger.error(f"Server failed to start within {max_wait} seconds.") + self.stop_server() + return False, port + + def stop_server(self): + """Stops the embedding server process if it's running.""" + if self._daemon_mode: + # Daemon is intentionally process-global: this manager detaches by default. + self.server_process = None + self.server_port = None + self._server_config = None + self._daemon_mode = False + self._registry_path = None + return + + if not self.server_process: + return + + if self.server_process and self.server_process.poll() is not None: + # Process already terminated + self.server_process = None + self.server_port = None + self._server_config = None + return + + logger.info( + f"Terminating server process (PID: {self.server_process.pid}) for backend {self.backend_module_name}..." + ) + + # Use simple termination first; if the server installed signal handlers, + # it will exit cleanly. Otherwise escalate to kill after a short wait. + try: + self.server_process.terminate() + except Exception: + pass + + try: + self.server_process.wait(timeout=5) # Give more time for graceful shutdown + logger.info(f"Server process {self.server_process.pid} terminated gracefully.") + except subprocess.TimeoutExpired: + logger.warning( + f"Server process {self.server_process.pid} did not terminate within 5 seconds, force killing..." + ) + try: + self.server_process.kill() + except Exception: + pass + try: + self.server_process.wait(timeout=2) + logger.info(f"Server process {self.server_process.pid} killed successfully.") + except subprocess.TimeoutExpired: + logger.error( + f"Failed to kill server process {self.server_process.pid} - it may be hung" + ) + + # Clean up process resources with timeout to avoid CI hang + try: + # Use shorter timeout in CI environments + is_ci = os.environ.get("CI") == "true" + timeout = 3 if is_ci else 10 + self.server_process.wait(timeout=timeout) + logger.info(f"Server process {self.server_process.pid} cleanup completed") + except subprocess.TimeoutExpired: + logger.warning(f"Process cleanup timeout after {timeout}s, proceeding anyway") + except Exception as e: + logger.warning(f"Error during process cleanup: {e}") + finally: + self.server_process = None + self.server_port = None + self._server_config = None + + def _finalize_process(self) -> None: + """Best-effort cleanup used by weakref.finalize/atexit.""" + try: + self.stop_server() + except Exception: + pass + + def _adopt_existing_server(self, *args, **kwargs) -> None: + # Legacy no-op retained for compatibility. + return + + def _launch_server_process_colab( + self, + command: list, + port: int, + *, + provider_options: Optional[dict] = None, + config_signature: Optional[dict] = None, + ) -> None: + """Launch the server process with Colab-specific settings.""" + logger.info(f"Colab Command: {' '.join(command)}") + + # In Colab, we need to be more careful about process management + env = os.environ.copy() + encoded_options = encode_provider_options(provider_options) + if encoded_options: + env["LEANN_EMBEDDING_OPTIONS"] = encoded_options + + self.server_process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + ) + self.server_port = port + logger.info(f"Colab server process started with PID: {self.server_process.pid}") + + # Register atexit callback (unified) + if not self._atexit_registered: + atexit.register(self._finalize_process) + self._atexit_registered = True + # Record config for in-process reuse is best-effort in Colab mode + if config_signature is not None: + self._server_config = config_signature + else: + self._server_config = { + "model_name": "", + "passages_file": "", + "embedding_mode": "sentence-transformers", + "provider_options": provider_options or {}, + } + + def _wait_for_server_ready_colab(self, port: int) -> tuple[bool, int]: + """Wait for the server to be ready with Colab-specific timeout.""" + max_wait, wait_interval = 30, 0.5 # Shorter timeout for Colab + + for _ in range(int(max_wait / wait_interval)): + if _check_port(port): + logger.info("Colab embedding server is ready!") + return True, port + + if self.server_process and self.server_process.poll() is not None: + # Check for error output + stdout, stderr = self.server_process.communicate() + logger.error("Colab server terminated during startup.") + logger.error(f"stdout: {stdout}") + logger.error(f"stderr: {stderr}") + return False, port + + time.sleep(wait_interval) + + logger.error(f"Colab server failed to start within {max_wait} seconds.") + self.stop_server() + return False, port + + @staticmethod + def _registry_dir() -> Path: + return Path.home() / ".leann" / "servers" + + @contextlib.contextmanager + def _registry_lock(self, config_signature: dict[str, Any]): + """Best-effort lock around the daemon check-then-start critical section. + + Two layers protect the registry from concurrent mutation: + + 1. **threading.Lock** (per registry key, module-global) β€” serialises + threads inside the same process. POSIX ``fcntl.flock`` is + process-granularity and does *not* block concurrent threads within + a single process, so this layer is required on every platform. + 2. **File lock** (``fcntl`` on POSIX, ``msvcrt`` on Windows) β€” + serialises separate OS processes. + """ + lock_file = None + lock_info_path: Optional[Path] = None + thread_lock: Optional[threading.Lock] = None + try: + lock_path = self._registry_dir() + lock_path.mkdir(parents=True, exist_ok=True) + lock_key = self._registry_key(config_signature) + + with _REGISTRY_LOCKS_GUARD: + thread_lock = _REGISTRY_LOCKS.setdefault(lock_key, threading.Lock()) + thread_lock.acquire() + + lock_file = (lock_path / f"{lock_key}.lock").open("a+") + lock_info_path = lock_path / f"{lock_key}.lockinfo.json" + self._recover_stale_lock_info(lock_info_path) + _flock_acquire(lock_file) + self._write_lock_info(lock_info_path) + yield + finally: + if lock_info_path is not None: + lock_info_path.unlink(missing_ok=True) + if lock_file is not None: + _flock_release(lock_file) + lock_file.close() + if thread_lock is not None and thread_lock.locked(): + thread_lock.release() + + def _recover_stale_lock_info(self, lock_info_path: Path) -> None: + if not lock_info_path.exists(): + return + try: + info = json.loads(lock_info_path.read_text(encoding="utf-8")) + pid = int(info.get("pid") or 0) + ts = float(info.get("ts") or 0) + except Exception: + lock_info_path.unlink(missing_ok=True) + return + + age = (time.time() - ts) if ts else (_LOCK_STALE_SECONDS + 1) + if age > _LOCK_STALE_SECONDS or (pid > 0 and not _pid_is_alive(pid)): + lock_info_path.unlink(missing_ok=True) + + def _write_lock_info(self, lock_info_path: Path) -> None: + payload = {"pid": os.getpid(), "ts": time.time()} + tmp_path = lock_info_path.with_suffix(".tmp") + tmp_path.write_text(json.dumps(payload), encoding="utf-8") + os.replace(tmp_path, lock_info_path) + + def _registry_key(self, config_signature: dict[str, Any]) -> str: + payload = { + "backend_module_name": self.backend_module_name, + "config_signature": config_signature, + } + encoded = json.dumps(payload, sort_keys=True, ensure_ascii=True).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + def _write_registry_record( + self, + *, + port: int, + config_signature: dict[str, Any], + daemon_ttl_seconds: int, + ) -> Path: + registry_dir = self._registry_dir() + registry_dir.mkdir(parents=True, exist_ok=True) + registry_path = registry_dir / f"{self._registry_key(config_signature)}.json" + record = { + "pid": self.server_process.pid if self.server_process else None, + "port": port, + "backend_module_name": self.backend_module_name, + "daemon_ttl_seconds": int(daemon_ttl_seconds), + "created_at": time.time(), + "config_signature": config_signature, + } + tmp_path = registry_path.with_suffix(".json.tmp") + tmp_path.write_text(json.dumps(record, indent=2), encoding="utf-8") + os.replace(tmp_path, registry_path) + return registry_path + + def _adopt_registered_server(self, config_signature: dict[str, Any]) -> Optional[int]: + registry_dir = self._registry_dir() + if not registry_dir.exists(): + return None + + target = registry_dir / f"{self._registry_key(config_signature)}.json" + if not target.exists(): + return None + + try: + record = json.loads(target.read_text(encoding="utf-8")) + except Exception: + target.unlink(missing_ok=True) + return None + + if record.get("backend_module_name") != self.backend_module_name: + target.unlink(missing_ok=True) + return None + if record.get("config_signature") != config_signature: + target.unlink(missing_ok=True) + return None + + pid = int(record.get("pid") or 0) + port = int(record.get("port") or 0) + if not _pid_is_alive(pid) or not _check_port(port): + target.unlink(missing_ok=True) + return None + + self._registry_path = target + logger.info("Reusing daemonized embedding server on port %s", port) + return port + + @classmethod + def list_daemons(cls) -> list[dict[str, Any]]: + registry_dir = cls._registry_dir() + if not registry_dir.exists(): + return [] + + records: list[dict[str, Any]] = [] + for record_path in sorted(registry_dir.glob("*.json")): + try: + record = json.loads(record_path.read_text(encoding="utf-8")) + except Exception: + record_path.unlink(missing_ok=True) + continue + + pid = int(record.get("pid") or 0) + port = int(record.get("port") or 0) + alive = _pid_is_alive(pid) and _check_port(port) + if not alive: + record_path.unlink(missing_ok=True) + continue + + record["record_path"] = str(record_path) + records.append(record) + return records + + @classmethod + def stop_daemons( + cls, + *, + backend_module_name: Optional[str] = None, + passages_file: Optional[str] = None, + ) -> int: + resolved_passages_file = _safe_resolve(Path(passages_file)) if passages_file else None + stopped = 0 + for record in cls.list_daemons(): + if backend_module_name and record.get("backend_module_name") != backend_module_name: + continue + + record_passages = ( + record.get("config_signature", {}).get("passages_file") + if isinstance(record.get("config_signature"), dict) + else None + ) + if resolved_passages_file and record_passages != resolved_passages_file: + continue + + pid = int(record.get("pid") or 0) + try: + os.kill(pid, 15) + stopped += 1 + except OSError: + pass + + record_path = record.get("record_path") + if record_path: + Path(record_path).unlink(missing_ok=True) + return stopped diff --git a/packages/leann-core/src/leann/interactive_utils.py b/packages/leann-core/src/leann/interactive_utils.py new file mode 100644 index 0000000..884e7b7 --- /dev/null +++ b/packages/leann-core/src/leann/interactive_utils.py @@ -0,0 +1,199 @@ +""" +Interactive session utilities for LEANN applications. + +Provides shared readline functionality and command handling across +CLI, API, and RAG example interactive modes. +""" + +import atexit +import os +from pathlib import Path +from types import ModuleType +from typing import Callable, Optional + +# Try to import readline with fallback for Windows +HAS_READLINE = False +readline: ModuleType | None = None +try: + import readline + + HAS_READLINE = True +except ImportError: + # Windows doesn't have readline by default + pass + + +class InteractiveSession: + """Manages interactive session with optional readline support and common commands.""" + + def __init__( + self, + history_name: str, + prompt: str = "You: ", + welcome_message: str = "", + ): + """ + Initialize interactive session with optional readline support. + + Args: + history_name: Name for history file (e.g., "cli", "api_chat") + (ignored if readline not available) + prompt: Input prompt to display + welcome_message: Message to show when starting session + + Note: + On systems without readline (e.g., Windows), falls back to basic input() + with limited functionality (no history, no line editing). + """ + self.history_name = history_name + self.prompt = prompt + self.welcome_message = welcome_message + self._setup_complete = False + + def setup_readline(self): + """Setup readline with history support (if available).""" + if self._setup_complete: + return + + if not HAS_READLINE: + # Readline not available (likely Windows), skip setup + self._setup_complete = True + return + rl = readline + if rl is None: + self._setup_complete = True + return + + # History file setup + history_dir = Path.home() / ".leann" / "history" + history_dir.mkdir(parents=True, exist_ok=True) + history_file = history_dir / f"{self.history_name}.history" + + # Load history if exists + try: + rl.read_history_file(str(history_file)) + rl.set_history_length(1000) + except (FileNotFoundError, FileExistsError, OSError): + pass + + # Save history on exit + atexit.register(rl.write_history_file, str(history_file)) + + # Optional: Enable vi editing mode (commented out by default) + # readline.parse_and_bind("set editing-mode vi") + + self._setup_complete = True + + def _show_help(self): + """Show available commands.""" + print("Commands:") + print(" quit/exit/q - Exit the chat") + print(" help - Show this help message") + print(" clear - Clear screen") + print(" history - Show command history") + + def _show_history(self): + """Show command history.""" + if not HAS_READLINE: + print(" History not available (readline not supported on this system)") + return + rl = readline + if rl is None: + print(" History not available (readline not supported on this system)") + return + + history_length = rl.get_current_history_length() + if history_length == 0: + print(" No history available") + return + + for i in range(history_length): + item = rl.get_history_item(i + 1) + if item: + print(f" {i + 1}: {item}") + + def get_user_input(self) -> Optional[str]: + """ + Get user input with readline support. + + Returns: + User input string, or None if EOF (Ctrl+D) + """ + try: + return input(self.prompt).strip() + except KeyboardInterrupt: + print("\n(Use 'quit' to exit)") + return "" # Return empty string to continue + except EOFError: + print("\nGoodbye!") + return None + + def run_interactive_loop(self, handler_func: Callable[[str], None]): + """ + Run the interactive loop with a custom handler function. + + Args: + handler_func: Function to handle user input that's not a built-in command + Should accept a string and handle the user's query + """ + self.setup_readline() + + if self.welcome_message: + print(self.welcome_message) + + while True: + user_input = self.get_user_input() + + if user_input is None: # EOF (Ctrl+D) + break + + if not user_input: # Empty input or KeyboardInterrupt + continue + + # Handle built-in commands + command = user_input.lower() + if command in ["quit", "exit", "q"]: + print("Goodbye!") + break + elif command == "help": + self._show_help() + elif command == "clear": + os.system("clear" if os.name != "nt" else "cls") + elif command == "history": + self._show_history() + else: + # Regular user input - pass to handler + try: + handler_func(user_input) + except Exception as e: + print(f"Error: {e}") + + +def create_cli_session(index_name: str) -> InteractiveSession: + """Create an interactive session for CLI usage.""" + return InteractiveSession( + history_name=index_name, + prompt="\nYou: ", + welcome_message="LEANN Assistant ready! Type 'quit' to exit, 'help' for commands\n" + + "=" * 40, + ) + + +def create_api_session() -> InteractiveSession: + """Create an interactive session for API chat.""" + return InteractiveSession( + history_name="api_chat", + prompt="You: ", + welcome_message="Leann Chat started (type 'quit' to exit, 'help' for commands)\n" + + "=" * 40, + ) + + +def create_rag_session(app_name: str, data_description: str) -> InteractiveSession: + """Create an interactive session for RAG examples.""" + return InteractiveSession( + history_name=f"{app_name}_rag", + prompt="You: ", + welcome_message=f"[Interactive Mode] Chat with your {data_description} data!\nType 'quit' or 'exit' to stop, 'help' for commands.\n" + + "=" * 40, + ) diff --git a/packages/leann-core/src/leann/interface.py b/packages/leann-core/src/leann/interface.py new file mode 100644 index 0000000..6b7d7b7 --- /dev/null +++ b/packages/leann-core/src/leann/interface.py @@ -0,0 +1,109 @@ +from abc import ABC, abstractmethod +from typing import Any, Literal, Optional + +import numpy as np + + +class LeannBackendBuilderInterface(ABC): + """Backend interface for building indexes""" + + @abstractmethod + def build(self, data: np.ndarray, ids: list[str], index_path: str, **kwargs) -> None: + """Build index + + Args: + data: Vector data (N, D) + ids: List of string IDs for each vector + index_path: Path to save index + **kwargs: Backend-specific build parameters + """ + pass + + +class LeannBackendSearcherInterface(ABC): + """Backend interface for searching""" + + @abstractmethod + def __init__(self, index_path: str, **kwargs): + """Initialize searcher + + Args: + index_path: Path to index file + **kwargs: Backend-specific loading parameters + """ + pass + + @abstractmethod + def _ensure_server_running( + self, passages_source_file: str, port: Optional[int], **kwargs + ) -> int: + """Ensure server is running""" + pass + + @abstractmethod + def search( + self, + query: np.ndarray, + top_k: int, + complexity: int = 64, + beam_width: int = 1, + prune_ratio: float = 0.0, + recompute_embeddings: bool = False, + pruning_strategy: Literal["global", "local", "proportional"] = "global", + zmq_port: Optional[int] = None, + **kwargs, + ) -> dict[str, Any]: + """Search for nearest neighbors + + Args: + query: Query vectors (B, D) where B is batch size, D is dimension + top_k: Number of nearest neighbors to return + complexity: Search complexity/candidate list size, higher = more accurate but slower + beam_width: Number of parallel search paths/IO requests per iteration + prune_ratio: Ratio of neighbors to prune via approximate distance (0.0-1.0) + recompute_embeddings: Whether to fetch fresh embeddings from server vs use stored PQ codes + pruning_strategy: PQ candidate selection strategy - "global" (default), "local", or "proportional" + zmq_port: ZMQ port for embedding server communication. Must be provided if recompute_embeddings is True. + **kwargs: Backend-specific parameters + + Returns: + {"labels": [...], "distances": [...]} + """ + pass + + @abstractmethod + def compute_query_embedding( + self, + query: str, + use_server_if_available: bool = True, + zmq_port: Optional[int] = None, + query_template: Optional[str] = None, + ) -> np.ndarray: + """Compute embedding for a query string + + Args: + query: The query string to embed + zmq_port: ZMQ port for embedding server + use_server_if_available: Whether to try using embedding server first + query_template: Optional prompt template to prepend to query + + Returns: + Query embedding as numpy array with shape (1, D) + """ + pass + + +class LeannBackendFactoryInterface(ABC): + """Backend factory interface""" + + @staticmethod + @abstractmethod + def builder(**kwargs) -> LeannBackendBuilderInterface: + """Create Builder instance""" + pass + + @staticmethod + @abstractmethod + def searcher(index_path: str, **kwargs) -> LeannBackendSearcherInterface: + """Create Searcher instance""" + pass diff --git a/packages/leann-core/src/leann/mcp.py b/packages/leann-core/src/leann/mcp.py new file mode 100755 index 0000000..f0b6017 --- /dev/null +++ b/packages/leann-core/src/leann/mcp.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python3 + +import argparse +import json +import subprocess +import sys + +_base_dir: str | None = None + + +def _leann_cmd() -> list[str]: + """Build the base command for invoking ``leann`` CLI. + + Using ``sys.executable -m leann`` guarantees we find the CLI even when + the ``leann`` console-script is not on PATH (common on Windows when + launched by mcp-proxy or other service wrappers). + """ + return [sys.executable, "-m", "leann"] + + +def _run_leann(*args, timeout=120): + """Run a leann CLI command and return (returncode, stdout, stderr).""" + result = subprocess.run( + ["leann", *args], + capture_output=True, + text=True, + timeout=timeout, + ) + return result.returncode, result.stdout, result.stderr + + +def _make_result(request_id, content_text): + return { + "jsonrpc": "2.0", + "id": request_id, + "result": {"content": [{"type": "text", "text": content_text}]}, + } + + +def _make_error(request_id, message): + return { + "jsonrpc": "2.0", + "id": request_id, + "error": {"code": -1, "message": message}, + } + + +TOOLS = [ + { + "name": "leann_search", + "description": ( + "Semantic code search across an indexed codebase. Returns matching code " + "chunks with file paths, scores, and surrounding context.\n\n" + "Use this to find relevant code before making changes β€” understand existing " + "patterns, locate implementations, and discover related files.\n\n" + "Examples: 'authentication middleware', 'database connection pooling', " + "'error handling in API routes', 'how are embeddings computed'" + ), + "inputSchema": { + "type": "object", + "properties": { + "index_name": { + "type": "string", + "description": "Name of the LEANN index to search. Use leann_list to see available indexes.", + }, + "query": { + "type": "string", + "description": "Natural language or technical search query.", + }, + "top_k": { + "type": "integer", + "default": 5, + "minimum": 1, + "maximum": 20, + "description": "Number of results to return (default 5).", + }, + "complexity": { + "type": "integer", + "default": 32, + "minimum": 16, + "maximum": 128, + "description": "Search precision level (default 32, use 64+ for thorough search).", + }, + }, + "required": ["index_name", "query"], + }, + }, + { + "name": "leann_list", + "description": "List all available LEANN indexes across projects. Shows index names, status, size, and location.", + "inputSchema": {"type": "object", "properties": {}}, + }, + { + "name": "leann_build", + "description": ( + "Build or incrementally update a LEANN index for a codebase. " + "If the index already exists, only new/modified/deleted files are processed " + "(incremental update). Use this to keep the index current after code changes.\n\n" + "Provide file paths or directories to index. For git repos, pass the output " + "of 'git ls-files' as individual paths." + ), + "inputSchema": { + "type": "object", + "properties": { + "index_name": { + "type": "string", + "description": "Name for the index (e.g., 'my-project'). Defaults to current directory name if omitted.", + }, + "docs": { + "type": "array", + "items": {"type": "string"}, + "description": "List of file paths or directories to index.", + }, + "backend_name": { + "type": "string", + "enum": ["hnsw", "ivf"], + "default": "ivf", + "description": "Index backend. 'ivf' supports incremental updates (recommended). 'hnsw' is faster for search but limited incremental support.", + }, + "force": { + "type": "boolean", + "default": False, + "description": "Force full rebuild instead of incremental update.", + }, + }, + "required": ["docs"], + }, + }, + { + "name": "leann_status", + "description": ( + "Show detailed status of a LEANN index: backend type, embedding model, " + "number of chunks, file count, index size, and whether the index is up to date." + ), + "inputSchema": { + "type": "object", + "properties": { + "index_name": { + "type": "string", + "description": "Name of the index to inspect.", + }, + }, + "required": ["index_name"], + }, + }, +] + + +def handle_search(request_id, args): + index_name = args.get("index_name", "") + query = args.get("query", "") + if not index_name or not query: + return _make_result(request_id, "Error: Both index_name and query are required.") + + top_k = args.get("top_k", 5) + complexity = args.get("complexity", 32) + + rc, stdout, stderr = _run_leann( + "search", + index_name, + query, + f"--top-k={top_k}", + f"--complexity={complexity}", + "--json", + "--show-metadata", + "--non-interactive", + ) + + if rc != 0: + return _make_result(request_id, f"Search failed: {stderr.strip()}") + + # Parse JSON results and format for code context + try: + results = json.loads(stdout) + except json.JSONDecodeError: + # Fallback to raw output if --json isn't available + return _make_result( + request_id, stdout if stdout.strip() else f"Search failed: {stderr.strip()}" + ) + + if not results: + return _make_result(request_id, f"No results found for '{query}'.") + + formatted = [] + for i, r in enumerate(results, 1): + meta = r.get("metadata", {}) + file_path = meta.get("file_path") or meta.get("source", "unknown") + score = r.get("score", 0) + text = r.get("text", "").strip() + formatted.append(f"### Result {i} β€” {file_path} (score: {score:.3f})\n```\n{text}\n```") + + header = f"Found {len(results)} results for '{query}':\n" + return _make_result(request_id, header + "\n\n".join(formatted)) + + +def handle_list(request_id): + rc, stdout, stderr = _run_leann("list") + if rc != 0: + return _make_result(request_id, f"Error listing indexes: {stderr.strip()}") + return _make_result(request_id, stdout) + + +def handle_build(request_id, args): + docs = args.get("docs", []) + if not docs: + return _make_result( + request_id, "Error: 'docs' parameter is required (list of file paths or directories)." + ) + + cmd = ["build"] + + index_name = args.get("index_name") + if index_name: + cmd.append(index_name) + + cmd.extend(["--docs", *docs]) + + backend = args.get("backend_name", "ivf") + cmd.extend([f"--backend-name={backend}"]) + + if args.get("force", False): + cmd.append("--force") + + # Auto-detect embedding model from existing index to ensure incremental + # updates use the same model (avoids mismatch with CLI default). + if index_name: + import json as _json + from pathlib import Path + + meta_path = Path.cwd() / ".leann" / "indexes" / index_name / "documents.leann.meta.json" + if meta_path.exists(): + try: + with open(meta_path, encoding="utf-8") as f: + meta = _json.load(f) + existing_model = meta.get("embedding_model") + existing_mode = meta.get("embedding_mode") + if existing_model: + cmd.extend([f"--embedding-model={existing_model}"]) + if existing_mode: + cmd.extend([f"--embedding-mode={existing_mode}"]) + except (OSError, ValueError): + pass + + rc, stdout, stderr = _run_leann(*cmd, timeout=600) + + if rc != 0: + return _make_result(request_id, f"Build failed:\n{stderr.strip()}\n{stdout.strip()}") + + return _make_result(request_id, stdout if stdout.strip() else "Build completed successfully.") + + +def handle_status(request_id, args): + index_name = args.get("index_name", "") + if not index_name: + return _make_result(request_id, "Error: index_name is required.") + + from pathlib import Path + + # Check standard location + leann_dir = Path.cwd() / ".leann" / "indexes" / index_name + meta_path = leann_dir / "documents.leann.meta.json" + passages_path = leann_dir / "documents.leann.passages.jsonl" + + if not meta_path.exists(): + return _make_result(request_id, f"Index '{index_name}' not found at {leann_dir}") + + try: + with open(meta_path) as f: + meta = json.load(f) + except Exception as e: + return _make_result(request_id, f"Error reading index metadata: {e}") + + # Count passages + num_chunks = 0 + file_paths = set() + if passages_path.exists(): + with open(passages_path) as f: + for line in f: + line = line.strip() + if not line: + continue + num_chunks += 1 + try: + passage = json.loads(line) + meta = passage.get("metadata", {}) + fp = meta.get("file_path") or meta.get("source", "") + if fp: + file_paths.add(fp) + except json.JSONDecodeError: + pass + + # Calculate total index size + total_size = 0 + if leann_dir.exists(): + for f in leann_dir.iterdir(): + if f.is_file(): + total_size += f.stat().st_size + + size_mb = total_size / (1024 * 1024) + + backend = meta.get("backend_name", "unknown") + embedding_model = meta.get("embedding_model", "unknown") + embedding_mode = meta.get("embedding_mode", "unknown") + dimensions = meta.get("dimensions", "unknown") + + status_lines = [ + f"Index: {index_name}", + f"Backend: {backend}", + f"Embedding: {embedding_model} ({embedding_mode})", + f"Dimensions: {dimensions}", + f"Chunks: {num_chunks}", + f"Files indexed: {len(file_paths)}", + f"Size: {size_mb:.1f} MB", + f"Location: {leann_dir}", + ] + + return _make_result(request_id, "\n".join(status_lines)) + + +def handle_request(request): + method = request.get("method") + request_id = request.get("id") + + if method == "initialize": + return { + "jsonrpc": "2.0", + "id": request_id, + "result": { + "capabilities": {"tools": {}}, + "protocolVersion": "2024-11-05", + "serverInfo": {"name": "leann-mcp", "version": "2.0.0"}, + }, + } + + if method == "notifications/initialized": + return None + + if method == "tools/list": + return { + "jsonrpc": "2.0", + "id": request_id, + "result": {"tools": TOOLS}, + } + + if method == "tools/call": + tool_name = request["params"]["name"] + args = request["params"].get("arguments", {}) + + try: + if tool_name == "leann_search": + return handle_search(request_id, args) + elif tool_name == "leann_list": + return handle_list(request_id) + elif tool_name == "leann_build": + return handle_build(request_id, args) + elif tool_name == "leann_status": + return handle_status(request_id, args) + else: + return _make_error(request_id, f"Unknown tool: {tool_name}") + except subprocess.TimeoutExpired: + return _make_result(request_id, "Error: Command timed out.") + except Exception as e: + return _make_error(request_id, str(e)) + + return None + + +def main(): + global _base_dir + + parser = argparse.ArgumentParser(description="LEANN MCP server (stdio)") + parser.add_argument( + "--base-dir", + help="Base directory where LEANN indexes are located. " + "The leann CLI will run with this as its working directory.", + ) + cli_args = parser.parse_args() + _base_dir = cli_args.base_dir + + for line in sys.stdin: + try: + request = json.loads(line.strip()) + response = handle_request(request) + if response: + print(json.dumps(response)) + sys.stdout.flush() + except Exception as e: + error_response = { + "jsonrpc": "2.0", + "id": None, + "error": {"code": -1, "message": str(e)}, + } + print(json.dumps(error_response)) + sys.stdout.flush() + + +if __name__ == "__main__": + main() diff --git a/packages/leann-core/src/leann/metadata_filter.py b/packages/leann-core/src/leann/metadata_filter.py new file mode 100644 index 0000000..5a8ffbd --- /dev/null +++ b/packages/leann-core/src/leann/metadata_filter.py @@ -0,0 +1,240 @@ +""" +Metadata filtering engine for LEANN search results. + +This module provides generic metadata filtering capabilities that can be applied +to search results from any LEANN backend. The filtering supports various +operators for different data types including numbers, strings, booleans, and lists. +""" + +import logging +from typing import Any, Optional, Union + +logger = logging.getLogger(__name__) + +# Type alias for filter specifications +FilterValue = Union[str, int, float, bool, list] +FilterSpec = dict[str, FilterValue] +MetadataFilters = dict[str, FilterSpec] + + +class MetadataFilterEngine: + """ + Engine for evaluating metadata filters against search results. + + Supports various operators for filtering based on metadata fields: + - Comparison: ==, !=, <, <=, >, >= + - Membership: in, not_in + - String operations: contains, starts_with, ends_with + - Boolean operations: is_true, is_false + """ + + def __init__(self): + """Initialize the filter engine with supported operators.""" + self.operators = { + "==": self._equals, + "!=": self._not_equals, + "<": self._less_than, + "<=": self._less_than_or_equal, + ">": self._greater_than, + ">=": self._greater_than_or_equal, + "in": self._in, + "not_in": self._not_in, + "contains": self._contains, + "starts_with": self._starts_with, + "ends_with": self._ends_with, + "is_true": self._is_true, + "is_false": self._is_false, + } + + def apply_filters( + self, search_results: list[dict[str, Any]], metadata_filters: Optional[MetadataFilters] + ) -> list[dict[str, Any]]: + """ + Apply metadata filters to a list of search results. + + Args: + search_results: List of result dictionaries, each containing 'metadata' field + metadata_filters: Dictionary of filter specifications + Format: {"field_name": {"operator": value}} + + Returns: + Filtered list of search results + """ + if not metadata_filters: + return search_results + + logger.debug(f"Applying filters: {metadata_filters}") + logger.debug(f"Input results count: {len(search_results)}") + + filtered_results = [] + for result in search_results: + if self._evaluate_filters(result, metadata_filters): + filtered_results.append(result) + + logger.debug(f"Filtered results count: {len(filtered_results)}") + return filtered_results + + def _evaluate_filters(self, result: dict[str, Any], filters: MetadataFilters) -> bool: + """ + Evaluate all filters against a single search result. + + All filters must pass (AND logic) for the result to be included. + + Args: + result: Full search result dictionary (including metadata, text, etc.) + filters: Filter specifications to evaluate + + Returns: + True if all filters pass, False otherwise + """ + for field_name, filter_spec in filters.items(): + if not self._evaluate_field_filter(result, field_name, filter_spec): + return False + return True + + def _evaluate_field_filter( + self, result: dict[str, Any], field_name: str, filter_spec: FilterSpec + ) -> bool: + """ + Evaluate a single field filter against a search result. + + Args: + result: Full search result dictionary + field_name: Name of the field to filter on + filter_spec: Filter specification for this field + + Returns: + True if the filter passes, False otherwise + """ + # First check top-level fields, then check metadata + field_value = result.get(field_name) + if field_value is None: + # Try to get from metadata if not found at top level + metadata = result.get("metadata", {}) + field_value = metadata.get(field_name) + + # Handle missing fields - they fail all filters except existence checks + if field_value is None: + logger.debug(f"Field '{field_name}' not found in result or metadata") + return False + + # Evaluate each operator in the filter spec + for operator, expected_value in filter_spec.items(): + if operator not in self.operators: + logger.warning(f"Unsupported operator: {operator}") + return False + + try: + if not self.operators[operator](field_value, expected_value): + logger.debug( + f"Filter failed: {field_name} {operator} {expected_value} " + f"(actual: {field_value})" + ) + return False + except Exception as e: + logger.warning( + f"Error evaluating filter {field_name} {operator} {expected_value}: {e}" + ) + return False + + return True + + # Comparison operators + def _equals(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value equals expected value.""" + return field_value == expected_value + + def _not_equals(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value does not equal expected value.""" + return field_value != expected_value + + def _less_than(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value is less than expected value.""" + return self._numeric_compare(field_value, expected_value, lambda a, b: a < b) + + def _less_than_or_equal(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value is less than or equal to expected value.""" + return self._numeric_compare(field_value, expected_value, lambda a, b: a <= b) + + def _greater_than(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value is greater than expected value.""" + return self._numeric_compare(field_value, expected_value, lambda a, b: a > b) + + def _greater_than_or_equal(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value is greater than or equal to expected value.""" + return self._numeric_compare(field_value, expected_value, lambda a, b: a >= b) + + # Membership operators + def _in(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value is in the expected list/collection.""" + if not isinstance(expected_value, (list, tuple, set)): + raise ValueError("'in' operator requires a list, tuple, or set") + return field_value in expected_value + + def _not_in(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value is not in the expected list/collection.""" + if not isinstance(expected_value, (list, tuple, set)): + raise ValueError("'not_in' operator requires a list, tuple, or set") + return field_value not in expected_value + + # String operators + def _contains(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value contains the expected substring.""" + field_str = str(field_value) + expected_str = str(expected_value) + return expected_str in field_str + + def _starts_with(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value starts with the expected prefix.""" + field_str = str(field_value) + expected_str = str(expected_value) + return field_str.startswith(expected_str) + + def _ends_with(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value ends with the expected suffix.""" + field_str = str(field_value) + expected_str = str(expected_value) + return field_str.endswith(expected_str) + + # Boolean operators + def _is_true(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value is truthy.""" + return bool(field_value) + + def _is_false(self, field_value: Any, expected_value: Any) -> bool: + """Check if field value is falsy.""" + return not bool(field_value) + + # Helper methods + def _numeric_compare(self, field_value: Any, expected_value: Any, compare_func) -> bool: + """ + Helper for numeric comparisons with type coercion. + + Args: + field_value: Value from metadata + expected_value: Value to compare against + compare_func: Comparison function to apply + + Returns: + Result of comparison + """ + try: + # Try to convert both values to numbers for comparison + if isinstance(field_value, str) and isinstance(expected_value, str): + # String comparison if both are strings + return compare_func(field_value, expected_value) + + # Numeric comparison - attempt to convert to float + field_num = ( + float(field_value) if not isinstance(field_value, (int, float)) else field_value + ) + expected_num = ( + float(expected_value) + if not isinstance(expected_value, (int, float)) + else expected_value + ) + + return compare_func(field_num, expected_num) + except (ValueError, TypeError): + # Fall back to string comparison if numeric conversion fails + return compare_func(str(field_value), str(expected_value)) diff --git a/packages/leann-core/src/leann/react_agent.py b/packages/leann-core/src/leann/react_agent.py new file mode 100644 index 0000000..a7cdc22 --- /dev/null +++ b/packages/leann-core/src/leann/react_agent.py @@ -0,0 +1,316 @@ +""" +Simple ReAct agent for multiturn retrieval with LEANN. + +This implements a basic ReAct (Reasoning + Acting) agent pattern: +- Thought: LLM reasons about what to do next +- Action: Performs a search action (local or web) +- Observation: Gets results from search +- Repeat until final answer + +Reference: Inspired by mini-swe-agent pattern, kept simple for multiturn retrieval. +""" + +from __future__ import annotations + +import logging +import re +from typing import Any + +from .api import LeannSearcher, SearchResult +from .chat import LLMInterface, get_llm +from .web_search import WebSearcher + +logger = logging.getLogger(__name__) + + +class ReActAgent: + """ + ReAct agent for multiturn retrieval with local and web search. + + Supports three tools: + - leann_search: search the local knowledge base + - web_search: search the public internet via Serper API (when configured) + - visit_page: fetch full page content via Jina Reader (when configured) + + The agent dynamically adapts its prompt and behavior based on which + tools are available (i.e., whether API keys are configured). + """ + + def __init__( + self, + searcher: LeannSearcher, + llm: LLMInterface | None = None, + llm_config: dict[str, Any] | None = None, + max_iterations: int = 5, + serper_api_key: str | None = None, + jina_api_key: str | None = None, + ): + self.searcher = searcher + if llm is None: + self.llm = get_llm(llm_config) + else: + self.llm = llm + self.max_iterations = max_iterations + self.search_history: list[dict[str, Any]] = [] + self.web_searcher = WebSearcher(api_key=serper_api_key, jina_api_key=jina_api_key) + self.web_search_available = bool(self.web_searcher.api_key) + + def _format_search_results(self, results: list[SearchResult]) -> str: + """Format search results as a string for the LLM.""" + if not results: + return "No results found." + formatted = [] + for i, result in enumerate(results, 1): + formatted.append(f"[Result {i}] (Score: {result.score:.3f})\n{result.text[:500]}...") + if result.metadata.get("source"): + formatted[-1] += f"\nSource: {result.metadata['source']}" + return "\n\n".join(formatted) + + def _create_react_prompt( + self, question: str, iteration: int, previous_observations: list[str] + ) -> str: + """Create the ReAct prompt, dynamically adapted to available tools.""" + if self.web_search_available: + tools_block = ( + "You have access to these tools:\n" + '1. leann_search("query"): Search the local private knowledge base (code, docs, history).\n' + '2. web_search("query"): Search the public internet for up-to-date information.\n' + '3. visit_page("url"): Read the full content of a specific URL.\n' + "\nStrategies:\n" + "- Use `leann_search` for internal project details, code implementation, or private history.\n" + "- Use `web_search` for public documentation, latest news, or general concepts.\n" + "- Use `visit_page` if you found a relevant link but need the full details.\n" + "- You can combine both!" + ) + action_examples = ( + 'Action: leann_search("your query")\n\nOR\n\n' + "Thought: [your reasoning]\n" + 'Action: web_search("your query")\n\nOR\n\n' + "Thought: [your reasoning]\n" + "Action: Final Answer: [your answer]" + ) + else: + tools_block = ( + "You have access to this tool:\n" + '1. leann_search("query"): Search the local private knowledge base (code, docs, history).\n' + "\nNote: Web search is not available (no API key configured). " + "Answer using only the local knowledge base." + ) + action_examples = ( + 'Action: leann_search("your query")\n\nOR\n\n' + "Thought: [your reasoning]\n" + "Action: Final Answer: [your answer]" + ) + + prompt = ( + "You are a helpful assistant that answers questions by searching through " + "a knowledge base" + ) + if self.web_search_available: + prompt += " AND the internet" + prompt += f".\n\nQuestion: {question}\n\n{tools_block}\n\nPrevious observations:\n" + + if previous_observations: + for i, obs in enumerate(previous_observations, 1): + prompt += f"\nObservation {i}:\n{obs}\n" + else: + prompt += "None yet.\n" + + prompt += ( + f"\nCurrent iteration: {iteration}/{self.max_iterations}\n\n" + "Think step by step.\n" + "Format your response EXACTLY like this:\n\n" + f"Thought: [your reasoning]\n{action_examples}\n\n" + 'IMPORTANT: You MUST start a new line with "Action:" to trigger a tool.\n' + ) + + return prompt + + def _parse_llm_response(self, response: str) -> tuple[str, str | None]: + """ + Parse LLM response to extract thought and action. + + Returns: + (thought, action) where action is a prefixed string like + "leann_search:query", "web_search:query", "visit_page:url", + or None if the agent wants to give a final answer. + """ + thought = "" + action = None + + if "Thought:" in response: + thought_part = response.split("Thought:")[1] + if "Action:" in thought_part: + thought = thought_part.split("Action:")[0].strip() + elif "Final Answer:" in thought_part: + thought = thought_part.split("Final Answer:")[0].strip() + else: + thought = thought_part.strip() + else: + if "Action:" in response or "Final Answer:" in response: + thought = response.split("Action:")[0].split("Final Answer:")[0].strip() + else: + thought = response.strip() + + if "Final Answer:" in response: + action = None + elif "Action:" in response: + action_part = response.split("Action:")[1].strip() + + match = re.search( + r'(web_search|leann_search|visit_page|search)\(["\']([^"\']+)["\']\)', + action_part, + ) + if match: + tool_name = match.group(1) + if tool_name == "search": + tool_name = "leann_search" + action = f"{tool_name}:{match.group(2)}" + elif "search(" in response.lower(): + match = re.search(r'search\(["\']([^"\']+)["\']\)', response, re.IGNORECASE) + if match: + action = f"leann_search:{match.group(1)}" + + return thought, action + + def search(self, query: str, top_k: int = 5) -> list[SearchResult]: + """Perform a local search and return results.""" + logger.info(f"Searching: {query}") + results = self.searcher.search(query, top_k=top_k) + return results + + def run(self, question: str, top_k: int = 5) -> str: + """ + Run the ReAct agent to answer a question. + + The agent routes between local (leann_search) and web (web_search, + visit_page) tools based on LLM reasoning. When web tools are + unavailable, the agent gracefully falls back to local-only search. + """ + logger.info(f"Starting ReAct agent for question: {question}") + self.search_history = [] + previous_observations: list[str] = [] + all_context: list[str] = [] + + for iteration in range(1, self.max_iterations + 1): + logger.info(f"\n--- Iteration {iteration}/{self.max_iterations} ---") + + prompt = self._create_react_prompt(question, iteration, previous_observations) + + logger.info("Getting LLM reasoning...") + response = self.llm.ask(prompt) + + thought, action = self._parse_llm_response(response) + logger.info(f"Thought: {thought}") + + if action is None: + if "Final Answer:" in response: + final_answer = response.split("Final Answer:")[1].strip() + else: + final_answer = response.strip() + if "Action:" in final_answer: + final_answer = final_answer.split("Action:")[0].strip() + + logger.info(f"Final answer: {final_answer}") + return final_answer + + logger.info(f"Action: {action}") + + results_count = 0 + + if action.startswith("web_search:"): + query_str = action.split(":", 1)[1] + + if not self.web_search_available: + observation = ( + "Web search is not available (no SERPER_API_KEY configured). " + "Use leann_search to search the local knowledge base instead." + ) + results_count = 0 + else: + web_results = self.web_searcher.search(query_str, top_k=top_k) + + is_error = len(web_results) == 1 and web_results[0].get("title") == "Error" + if is_error: + observation = ( + f"Web search failed: {web_results[0].get('snippet', 'Unknown error')}. " + "Try leann_search for local results instead." + ) + results_count = 0 + elif not web_results: + observation = "No web results found." + results_count = 0 + else: + formatted = [] + for i, res in enumerate(web_results, 1): + formatted.append( + f"[Web Result {i}]\nTitle: {res['title']}\n" + f"Link: {res['link']}\nSnippet: {res['snippet']}" + ) + observation = "\n\n".join(formatted) + results_count = len(web_results) + + elif action.startswith("visit_page:"): + url = action.split(":", 1)[1] + try: + content = self.web_searcher.get_page_content(url) + except Exception as e: + content = f"Error fetching page: {e!s}" + results_count = 1 if not content.startswith("Error") else 0 + observation = f"Content of {url}:\n{content[:15000]}" + + else: + query_str = action.split(":", 1)[1] if ":" in action else action + results = self.search(query_str, top_k=top_k) + results_count = len(results) + observation = self._format_search_results(results) + + previous_observations.append(observation) + all_context.append(f"Action: {action}\n{observation}") + + if action.startswith("web_search:") or action.startswith("visit_page:"): + source = "web" + else: + source = "local" + + self.search_history.append( + { + "iteration": iteration, + "thought": thought, + "action": action, + "results_count": results_count, + "source": source, + } + ) + + logger.warning(f"Reached max iterations ({self.max_iterations}), getting final answer...") + final_prompt = f"""Based on all the searches performed, provide your final answer to the question. + +Question: {question} + +All search results: +{chr(10).join(all_context)} + +Provide your final answer now. +""" + final_answer = self.llm.ask(final_prompt) + return final_answer.strip() + + +def create_react_agent( + index_path: str, + llm_config: dict[str, Any] | None = None, + max_iterations: int = 5, + serper_api_key: str | None = None, + jina_api_key: str | None = None, + **searcher_kwargs, +) -> ReActAgent: + """Convenience function to create a ReActAgent.""" + searcher = LeannSearcher(index_path, **searcher_kwargs) + return ReActAgent( + searcher=searcher, + llm_config=llm_config, + max_iterations=max_iterations, + serper_api_key=serper_api_key, + jina_api_key=jina_api_key, + ) diff --git a/packages/leann-core/src/leann/registry.py b/packages/leann-core/src/leann/registry.py new file mode 100644 index 0000000..2b74a20 --- /dev/null +++ b/packages/leann-core/src/leann/registry.py @@ -0,0 +1,98 @@ +# packages/leann-core/src/leann/registry.py + +import importlib +import importlib.metadata +import json +import logging +from pathlib import Path +from typing import TYPE_CHECKING, Optional, Union + +if TYPE_CHECKING: + from leann.interface import LeannBackendFactoryInterface + +# Set up logger for this module +logger = logging.getLogger(__name__) + +BACKEND_REGISTRY: dict[str, "LeannBackendFactoryInterface"] = {} + + +def register_backend(name: str): + """A decorator to register a new backend class.""" + + def decorator(cls): + logger.debug(f"Registering backend '{name}'") + BACKEND_REGISTRY[name] = cls + return cls + + return decorator + + +def autodiscover_backends(): + """Automatically discovers and imports all 'leann-backend-*' packages.""" + # print("INFO: Starting backend auto-discovery...") + discovered_backends = [] + for dist in importlib.metadata.distributions(): + dist_name = dist.metadata["name"] + if dist_name is None: + continue + if dist_name.startswith("leann-backend-"): + backend_module_name = dist_name.replace("-", "_") + discovered_backends.append(backend_module_name) + + for backend_module_name in sorted(discovered_backends): # sort for deterministic loading + try: + importlib.import_module(backend_module_name) + # Registration message is printed by the decorator + except ImportError: + # print(f"WARN: Could not import backend module '{backend_module_name}': {e}") + pass + # print("INFO: Backend auto-discovery finished.") + + +def register_project_directory(project_dir: Optional[Union[str, Path]] = None): + """ + Register a project directory in the global LEANN registry. + + This allows `leann list` to discover indexes created by apps or other tools. + + Args: + project_dir: Directory to register. If None, uses current working directory. + """ + if project_dir is None: + project_dir = Path.cwd() + else: + project_dir = Path(project_dir) + + # Only register directories that have some kind of LEANN content. + # Check CLI-format first to avoid an expensive rglob on large directories. + has_cli_indexes = (project_dir / ".leann" / "indexes").exists() + if not has_cli_indexes and not any(project_dir.rglob("*.leann.meta.json")): + # Don't register if there are no LEANN indexes + return + + global_registry = Path.home() / ".leann" / "projects.json" + global_registry.parent.mkdir(exist_ok=True) + + project_str = str(project_dir.resolve()) + + # Load existing registry + projects = [] + if global_registry.exists(): + try: + with open(global_registry) as f: + projects = json.load(f) + except Exception: + logger.debug("Could not load existing project registry") + projects = [] + + # Add project if not already present + if project_str not in projects: + projects.append(project_str) + + # Save updated registry + try: + with open(global_registry, "w") as f: + json.dump(projects, f, indent=2) + logger.debug(f"Registered project directory: {project_str}") + except Exception as e: + logger.warning(f"Could not save project registry: {e}") diff --git a/packages/leann-core/src/leann/searcher_base.py b/packages/leann-core/src/leann/searcher_base.py new file mode 100644 index 0000000..a891782 --- /dev/null +++ b/packages/leann-core/src/leann/searcher_base.py @@ -0,0 +1,231 @@ +import json +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any, Literal, Optional + +import numpy as np + +from .embedding_server_manager import EmbeddingServerManager +from .interface import LeannBackendSearcherInterface + + +class BaseSearcher(LeannBackendSearcherInterface, ABC): + """ + Abstract base class for Leann searchers, containing common logic for + loading metadata, managing embedding servers, and handling file paths. + """ + + def __init__(self, index_path: str, backend_module_name: str, **kwargs): + """ + Initializes the BaseSearcher. + + Args: + index_path: Path to the Leann index file (e.g., '.../my_index.leann'). + backend_module_name: The specific embedding server module to use + (e.g., 'leann_backend_hnsw.hnsw_embedding_server'). + **kwargs: Additional keyword arguments. + """ + self.index_path = Path(index_path) + self.index_dir = self.index_path.parent + self.meta = kwargs.get("meta", self._load_meta()) + + if not self.meta: + raise ValueError("Searcher requires metadata from .meta.json.") + + self.dimensions = self.meta.get("dimensions") + if not self.dimensions: + raise ValueError("Dimensions not found in Leann metadata.") + + self.embedding_model = self.meta.get("embedding_model") + if not self.embedding_model: + print("WARNING: embedding_model not found in meta.json. Recompute will fail.") + + self.embedding_mode = self.meta.get("embedding_mode", "sentence-transformers") + self.embedding_options = self.meta.get("embedding_options", {}) + self.enable_warmup = bool(kwargs.get("enable_warmup", True)) + self.use_daemon = bool(kwargs.get("use_daemon", True)) + self.daemon_ttl_seconds = int(kwargs.get("daemon_ttl_seconds", 900)) + + self.embedding_server_manager = EmbeddingServerManager( + backend_module_name=backend_module_name, + ) + + def _load_meta(self) -> dict[str, Any]: + """Loads the metadata file associated with the index.""" + # This is the corrected logic for finding the meta file. + meta_path = self.index_dir / f"{self.index_path.name}.meta.json" + if not meta_path.exists(): + raise FileNotFoundError(f"Leann metadata file not found at {meta_path}") + with open(meta_path, encoding="utf-8") as f: + return json.load(f) + + def _ensure_server_running( + self, passages_source_file: str, port: Optional[int], **kwargs + ) -> int: + """ + Ensures the embedding server is running if recompute is needed. + This is a helper for subclasses. + """ + if not self.embedding_model: + raise ValueError("Cannot use recompute mode without 'embedding_model' in meta.json.") + + # Get distance_metric from meta if not provided in kwargs + distance_metric = ( + kwargs.get("distance_metric") + or self.meta.get("backend_kwargs", {}).get("distance_metric") + or "mips" + ) + + # Filter out ALL prompt templates from provider_options during search + # Templates are applied in compute_query_embedding (line 109-110) BEFORE server call + # The server should never apply templates during search to avoid double-templating + search_provider_options = { + k: v + for k, v in self.embedding_options.items() + if k not in ("build_prompt_template", "query_prompt_template", "prompt_template") + } + + server_started, actual_port = self.embedding_server_manager.start_server( + port=port if port is not None else 5557, + model_name=self.embedding_model, + embedding_mode=self.embedding_mode, + passages_file=passages_source_file, + distance_metric=distance_metric, + enable_warmup=kwargs.get("enable_warmup", self.enable_warmup), + use_daemon=kwargs.get("use_daemon", self.use_daemon), + daemon_ttl_seconds=kwargs.get("daemon_ttl_seconds", self.daemon_ttl_seconds), + provider_options=search_provider_options, + ) + if not server_started: + raise RuntimeError(f"Failed to start embedding server on port {actual_port}") + + return actual_port + + def compute_query_embedding( + self, + query: str, + use_server_if_available: bool = True, + zmq_port: Optional[int] = None, + query_template: Optional[str] = None, + ) -> np.ndarray: + """ + Compute embedding for a query string. + + Args: + query: The query string to embed + zmq_port: ZMQ port for embedding server + use_server_if_available: Whether to try using embedding server first + query_template: Optional prompt template to prepend to query + + Returns: + Query embedding as numpy array + """ + # Apply query template BEFORE any computation path + # This ensures template is applied consistently for both server and fallback paths + if query_template: + query = f"{query_template}{query}" + + # Try to use embedding server if available and requested + if use_server_if_available: + try: + # TODO: Maybe we can directly use this port here? + # For this internal method, it's ok to assume that the server is running + # on that port? + + # Ensure we have a server with passages_file for compatibility + passages_source_file = self.index_dir / f"{self.index_path.name}.meta.json" + # Convert to absolute path to ensure server can find it + zmq_port = self._ensure_server_running( + str(passages_source_file.resolve()), + zmq_port, + enable_warmup=self.enable_warmup, + use_daemon=self.use_daemon, + daemon_ttl_seconds=self.daemon_ttl_seconds, + ) + + return self._compute_embedding_via_server([query], zmq_port)[ + 0:1 + ] # Return (1, D) shape + except Exception as e: + print(f"⚠️ Embedding server failed: {e}") + print("⏭️ Falling back to direct model loading...") + + # Fallback to direct computation + from .embedding_compute import compute_embeddings + + embedding_mode = self.meta.get("embedding_mode", "sentence-transformers") + return compute_embeddings( + [query], + self.embedding_model, + embedding_mode, + provider_options=self.embedding_options, + ) + + def _compute_embedding_via_server(self, chunks: list, zmq_port: int) -> np.ndarray: + """Compute embeddings using the ZMQ embedding server.""" + import msgpack + import zmq + + try: + context = zmq.Context() + socket = context.socket(zmq.REQ) + socket.setsockopt(zmq.RCVTIMEO, 30000) # 30 second timeout + socket.connect(f"tcp://localhost:{zmq_port}") + + # Send embedding request + request = chunks + request_bytes = msgpack.packb(request) + socket.send(request_bytes) + + # Wait for response + response_bytes = socket.recv() + response = msgpack.unpackb(response_bytes) + + socket.close() + context.term() + + # Convert response to numpy array + if isinstance(response, list) and len(response) > 0: + return np.array(response, dtype=np.float32) + else: + raise RuntimeError("Invalid response from embedding server") + + except Exception as e: + raise RuntimeError(f"Failed to compute embeddings via server: {e}") + + @abstractmethod + def search( + self, + query: np.ndarray, + top_k: int, + complexity: int = 64, + beam_width: int = 1, + prune_ratio: float = 0.0, + recompute_embeddings: bool = False, + pruning_strategy: Literal["global", "local", "proportional"] = "global", + zmq_port: Optional[int] = None, + **kwargs, + ) -> dict[str, Any]: + """ + Search for the top_k nearest neighbors of the query vector. + + Args: + query: Query vectors (B, D) where B is batch size, D is dimension + top_k: Number of nearest neighbors to return + complexity: Search complexity/candidate list size, higher = more accurate but slower + beam_width: Number of parallel search paths/IO requests per iteration + prune_ratio: Ratio of neighbors to prune via approximate distance (0.0-1.0) + recompute_embeddings: Whether to fetch fresh embeddings from server vs use stored PQ codes + pruning_strategy: PQ candidate selection strategy - "global" (default), "local", or "proportional" + zmq_port: ZMQ port for embedding server communication. Must be provided if recompute_embeddings is True. + **kwargs: Backend-specific parameters (e.g., batch_size, dedup_node_dis, etc.) + + Returns: + Dict with 'labels' (list of lists) and 'distances' (ndarray) + """ + pass + + def __del__(self): + """Ensures the embedding server is stopped when the searcher is destroyed.""" + if hasattr(self, "embedding_server_manager"): + self.embedding_server_manager.stop_server() diff --git a/packages/leann-core/src/leann/server.py b/packages/leann-core/src/leann/server.py new file mode 100644 index 0000000..890e56b --- /dev/null +++ b/packages/leann-core/src/leann/server.py @@ -0,0 +1,213 @@ +""" +Minimal HTTP API server for LEANN. + +This exposes LEANN indexes over HTTP so clients can: +- List available indexes +- Run semantic search against an index + +The design intentionally keeps dependencies optional: +- FastAPI + pydantic are imported lazily inside `create_app()` +- uvicorn is imported lazily inside `serve_async()` (and `main()` calls that) + +This way, core LEANN usage is unaffected unless you actually run the server. +""" + +from __future__ import annotations + +import asyncio +import os +from pathlib import Path +from typing import Any + +from pydantic import BaseModel as _BaseModel + +from .api import LeannSearcher +from .cli import LeannCLI + + +class SearchRequest(_BaseModel): + query: str + top_k: int = 5 + complexity: int = 64 + beam_width: int = 1 + prune_ratio: float = 0.0 + recompute_embeddings: bool = True + pruning_strategy: str = "global" + use_grep: bool = False + + +class SearchResultModel(_BaseModel): + id: str + score: float + text: str + metadata: dict[str, Any] + + +def _ensure_fastapi(): + """Lazy import FastAPI and Pydantic, with a clear error if missing.""" + try: + from fastapi import FastAPI, HTTPException + from pydantic import BaseModel + except ImportError as e: # pragma: no cover - dependency error path + raise RuntimeError( + "FastAPI and pydantic are required for the LEANN HTTP server.\n" + "Install them with:\n\n" + " uv pip install 'fastapi>=0.115' 'pydantic>=2' 'uvicorn[standard]'\n" + ) from e + + return FastAPI, HTTPException, BaseModel + + +def _resolve_index_path(index_name: str) -> str: + """ + Resolve an index path for the HTTP server. + + For now we use the same convention as the CLI: + - Look in the current project's `.leann/indexes//documents.leann`. + + This keeps behavior predictable when running `leann serve` from a project root. + """ + cli = LeannCLI() + index_path = cli.get_index_path(index_name) + if not cli.index_exists(index_name): + raise FileNotFoundError( + f"Index '{index_name}' not found in current project. " + f"Build it with: leann build {index_name} --docs ./your_docs" + ) + return index_path + + +def _list_current_project_indexes() -> list[dict[str, Any]]: + """ + Return machine-readable index metadata for the current project. + + This mirrors `LeannCLI.list_indexes()` but only for the current project + and without printing to stdout. + """ + cli = LeannCLI() + current_path = Path.cwd() + indexes: list[dict[str, Any]] = [] + + for idx in cli._discover_indexes_in_project(current_path): + # `idx` includes keys like: name, type (cli/app), status, size_mb + indexes.append( + { + "name": idx.get("name", ""), + "type": idx.get("type", "cli"), + "status": idx.get("status", ""), + "size_mb": idx.get("size_mb", 0.0), + "project_path": str(current_path), + } + ) + + return indexes + + +def create_app(): + """ + Create and return a FastAPI application exposing LEANN as a simple vector DB. + + Endpoints: + - GET /health -> basic health check + - GET /indexes -> list indexes in current project + - POST /indexes/{name}/search -> semantic search + """ + + FastAPI, HTTPException, BaseModel = _ensure_fastapi() + + app = FastAPI( + title="LEANN Vector DB Server", + description=( + "HTTP API for querying LEANN indexes.\n\n" + "This is a minimal first version focused on search. " + "Run it from a project root where `.leann/indexes` exists." + ), + version="0.1.0", + ) + + @app.get("/health") + async def health() -> dict[str, str]: + return {"status": "ok"} + + @app.get("/indexes") + async def list_indexes() -> list[dict[str, Any]]: + """ + List indexes for the current project (the working directory where the server runs). + """ + return _list_current_project_indexes() + + @app.post("/indexes/{index_name}/search", response_model=list[SearchResultModel]) + async def search_index(index_name: str, body: SearchRequest): + """ + Run semantic search against an existing LEANN index. + """ + try: + index_path = _resolve_index_path(index_name) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + + searcher = LeannSearcher(index_path=index_path) + results = searcher.search( + query=body.query, + top_k=body.top_k, + complexity=body.complexity, + beam_width=body.beam_width, + prune_ratio=body.prune_ratio, + recompute_embeddings=body.recompute_embeddings, + pruning_strategy=body.pruning_strategy, # type: ignore[arg-type] + use_grep=body.use_grep, + ) + + # Normalize into JSON-serializable structures + return [ + SearchResultModel( + id=r.id, + score=float(r.score), + text=r.text, + metadata=dict(r.metadata or {}), + ) + for r in results + ] + + return app + + +async def serve_async() -> None: + """ + Run the HTTP server on the current asyncio event loop. + + Use this from async contexts (e.g. ``leann serve``, which runs under + ``asyncio.run()``). Do not use ``uvicorn.run()`` there: it starts its own + loop and raises "Cannot run the event loop while another loop is running". + """ + try: + import uvicorn + except ImportError as e: # pragma: no cover - dependency error path + raise RuntimeError( + "uvicorn is required to run the LEANN HTTP server.\n" + "Install it with:\n\n" + " uv pip install 'uvicorn[standard]'\n" + ) from e + + app = create_app() + host = os.getenv("LEANN_SERVER_HOST", "127.0.0.1") + port = int(os.getenv("LEANN_SERVER_PORT", "8000")) + config = uvicorn.Config(app, host=host, port=port) + server = uvicorn.Server(config) + await server.serve() + + +def main() -> None: + """ + Entrypoint to run the HTTP server with uvicorn. + + Example: + uv run python -m leann.server + # or: + leann serve + """ + asyncio.run(serve_async()) + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/packages/leann-core/src/leann/settings.py b/packages/leann-core/src/leann/settings.py new file mode 100644 index 0000000..3d6afb8 --- /dev/null +++ b/packages/leann-core/src/leann/settings.py @@ -0,0 +1,167 @@ +"""Runtime configuration helpers for LEANN.""" + +from __future__ import annotations + +import json +import logging +import os +from typing import Any + +logger = logging.getLogger(__name__) + +# Default fallbacks to preserve current behaviour while keeping them in one place. +_DEFAULT_OLLAMA_HOST = "http://localhost:11434" +_DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1" +_DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com" +_DEFAULT_MINIMAX_BASE_URL = "https://api.minimax.io/v1" +_DEFAULT_NOVITA_BASE_URL = "https://api.novita.ai/openai" + + +def _clean_url(value: str) -> str: + """Normalize URL strings by stripping trailing slashes.""" + + return value.rstrip("/") if value else value + + +def resolve_ollama_host(explicit: str | None = None) -> str: + """Resolve the Ollama-compatible endpoint to use.""" + + candidates = ( + explicit, + os.getenv("LEANN_LOCAL_LLM_HOST"), + os.getenv("LEANN_OLLAMA_HOST"), + os.getenv("OLLAMA_HOST"), + os.getenv("LOCAL_LLM_ENDPOINT"), + ) + + for candidate in candidates: + if candidate: + return _clean_url(candidate) + + return _clean_url(_DEFAULT_OLLAMA_HOST) + + +def resolve_openai_base_url(explicit: str | None = None) -> str: + """Resolve the base URL for OpenAI-compatible services.""" + + candidates = ( + explicit, + os.getenv("LEANN_OPENAI_BASE_URL"), + os.getenv("OPENAI_BASE_URL"), + os.getenv("LOCAL_OPENAI_BASE_URL"), + ) + + for candidate in candidates: + if candidate: + return _clean_url(candidate) + + return _clean_url(_DEFAULT_OPENAI_BASE_URL) + + +def resolve_anthropic_base_url(explicit: str | None = None) -> str: + """Resolve the base URL for Anthropic-compatible services.""" + + candidates = ( + explicit, + os.getenv("LEANN_ANTHROPIC_BASE_URL"), + os.getenv("ANTHROPIC_BASE_URL"), + os.getenv("LOCAL_ANTHROPIC_BASE_URL"), + ) + + for candidate in candidates: + if candidate: + return _clean_url(candidate) + + return _clean_url(_DEFAULT_ANTHROPIC_BASE_URL) + + +def resolve_openai_api_key(explicit: str | None = None) -> str | None: + """Resolve the API key for OpenAI-compatible services.""" + + if explicit: + return explicit + + return os.getenv("OPENAI_API_KEY") + + +def resolve_anthropic_api_key(explicit: str | None = None) -> str | None: + """Resolve the API key for Anthropic services.""" + + if explicit: + return explicit + + return os.getenv("ANTHROPIC_API_KEY") + + +def resolve_minimax_base_url(explicit: str | None = None) -> str: + """Resolve the base URL for MiniMax-compatible services.""" + + candidates = ( + explicit, + os.getenv("LEANN_MINIMAX_BASE_URL"), + os.getenv("MINIMAX_BASE_URL"), + ) + + for candidate in candidates: + if candidate: + return _clean_url(candidate) + + return _clean_url(_DEFAULT_MINIMAX_BASE_URL) + + +def resolve_minimax_api_key(explicit: str | None = None) -> str | None: + """Resolve the API key for MiniMax services.""" + + if explicit: + return explicit + + return os.getenv("MINIMAX_API_KEY") + + +def resolve_novita_base_url(explicit: str | None = None) -> str: + """Resolve the base URL for Novita AI services.""" + + candidates = ( + explicit, + os.getenv("LEANN_NOVITA_BASE_URL"), + os.getenv("NOVITA_BASE_URL"), + os.getenv("OPENAI_BASE_URL"), # Fallback to OpenAI base URL + ) + + for candidate in candidates: + if candidate: + return _clean_url(candidate) + + return _clean_url(_DEFAULT_NOVITA_BASE_URL) + + +def resolve_novita_api_key(explicit: str | None = None) -> str | None: + """Resolve the API key for Novita AI services.""" + + if explicit: + return explicit + + novita_key = os.getenv("NOVITA_API_KEY") + if novita_key: + return novita_key + + openai_key = os.getenv("OPENAI_API_KEY") + if openai_key: + logger.warning( + "NOVITA_API_KEY not set, falling back to OPENAI_API_KEY. " + "This may cause authentication issues if the OpenAI key is not valid for Novita AI." + ) + return openai_key + + +def encode_provider_options(options: dict[str, Any] | None) -> str | None: + """Serialize provider options for child processes.""" + + if not options: + return None + + try: + return json.dumps(options) + except (TypeError, ValueError): + # Fall back to empty payload if serialization fails + return None diff --git a/packages/leann-core/src/leann/sync.py b/packages/leann-core/src/leann/sync.py new file mode 100644 index 0000000..fa04bd1 --- /dev/null +++ b/packages/leann-core/src/leann/sync.py @@ -0,0 +1,300 @@ +import logging +import os +import pickle +from dataclasses import dataclass, field +from hashlib import sha256 +from pathlib import Path +from typing import Optional + +logger = logging.getLogger(__name__) + +# Keep in sync with leann build's default extension allowlist (load_documents). +DEFAULT_INDEX_EXTENSIONS: list[str] = [ + ".txt", + ".md", + ".docx", + ".pptx", + ".py", + ".js", + ".ts", + ".jsx", + ".tsx", + ".java", + ".cpp", + ".c", + ".h", + ".hpp", + ".cs", + ".go", + ".rs", + ".rb", + ".php", + ".swift", + ".kt", + ".scala", + ".r", + ".sql", + ".sh", + ".bash", + ".zsh", + ".fish", + ".ps1", + ".bat", + ".json", + ".yaml", + ".yml", + ".xml", + ".toml", + ".ini", + ".cfg", + ".conf", + ".html", + ".css", + ".scss", + ".less", + ".vue", + ".svelte", + ".ipynb", + ".R", + ".jl", +] + + +def hash_data(data: str | bytes): + if isinstance(data, str): + data = data.encode() + return sha256(data).hexdigest() + + +def parse_include_extensions(custom_file_types: Optional[str]) -> list[str]: + """Return the extension allowlist used for sync/watch (matches build defaults).""" + if not custom_file_types: + return list(DEFAULT_INDEX_EXTENSIONS) + extensions = [ext.strip() for ext in custom_file_types.split(",") if ext.strip()] + return [ext if ext.startswith(".") else f".{ext}" for ext in extensions] + + +def _extension_allowed(path: Path, include_extensions: list[str]) -> bool: + allowed = {ext.lower() for ext in include_extensions} + return path.suffix.lower() in allowed + + +def _path_has_hidden_segment(path: Path) -> bool: + return any(part.startswith(".") and part not in (".", "..") for part in path.parts) + + +def _hash_file_bytes(path: Path) -> str: + with open(path, "rb") as f: + return hash_data(f.read()) + + +def _iter_directory_files( + root_dir: str, + include_extensions: list[str], + include_hidden: bool, +) -> list[str]: + root = Path(root_dir).resolve() + if not root.is_dir(): + return [] + + paths: list[str] = [] + for dirpath, dirnames, filenames in os.walk(root): + if not include_hidden: + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + current = Path(dirpath) + for name in filenames: + if not include_hidden and name.startswith("."): + continue + file_path = (current / name).resolve() + try: + rel = file_path.relative_to(root) + except ValueError: + continue + if not include_hidden and _path_has_hidden_segment(rel): + continue + if not _extension_allowed(file_path, include_extensions): + continue + if file_path.is_file(): + paths.append(str(file_path)) + return paths + + +@dataclass +class MerkleTreeNode: + ## TODO: this merkle tree only has two layer, need to improve if we want to scale to large codebase + hash: str + data: str + children: dict[str, "MerkleTreeNode"] = field(default_factory=dict) + parent_id: str | None = None + + +class MerkleTree: + def __init__(self): + self.nodes: dict[str, MerkleTreeNode] = {} + self.root: MerkleTreeNode | None = None + + def add_node(self, data: str, parent_id=None, hash: Optional[str] = None): + hash = hash_data(data) if hash is None else hash + + node = MerkleTreeNode(hash=hash, data=data, parent_id=parent_id) + self.nodes[hash] = node + + if parent_id is None: + self.root = node + else: + self.nodes[parent_id].children[hash] = node + + return hash + + def compare_with(self, other: "MerkleTree"): + """ + Simple comparison of two flat trees. Check the individual file hashes + only if the root has changed, otherwise return no changes. + """ + assert self.root is not None and other.root is not None + + if self.root.hash == other.root.hash: + return [], [], [] + + old_files = self.root.children + new_files = other.root.children + + all_nodes = new_files.keys() | old_files.keys() + + added, removed, modified = [], [], [] + for path in all_nodes: + if path in new_files and path in old_files: + if new_files[path].data != old_files[path].data: + modified.append(path) + elif path in new_files and path not in old_files: + added.append(path) + else: + removed.append(path) + + return added, removed, modified + + +class FileSynchronizer: + def __init__( + self, + root_dir: Optional[str] = None, + explicit_files: Optional[list[str]] = None, + ignore_patterns: Optional[list] = None, + include_extensions: Optional[list[str]] = None, + include_hidden: bool = False, + auto_load=True, + snapshot_path: Optional[str] = None, + ): + self.root_dir = str(Path(root_dir).resolve()) if root_dir else None + self.explicit_files = ( + [str(Path(path).resolve()) for path in explicit_files] if explicit_files else [] + ) + if self.root_dir is None and not self.explicit_files: + raise ValueError("FileSynchronizer requires root_dir and/or explicit_files") + if self.root_dir is not None and not os.path.isdir(self.root_dir): + raise ValueError("This is not a valid directory") + + self.ignore_patterns = ignore_patterns + self.include_extensions = include_extensions or list(DEFAULT_INDEX_EXTENSIONS) + self.include_hidden = include_hidden + self._custom_snapshot_path = snapshot_path + self._pending_tree: Optional[MerkleTree] = None + self.tree: Optional[MerkleTree] = None + if auto_load: + self.load_snapshot() + + def _collect_paths(self) -> list[str]: + paths: list[str] = [] + if self.root_dir: + paths.extend( + _iter_directory_files( + self.root_dir, + self.include_extensions, + self.include_hidden, + ) + ) + for file_path in self.explicit_files: + path = Path(file_path).resolve() + if not path.is_file(): + continue + if not self.include_hidden and _path_has_hidden_segment(path): + continue + if not _extension_allowed(path, self.include_extensions): + continue + paths.append(str(path)) + return sorted(set(paths)) + + def generate_file_hashes(self): + file_hashes: dict[str, str] = {} + for file_path in self._collect_paths(): + try: + file_hashes[file_path] = _hash_file_bytes(Path(file_path)) + except OSError: + logger.warning("Cannot hash file %s", file_path) + return file_hashes + + def build_merkle_tree(self, file_hashes): + """ + Build a flat merkle tree suitable for quick checking of file changes. + """ + tree = MerkleTree() + + sorted_paths = sorted(file_hashes) + root_data = "".join(path + file_hashes[path] for path in sorted_paths) + + root_id = tree.add_node(root_data) + + for path in sorted_paths: + tree.add_node(file_hashes[path], parent_id=root_id, hash=path) + + return tree + + def detect_changes(self) -> tuple[list[str], list[str], list[str]]: + """Detect changes without persisting. Call commit() after successful processing.""" + file_hashes = self.generate_file_hashes() + new_tree = self.build_merkle_tree(file_hashes) + self._pending_tree = new_tree + + if self.tree is None: + return list(file_hashes.keys()), [], [] + + return self.tree.compare_with(new_tree) + + def commit(self): + """Persist the pending snapshot after successful processing.""" + if self._pending_tree is not None: + self.tree = self._pending_tree + self._pending_tree = None + self.save_snapshot() + + def create_snapshot(self): + """Build and persist a snapshot from the current file state (for initial / forced builds).""" + file_hashes = self.generate_file_hashes() + self.tree = self.build_merkle_tree(file_hashes) + self.save_snapshot() + + def check_for_changes(self) -> tuple[list[str], list[str], list[str]]: + """Detect and auto-commit changes (convenience wrapper).""" + changes = self.detect_changes() + self.commit() + return changes + + @property + def snapshot_path(self): + if self._custom_snapshot_path: + return self._custom_snapshot_path + base = self.root_dir or "files" + return f"{base}.sync_context.pickle" + + def save_snapshot(self): + assert self.tree is not None + + with open(self.snapshot_path, "wb") as f: + pickle.dump(self.tree, f) + + def load_snapshot(self): + try: + with open(self.snapshot_path, "rb") as f: + self.tree = pickle.load(f) + except FileNotFoundError: + self.tree = None diff --git a/packages/leann-core/src/leann/web_search.py b/packages/leann-core/src/leann/web_search.py new file mode 100644 index 0000000..72d4e70 --- /dev/null +++ b/packages/leann-core/src/leann/web_search.py @@ -0,0 +1,65 @@ +import json +import logging +import os +from typing import Any + +import requests + +logger = logging.getLogger(__name__) + + +class WebSearcher: + """Helper class for performing web searches via Serper API.""" + + def __init__(self, api_key: str | None = None, jina_api_key: str | None = None): + self.api_key = api_key or os.getenv("SERPER_API_KEY") + self.jina_api_key = jina_api_key or os.getenv("JINA_API_KEY") + if not self.api_key: + logger.warning("No SERPER_API_KEY found. Web search will not be available.") + + def search(self, query: str, top_k: int = 5) -> list[dict[str, Any]]: + if not self.api_key: + return [{"title": "Error", "link": "", "snippet": "Missing SERPER_API_KEY env var"}] + + url = "https://google.serper.dev/search" + payload = json.dumps({"q": query, "num": top_k}) + headers = {"X-API-KEY": self.api_key, "Content-Type": "application/json"} + + try: + response = requests.request("POST", url, headers=headers, data=payload) + response.raise_for_status() + data = response.json() + + results = [] + + if "organic" in data: + for item in data["organic"][:top_k]: + results.append( + { + "title": item.get("title", ""), + "link": item.get("link", ""), + "snippet": item.get("snippet", ""), + } + ) + + return results + except Exception as e: + logger.error(f"Web search failed: {e}") + return [{"title": "Error", "link": "", "snippet": f"Web search failed: {e!s}"}] + + def get_page_content(self, url: str) -> str: + """Fetch page content using Jina AI Reader (https://jina.ai/reader).""" + jina_url = f"https://r.jina.ai/{url}" + + headers = {"X-Return-Format": "markdown"} + if self.jina_api_key: + headers["Authorization"] = f"Bearer {self.jina_api_key}" + + try: + logger.info(f"Fetching content from: {url}") + response = requests.get(jina_url, headers=headers) + response.raise_for_status() + return response.text + except Exception as e: + logger.error(f"Failed to fetch content from {url}: {e}") + return f"Error fetching content: {e!s}" diff --git a/packages/leann-mcp/README.md b/packages/leann-mcp/README.md new file mode 100644 index 0000000..8af4556 --- /dev/null +++ b/packages/leann-mcp/README.md @@ -0,0 +1,158 @@ +# πŸ”₯ LEANN Claude Code Integration + +Transform your development workflow with intelligent code assistance using LEANN's semantic search directly in Claude Code. + +For agent-facing discovery details, see `llms.txt` in the repository root. + +**Backend work (IVF, incremental build, reindex):** See [BACKEND.md](./BACKEND.md) for how the MCP server relates to backends and for notes on #231, #89, and #141. + +## Prerequisites + +Install LEANN globally for MCP integration (with default backend): + +```bash +uv tool install leann-core --with leann +``` +This installs the `leann` CLI into an isolated tool environment and includes both backends so `leann build` works out-of-the-box. + +## πŸš€ Quick Setup + +Add the LEANN MCP server to Claude Code. Choose the scope based on how widely you want it available. Below is the command to install it globally; if you prefer a local install, skip this step: + +```bash +# Global (recommended): available in all projects for your user +claude mcp add --scope user leann-server -- leann_mcp +``` + +- `leann-server`: the display name of the MCP server in Claude Code (you can change it). +- `leann_mcp`: the Python entry point installed with LEANN that starts the MCP server. + +Verify it is registered globally: + +```bash +claude mcp list | cat +``` + +## πŸ› οΈ Available Tools + +Once connected, you'll have access to these powerful semantic search tools in Claude Code: + +- **`leann_search`** - Semantic code search with file paths, scores, and context +- **`leann_list`** - List all available indexes across your projects +- **`leann_build`** - Build or incrementally update an index (keeps it current as code changes) +- **`leann_status`** - Show index details: backend, embedding model, chunk count, file count, size + + +## 🎯 Quick Start Example + +```bash +# Add locally if you did not add it globally (current folder only; default if --scope is omitted) +claude mcp add leann-server -- leann_mcp + +# Build an index for your project (change to your actual path) +# See the advanced examples below for more ways to configure indexing +# Set the index name (replace 'my-project' with your own) +leann build my-project --docs $(git ls-files) + +# Start Claude Code +claude +``` +**Performance tip**: For maximum speed when storage space is not a concern, add the `--no-recompute` flag to your build command. This materializes all tensors and stores them on disk, avoiding recomputation on subsequent builds: + +```bash +leann build my-project --docs $(git ls-files) --no-recompute +``` + +## πŸš€ Advanced Usage Examples to build the index + +### Index Entire Git Repository +```bash +# Index all tracked files in your Git repository. +# Note: submodules are currently skipped; we can add them back if needed. +leann build my-repo --docs $(git ls-files) --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw + +# Index only tracked Python files from Git. +leann build my-python-code --docs $(git ls-files "*.py") --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw + +# If you encounter empty requests caused by empty files (e.g., __init__.py), exclude zero-byte files. Thanks @ww2283 for pointing [that](https://github.com/yichuan-w/LEANN/issues/48) out +leann build leann-prospec-lig --docs $(find ./src -name "*.py" -not -empty) --embedding-mode openai --embedding-model text-embedding-3-small +``` + +### Multiple Directories and Files +```bash +# Index multiple directories +leann build my-codebase --docs ./src ./tests ./docs ./config --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw + +# Mix files and directories +leann build my-project --docs ./README.md ./src/ ./package.json ./docs/ --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw + +# Specific files only +leann build my-configs --docs ./tsconfig.json ./package.json ./webpack.config.js --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw +``` + +### Advanced Git Integration +```bash +# Index recently modified files +leann build recent-changes --docs $(git diff --name-only HEAD~10..HEAD) --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw + +# Index files matching pattern +leann build frontend --docs $(git ls-files "*.tsx" "*.ts" "*.jsx" "*.js") --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw + +# Index documentation and config files +leann build docs-and-configs --docs $(git ls-files "*.md" "*.yml" "*.yaml" "*.json" "*.toml") --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw +``` + + +## **Try this in Claude Code:** +``` +Help me understand this codebase. List available indexes and search for authentication patterns. +``` + +

+ LEANN in Claude Code +

+ +If you see a prompt asking whether to proceed with LEANN, you can now use it in your chat! + +## 🧠 How It Works + +The integration consists of three key components working seamlessly together: + +- **`leann`** - Core CLI tool for indexing and searching (installed globally via `uv tool install`) +- **`leann_mcp`** - MCP server that wraps `leann` commands for Claude Code integration +- **Claude Code** - Calls `leann_mcp`, which executes `leann` commands and returns intelligent results + +## πŸ“ File Support + +LEANN understands **30+ file types** including: +- **Programming**: Python, JavaScript, TypeScript, Java, Go, Rust, C++, C# +- **Data**: SQL, YAML, JSON, CSV, XML +- **Documentation**: Markdown, TXT, PDF +- **And many more!** + +## πŸ’Ύ Storage & Organization + +- **Project indexes**: Stored in `.leann/` directory (just like `.git`) +- **Global registry**: Project tracking at `~/.leann/projects.json` +- **Multi-project support**: Switch between different codebases seamlessly +- **Portable**: Transfer indexes between machines with minimal overhead + +## πŸ—‘οΈ Uninstalling + +To remove the LEANN MCP server from Claude Code: + +```bash +claude mcp remove leann-server +``` +To remove LEANN +``` +uv pip uninstall leann leann-backend-hnsw leann-core +``` + +To globally remove LEANN (for version update) +``` +uv tool list | cat +uv tool uninstall leann-core +command -v leann || echo "leann gone" +command -v leann_mcp || echo "leann_mcp gone" +``` diff --git a/packages/leann/README.md b/packages/leann/README.md new file mode 100644 index 0000000..2c3a401 --- /dev/null +++ b/packages/leann/README.md @@ -0,0 +1,43 @@ +# LEANN - The smallest vector index in the world + +LEANN is a revolutionary vector database that democratizes personal AI. Transform your laptop into a powerful RAG system that can index and search through millions of documents while using **97% less storage** than traditional solutions **without accuracy loss**. + +## Installation + +```bash +# Default installation (HNSW, DiskANN, and IVF backends) +uv pip install leann + +# CPU-only install (Linux) +uv pip install \ + --default-index https://download.pytorch.org/whl/cpu \ + --index https://pypi.org/simple \ + --index-strategy first-index \ + "leann[cpu]" +``` + +## Quick Start + +```python +from leann import LeannBuilder, LeannSearcher, LeannChat +from pathlib import Path +INDEX_PATH = str(Path("./").resolve() / "demo.leann") + +# Build an index (choose backend: "hnsw", "diskann", or "ivf" for incremental updates) +builder = LeannBuilder(backend_name="hnsw") # or "diskann" / "ivf" +builder.add_text("LEANN saves 97% storage compared to traditional vector databases.") +builder.add_text("Tung Tung Tung Sahur calledβ€”they need their banana‑crocodile hybrid back") +builder.build_index(INDEX_PATH) + +# Search +searcher = LeannSearcher(INDEX_PATH) +results = searcher.search("fantastical AI-generated creatures", top_k=1) + +# Chat with your data +chat = LeannChat(INDEX_PATH, llm_config={"type": "hf", "model": "Qwen/Qwen3-0.6B"}) +response = chat.ask("How much storage does LEANN save?", top_k=1) +``` + +## License + +MIT License diff --git a/packages/leann/__init__.py b/packages/leann/__init__.py new file mode 100644 index 0000000..88a30b7 --- /dev/null +++ b/packages/leann/__init__.py @@ -0,0 +1,12 @@ +""" +LEANN - Low-storage Embedding Approximation for Neural Networks + +A revolutionary vector database that democratizes personal AI. +""" + +__version__ = "0.1.0" + +# Re-export main API from leann-core +from leann_core import LeannBuilder, LeannChat, LeannSearcher + +__all__ = ["LeannBuilder", "LeannChat", "LeannSearcher"] diff --git a/packages/leann/pyproject.toml b/packages/leann/pyproject.toml new file mode 100644 index 0000000..3b5e070 --- /dev/null +++ b/packages/leann/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "leann" +version = "0.3.8" +description = "LEANN - The smallest vector index in the world. RAG Everything with LEANN!" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [ + { name = "LEANN Team" } +] +keywords = ["vector-database", "rag", "embeddings", "search", "ai"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +# Default installation: core + hnsw + diskann + ivf (incremental updates) +dependencies = [ + "leann-core>=0.1.0", + "leann-backend-hnsw>=0.1.0", + "leann-backend-diskann>=0.1.0", + "leann-backend-ivf>=0.3.6", +] + +[project.optional-dependencies] +cpu = [ + "leann-core[cpu]>=0.1.0", +] + +[project.urls] +Repository = "https://github.com/yichuan-w/LEANN" +Issues = "https://github.com/yichuan-w/LEANN/issues" diff --git a/packages/wechat-exporter/__init__.py b/packages/wechat-exporter/__init__.py new file mode 100644 index 0000000..a9a2c5b --- /dev/null +++ b/packages/wechat-exporter/__init__.py @@ -0,0 +1 @@ +__all__ = [] diff --git a/packages/wechat-exporter/main.py b/packages/wechat-exporter/main.py new file mode 100644 index 0000000..7561f05 --- /dev/null +++ b/packages/wechat-exporter/main.py @@ -0,0 +1,144 @@ +import json +import sqlite3 +import xml.etree.ElementTree as ElementTree +from pathlib import Path +from typing import Annotated + +import requests +import typer +from tqdm import tqdm + +app = typer.Typer() + + +def get_safe_path(s: str) -> str: + """ + Remove invalid characters to sanitize a path. + :param s: str to sanitize + :returns: sanitized str + """ + ban_chars = "\\ / : * ? \" ' < > | $ \r \n".replace(" ", "") + for i in ban_chars: + s = s.replace(i, "") + return s + + +def process_history(history: str): + if history.startswith(""): + try: + root = ElementTree.fromstring(history) + title = root.find(".//title").text if root.find(".//title") is not None else None + quoted = ( + root.find(".//refermsg/content").text + if root.find(".//refermsg/content") is not None + else None + ) + if title and quoted: + return {"title": title, "quoted": process_history(quoted)} + if title: + return title + except Exception: + return history + return history + + +def get_message(history: dict | str): + if isinstance(history, dict): + if "title" in history: + return history["title"] + else: + return history + + +def export_chathistory(user_id: str): + res = requests.get( + "http://localhost:48065/wechat/chatlog", + params={"userId": user_id, "count": 100000}, + ).json() + for i in range(len(res["chatLogs"])): + res["chatLogs"][i]["content"] = process_history(res["chatLogs"][i]["content"]) + res["chatLogs"][i]["message"] = get_message(res["chatLogs"][i]["content"]) + return res["chatLogs"] + + +@app.command() +def export_all(dest: Annotated[Path, typer.Argument(help="Destination path to export to.")]): + """ + Export all users' chat history to json files. + """ + if not dest.is_dir(): + if not dest.exists(): + inp = typer.prompt("Destination path does not exist, create it? (y/n)") + if inp.lower() == "y": + dest.mkdir(parents=True) + else: + typer.echo("Aborted.", err=True) + return + else: + typer.echo("Destination path is not a directory!", err=True) + return + all_users = requests.get("http://localhost:48065/wechat/allcontacts").json() + + exported_count = 0 + for user in tqdm(all_users): + try: + usr_chatlog = export_chathistory(user["arg"]) + + # Only write file if there are messages + if len(usr_chatlog) > 0: + out_path = dest / get_safe_path((user["title"] or "") + "-" + user["arg"] + ".json") + with open(out_path, "w", encoding="utf-8") as f: + json.dump(usr_chatlog, f, ensure_ascii=False, indent=2) + exported_count += 1 + except Exception as e: + print(f"Error exporting {user.get('title', 'Unknown')}: {e}") + continue + + print(f"Exported {exported_count} users' chat history to {dest} in json.") + + +@app.command() +def export_sqlite( + dest: Annotated[Path, typer.Argument(help="Destination path to export to.")] = Path( + "chatlog.db" + ), +): + """ + Export all users' chat history to a sqlite database. + """ + connection = sqlite3.connect(dest) + cursor = connection.cursor() + cursor.execute( + "CREATE TABLE IF NOT EXISTS chatlog (id INTEGER PRIMARY KEY AUTOINCREMENT, with_id TEXT, from_user TEXT, to_user TEXT, message TEXT, timest DATETIME, auxiliary TEXT)" + ) + cursor.execute("CREATE INDEX IF NOT EXISTS chatlog_with_id_index ON chatlog (with_id)") + cursor.execute("CREATE TABLE iF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)") + + all_users = requests.get("http://localhost:48065/wechat/allcontacts").json() + for user in tqdm(all_users): + cursor.execute( + "INSERT OR IGNORE INTO users (id, name) VALUES (?, ?)", + (user["arg"], user["title"]), + ) + usr_chatlog = export_chathistory(user["arg"]) + for msg in usr_chatlog: + cursor.execute( + "INSERT INTO chatlog (with_id, from_user, to_user, message, timest, auxiliary) VALUES (?, ?, ?, ?, ?, ?)", + ( + user["arg"], + msg["fromUser"], + msg["toUser"], + msg["message"], + msg["createTime"], + str(msg["content"]), + ), + ) + connection.commit() + + +def main(): + app() + + +if __name__ == "__main__": + main() diff --git a/packages/wechat-exporter/wechattweak-cli b/packages/wechat-exporter/wechattweak-cli new file mode 100755 index 0000000..51e211e Binary files /dev/null and b/packages/wechat-exporter/wechattweak-cli differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f96b96d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,205 @@ +[build-system] +requires = ["setuptools>=61.0", "cmake>=3.24"] +build-backend = "setuptools.build_meta" + +[project] +name = "leann-workspace" +version = "0.1.0" +requires-python = ">=3.10" + +dependencies = [ + "leann-core", + "leann-backend-hnsw", + "typer>=0.12.3", + "numpy>=1.26.0", + "torch", + "tqdm", + "datasets>=2.15.0", + "evaluate", + "colorama", + "boto3", + "protobuf==4.25.3", + "sglang", + "ollama", + "requests>=2.25.0", + "sentence-transformers>=3.0.0", + # Keep Py3.9 compatible below 4.46; allow newer for Py>=3.10 (e.g., ColQwen/ColPali). + "transformers<4.46; python_version < '3.10'", + "transformers>=4.53.1,<4.58; python_version >= '3.10'", + "openai>=1.0.0", + # PDF parsing dependencies - essential for document processing + "PyPDF2>=3.0.0", + "pdfplumber>=0.11.0", + "pymupdf>=1.26.0", + "pypdfium2>=4.30.0", + # LlamaIndex core and readers - updated versions + "llama-index>=0.12.44", + "llama-index-readers-file>=0.4.0", # Essential for PDF parsing + # "llama-index-readers-docling", # Requires Python >= 3.10 + # "llama-index-node-parser-docling", # Requires Python >= 3.10 + "llama-index-vector-stores-faiss>=0.4.0", + "llama-index-embeddings-huggingface>=0.5.5", + # Other dependencies + "ipykernel==6.29.5", + "msgpack>=1.1.1", + "mlx>=0.26.3; sys_platform == 'darwin' and platform_machine == 'arm64'", + "mlx-lm>=0.26.0; sys_platform == 'darwin' and platform_machine == 'arm64'", + "psutil>=5.8.0", + "pybind11>=3.0.0", + "pathspec>=0.12.1", + "nbconvert>=7.16.6", + "gitignore-parser>=0.1.12", + # AST-aware code chunking dependencies + "astchunk>=0.1.0", + "tree-sitter>=0.20.0", + "tree-sitter-python>=0.20.0", + "tree-sitter-java>=0.20.0", + "tree-sitter-c-sharp>=0.20.0", + "tree-sitter-typescript>=0.20.0", + "torchvision>=0.23.0", + "einops", + "seaborn", +] + +[project.optional-dependencies] +diskann = [ + "leann-backend-diskann", +] + +# GPU-accelerated FlashLib (exact k-NN on Triton/CuteDSL) backend. Requires a CUDA GPU. +flashlib = [ + "leann-backend-flashlib", +] + +# GPU-accelerated FlashLib IVF-Flat (approximate-NN) backend. Requires a CUDA GPU. +flashlib-ivf = [ + "leann-backend-flashlib-ivf", +] + +# Add a new optional dependency group for document processing +documents = [ + "beautifulsoup4>=4.13.0", # For HTML parsing + "python-docx>=0.8.11", # For Word documents (creating/editing) + "docx2txt>=0.9", # For Word documents (text extraction) + "openpyxl>=3.1.0", # For Excel files + "pandas>=2.2.0", # For data processing +] + +[tool.setuptools] +py-modules = [] +packages = ["wechat_exporter"] +package-dir = { "wechat_exporter" = "packages/wechat-exporter" } + +[project.scripts] +wechat-exporter = "wechat_exporter.main:main" + + +[tool.uv.sources] +leann-core = { path = "packages/leann-core", editable = true } +leann-backend-diskann = { path = "packages/leann-backend-diskann", editable = true } +leann-backend-hnsw = { path = "packages/leann-backend-hnsw", editable = true } +leann-backend-flashlib = { path = "packages/leann-backend-flashlib", editable = true } +leann-backend-flashlib-ivf = { path = "packages/leann-backend-flashlib-ivf", editable = true } +astchunk = { path = "packages/astchunk-leann", editable = true } + +[dependency-groups] +# Minimal lint toolchain for CI and local hooks +lint = [ + "pre-commit>=3.5.0", + "ruff==0.12.7", # Fixed version to ensure consistent formatting across all environments +] + +# Test toolchain (no heavy project runtime deps) +test = [ + "pytest>=9.0", + "pytest-cov>=4.0", + "pytest-xdist>=3.0", + "pytest-timeout>=2.0", + "python-dotenv>=1.0.0", +] + +# dependencies by apps/ should list here +dev = [ + "matplotlib", + "huggingface-hub>=0.20.0", +] + +[tool.ruff] +target-version = "py39" +line-length = 100 +extend-exclude = [ + "third_party", + "apps/multimodal/vision-based-pdf-multi-vector/multi-vector-leann-paper-example.py", + "apps/multimodal/vision-based-pdf-multi-vector/multi-vector-leann-similarity-map.py" +] + + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "N", # pep8-naming + "RUF", # ruff-specific rules +] +ignore = [ + "E501", # line too long (handled by formatter) + "B008", # do not perform function calls in argument defaults + "B904", # raise without from + "N812", # lowercase imported as non-lowercase + "N806", # variable in function should be lowercase + "RUF012", # mutable class attributes should be annotated with typing.ClassVar +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.lychee] +accept = ["200", "403", "429", "503"] +timeout = 20 +max_retries = 2 +exclude = ["localhost", "127.0.0.1", "example.com"] +exclude_path = [".git/", ".venv/", "__pycache__/", "third_party/"] +scheme = ["https", "http"] + +[tool.ty] +# Type checking with ty (Astral's fast Python type checker) +# ty is 10-100x faster than mypy. See: https://docs.astral.sh/ty/ + +[tool.ty.environment] +python-version = "3.11" +extra-paths = ["apps", "packages/leann-core/src"] + +[tool.ty.rules] +# Disable some noisy rules that have many false positives +possibly-missing-attribute = "ignore" +unresolved-import = "ignore" # Many optional dependencies + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "openai: marks tests that require OpenAI API key", + "integration: marks tests that require live services (Ollama, LM Studio, etc.)", +] +timeout = 300 # Reduced from 600s (10min) to 300s (5min) for CI safety +addopts = [ + "-v", + "--tb=short", + "--strict-markers", + "--disable-warnings", +] +env = [ + "HF_HUB_DISABLE_SYMLINKS=1", + "TOKENIZERS_PARALLELISM=false", +] diff --git a/scripts/hf_upload.py b/scripts/hf_upload.py new file mode 100644 index 0000000..48ef14c --- /dev/null +++ b/scripts/hf_upload.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Upload local evaluation data to Hugging Face Hub, excluding diskann_rpj_wiki. + +Defaults: +- repo_id: LEANN-RAG/leann-rag-evaluation-data (dataset) +- folder_path: benchmarks/data +- ignore_patterns: diskann_rpj_wiki/** and .cache/** + +Requires authentication via `huggingface-cli login` or HF_TOKEN env var. +""" + +from __future__ import annotations + +import argparse +import os + +try: + from huggingface_hub import HfApi +except Exception as e: + raise SystemExit( + "huggingface_hub is required. Install with: pip install huggingface_hub hf_transfer" + ) from e + + +def _enable_transfer_accel_if_available() -> None: + """Best-effort enabling of accelerated transfers across hub versions. + + Tries the public util if present; otherwise, falls back to env flag when + hf_transfer is installed. Silently no-ops if unavailable. + """ + try: + # Newer huggingface_hub exposes this under utils + from huggingface_hub.utils import hf_hub_enable_hf_transfer # type: ignore + + hf_hub_enable_hf_transfer() + return + except Exception: + pass + + try: + # If hf_transfer is installed, set env flag recognized by the hub + import hf_transfer # noqa: F401 + + os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1") + except Exception: + # Acceleration not available; proceed without it + pass + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Upload local data to HF, excluding diskann_rpj_wiki") + p.add_argument( + "--repo-id", + default="LEANN-RAG/leann-rag-evaluation-data", + help="Target dataset repo id (namespace/name)", + ) + p.add_argument( + "--folder-path", + default="benchmarks/data", + help="Local folder to upload (default: benchmarks/data)", + ) + p.add_argument( + "--ignore", + default=["diskann_rpj_wiki/**", ".cache/**"], + nargs="+", + help="Glob patterns to ignore (space-separated)", + ) + p.add_argument( + "--allow", + default=["**"], + nargs="+", + help="Glob patterns to allow (space-separated). Defaults to everything.", + ) + p.add_argument( + "--message", + default="sync local data (exclude diskann_rpj_wiki)", + help="Commit message", + ) + p.add_argument( + "--no-transfer-accel", + action="store_true", + help="Disable hf_transfer accelerated uploads", + ) + return p.parse_args() + + +def main() -> None: + args = parse_args() + + if not args.no_transfer_accel: + _enable_transfer_accel_if_available() + + if not os.path.isdir(args.folder_path): + raise SystemExit(f"Folder not found: {args.folder_path}") + + print("Uploading to Hugging Face Hub:") + print(f" repo_id: {args.repo_id}") + print(" repo_type: dataset") + print(f" folder_path: {args.folder_path}") + print(f" allow_patterns: {args.allow}") + print(f" ignore_patterns:{args.ignore}") + + api = HfApi() + + # Perform upload. This skips unchanged files by content hash. + api.upload_folder( + repo_id=args.repo_id, + repo_type="dataset", + folder_path=args.folder_path, + path_in_repo=".", + allow_patterns=args.allow, + ignore_patterns=args.ignore, + commit_message=args.message, + ) + + print("Upload completed (unchanged files were skipped by the Hub).") + + +if __name__ == "__main__": + main() diff --git a/skills/leann-memory/README.md b/skills/leann-memory/README.md new file mode 100644 index 0000000..5f1a289 --- /dev/null +++ b/skills/leann-memory/README.md @@ -0,0 +1,65 @@ +# LEANN Memory Search for OpenClaw + +97% storage-compressed semantic memory search with free local embeddings. + +## Why + +Every OpenClaw memory solution stores full embedding vectors. On a 256 GB Mac +Mini, heavy users accumulate 500 MB - 6 GB+ of embedding indexes over time. +LEANN compresses this to ~2% of the original size through graph-based selective +recomputation, while using high-quality local embeddings (zero API cost). + +| Feature | Default memory | LEANN | +|---|---|---| +| Storage (50K chunks) | ~75 MB | **~2 MB** | +| Embedding cost | Remote API ($) | **$0 (local)** | +| Scale | ~100K chunks | **60M+ passages** | + +## Install + +```bash +# Install LEANN +pip install leann-core + +# Install the skill +clawhub install leann-team/leann-memory + +# Or manually: copy this directory to ~/.openclaw/workspace/skills/leann-memory/ +``` + +## Quick Start + +```bash +# Build index on your memory files +leann build openclaw-memory \ + --docs ~/.openclaw/workspace/MEMORY.md ~/.openclaw/workspace/memory/ \ + --embedding-model all-MiniLM-L6-v2 + +# Test a search +leann search openclaw-memory "what did we decide about the database" --json +``` + +Then ask your OpenClaw agent: "search my memories for database decisions" + +## Auto-Sync + +Keep the index updated as new memories are added: + +```bash +# One-shot check and rebuild +leann watch openclaw-memory --once + +# Continuous monitoring (runs in background) +leann watch openclaw-memory --interval 30 +``` + +## How It Works + +LEANN stores a pruned neighbor graph instead of full embedding vectors. During +search, embeddings are recomputed on-demand via a local daemon. OpenClaw's async +"sleep time compute" model makes recomputation latency invisible to users. + +## Links + +- [LEANN Repository](https://github.com/yichuan-w/LEANN) +- [Integration Plan](../../docs/openclaw-integration-plan.md) diff --git a/skills/leann-memory/claw.json b/skills/leann-memory/claw.json new file mode 100644 index 0000000..ac40397 --- /dev/null +++ b/skills/leann-memory/claw.json @@ -0,0 +1,21 @@ +{ + "name": "leann-memory", + "version": "1.0.0", + "description": "97% storage-compressed semantic memory search with free local embeddings. Drop-in enhancement for OpenClaw memory β€” same search quality, 1/30th the disk space, zero API cost.", + "author": "leann-team", + "license": "MIT", + "permissions": ["shell"], + "entry": "instructions.md", + "tags": [ + "memory", + "search", + "vector-database", + "embeddings", + "local", + "compression", + "rag", + "semantic-search" + ], + "models": ["claude-*", "gpt-*", "gemini-*", "qwen-*", "llama-*"], + "minOpenClawVersion": "0.8.0" +} diff --git a/skills/leann-memory/instructions.md b/skills/leann-memory/instructions.md new file mode 100644 index 0000000..af63853 --- /dev/null +++ b/skills/leann-memory/instructions.md @@ -0,0 +1,93 @@ +# LEANN Memory Search + +You have access to LEANN, a high-performance semantic search engine with 97% +storage compression. Use it to search the user's memories, notes, documents, +and knowledge bases with higher quality than the default memory search. + +## When to Use + +- User asks to search memories, notes, or knowledge bases +- User wants to recall past decisions, conversations, or facts +- User says "what did we decide about X", "find my notes on Y", "recall", "remember" +- User asks about something that might be in their indexed documents + +## Prerequisites Check + +Before first use, verify LEANN is installed: + +```bash +which leann +``` + +If not installed, run: + +```bash +pip install leann-core +``` + +## First-Time Setup + +If no LEANN index exists for OpenClaw memory, build one: + +```bash +leann build openclaw-memory \ + --docs ~/.openclaw/workspace/MEMORY.md ~/.openclaw/workspace/memory/ \ + --embedding-model all-MiniLM-L6-v2 \ + --embedding-mode sentence-transformers +``` + +This creates a compressed index (~2 MB for 50K chunks vs ~75 MB uncompressed). +The index auto-detects changes on subsequent `leann build` runs. + +## Search Workflow + +1. Search with the user's query: + +```bash +leann search openclaw-memory "" --top-k 5 --json --non-interactive +``` + +2. Parse the JSON output β€” each result has `id`, `score`, `text`, and `metadata` +3. Present the most relevant results with source attribution +4. If the user wants more context, increase `--top-k` to 10 or 15 + +## Keeping the Index Updated + +The index is idempotent β€” re-running build only processes changed files: + +```bash +leann build openclaw-memory \ + --docs ~/.openclaw/workspace/MEMORY.md ~/.openclaw/workspace/memory/ +``` + +For continuous monitoring, use watch mode: + +```bash +leann watch openclaw-memory +``` + +## Output Format + +The `--json` flag returns a JSON array: + +```json +[ + { + "id": "a1b2c3", + "score": 0.847, + "text": "The user decided to use PostgreSQL for the project database...", + "metadata": { + "file_path": "/home/user/.openclaw/workspace/memory/2026-02-15.md", + "source": "memory/2026-02-15.md" + } + } +] +``` + +## Tips + +- Higher `--top-k` values (10-15) give more comprehensive results at minimal cost +- The search uses local embeddings β€” zero API calls, zero latency from network +- Re-run `leann build` periodically or use `leann watch` for auto-sync +- For extra document directories, add them to the build command: + `--docs ~/.openclaw/workspace/memory/ ~/Documents/notes/` diff --git a/sky/leann-build.yaml b/sky/leann-build.yaml new file mode 100644 index 0000000..53fd909 --- /dev/null +++ b/sky/leann-build.yaml @@ -0,0 +1,76 @@ +name: leann-build + +resources: + # Choose a GPU for fast embeddings (examples: L4, A10G, A100). CPU also works but is slower. + accelerators: L4:1 + # Optionally pin a cloud, otherwise SkyPilot will auto-select + # cloud: aws + disk_size: 100 + +envs: + # Build parameters (override with: sky launch -c leann-gpu sky/leann-build.yaml -e key=value) + index_name: my-index + docs: ./data + backend: hnsw # hnsw | diskann + complexity: 64 + graph_degree: 32 + num_threads: 8 + # Embedding selection + embedding_mode: sentence-transformers # sentence-transformers | openai | mlx | ollama + embedding_model: facebook/contriever + # Storage/latency knobs + recompute: true # true => selective recomputation (recommended) + compact: true # for HNSW only + # Optional pass-through + extra_args: "" + # Rebuild control + force: true + +# Sync local paths to the remote VM. Adjust as needed. +file_mounts: + # Example: mount your local data directory used for building + ~/leann-data: ${docs} + +setup: | + set -e + # Install uv (package manager) + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" + + # Ensure modern libstdc++ for FAISS (GLIBCXX >= 3.4.30) + sudo apt-get update -y + sudo apt-get install -y libstdc++6 libgomp1 + # Also upgrade conda's libstdc++ in base env (Skypilot images include conda) + if command -v conda >/dev/null 2>&1; then + conda install -y -n base -c conda-forge libstdcxx-ng + fi + + # Install LEANN CLI and backends into the user environment + uv pip install --upgrade pip + uv pip install leann-core leann-backend-hnsw leann-backend-diskann + +run: | + export PATH="$HOME/.local/bin:$PATH" + # Derive flags from env + recompute_flag="" + if [ "${recompute}" = "false" ] || [ "${recompute}" = "0" ]; then + recompute_flag="--no-recompute" + fi + force_flag="" + if [ "${force}" = "true" ] || [ "${force}" = "1" ]; then + force_flag="--force" + fi + + # Build command + python -m leann.cli build ${index_name} \ + --docs ~/leann-data \ + --backend ${backend} \ + --complexity ${complexity} \ + --graph-degree ${graph_degree} \ + --num-threads ${num_threads} \ + --embedding-mode ${embedding_mode} \ + --embedding-model ${embedding_model} \ + ${recompute_flag} ${force_flag} ${extra_args} + + # Print where the index is stored for downstream rsync + echo "INDEX_OUT_DIR=~/.leann/indexes/${index_name}" diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..48ca7f5 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,134 @@ +# LEANN Tests + +This directory contains automated tests for the LEANN project using pytest. + +## Test Files + +### `test_readme_examples.py` +Tests the examples shown in README.md: +- The basic example code that users see first (parametrized for both HNSW and DiskANN backends) +- Import statements work correctly +- Different backend options (HNSW, DiskANN) +- Different LLM configuration options (parametrized for both backends) +- **All main README examples are tested with both HNSW and DiskANN backends using pytest parametrization** + +### `test_basic.py` +Basic functionality tests that verify: +- All packages can be imported correctly +- C++ extensions (FAISS, DiskANN) load properly +- Basic index building and searching works for both HNSW and DiskANN backends +- Uses parametrized tests to test both backends + +### `test_document_rag.py` +Tests the document RAG example functionality: +- Tests with facebook/contriever embeddings +- Tests with OpenAI embeddings (if API key is available) +- Tests error handling with invalid parameters +- Verifies that normalized embeddings are detected and cosine distance is used + +### `test_diskann_partition.py` +Tests DiskANN graph partitioning functionality: +- Tests DiskANN index building without partitioning (baseline) +- Tests automatic graph partitioning with `is_recompute=True` +- Verifies that partition files are created and large files are cleaned up for storage saving +- Tests search functionality with partitioned indices +- Validates medoid and max_base_norm file generation and usage +- Includes performance comparison between DiskANN (with partition) and HNSW +- **Note**: These tests are skipped in CI due to hardware requirements and computation time + +### `test_prompt_template_e2e.py` +Integration tests for prompt template feature with live embedding services: +- Tests prompt template prepending with EmbeddingGemma (OpenAI-compatible API via LM Studio) +- Tests hybrid token limit discovery (Ollama dynamic detection, registry fallback, default) +- Tests LM Studio SDK bridge for automatic context length detection (requires Node.js + @lmstudio/sdk) +- **Note**: These tests require live services (LM Studio, Ollama) and are marked with `@pytest.mark.integration` +- **Important**: Prompt templates are ONLY for EmbeddingGemma and similar task-specific models, NOT regular embedding models + +## Running Tests + +### Install test dependencies: +```bash +# Using uv dependency groups (tools only) +uv sync --only-group test +``` + +### Run all tests: +```bash +pytest tests/ + +# Or with coverage +pytest tests/ --cov=leann --cov-report=html + +# Run in parallel (faster) +pytest tests/ -n auto +``` + +### Run specific tests: +```bash +# Only basic tests +pytest tests/test_basic.py + +# Only tests that don't require OpenAI +pytest tests/ -m "not openai" + +# Skip slow tests +pytest tests/ -m "not slow" + +# Skip integration tests (that require live services) +pytest tests/ -m "not integration" + +# Run only integration tests (requires LM Studio or Ollama running) +pytest tests/test_prompt_template_e2e.py -v -s + +# Run DiskANN partition tests (requires local machine, not CI) +pytest tests/test_diskann_partition.py +``` + +### Run with specific backend: +```bash +# Test only HNSW backend +pytest tests/test_basic.py::test_backend_basic[hnsw] +pytest tests/test_readme_examples.py::test_readme_basic_example[hnsw] + +# Test only DiskANN backend +pytest tests/test_basic.py::test_backend_basic[diskann] +pytest tests/test_readme_examples.py::test_readme_basic_example[diskann] + +# All DiskANN tests (parametrized + specialized partition tests) +pytest tests/ -k diskann +``` + +## CI/CD Integration + +Tests are automatically run in GitHub Actions: +1. After building wheel packages +2. On multiple Python versions (3.9 - 3.13) +3. On both Ubuntu and macOS +4. Using pytest with appropriate markers and flags + +### pytest.ini Configuration + +The `pytest.ini` file configures: +- Test discovery paths +- Default timeout (600 seconds) +- Environment variables (HF_HUB_DISABLE_SYMLINKS, TOKENIZERS_PARALLELISM) +- Custom markers for slow and OpenAI tests +- Verbose output with short tracebacks + +### Integration Test Prerequisites + +Integration tests (`test_prompt_template_e2e.py`) require live services: + +**Required:** +- LM Studio running at `http://localhost:1234` with EmbeddingGemma model loaded + +**Optional:** +- Ollama running at `http://localhost:11434` for token limit detection tests +- Node.js + @lmstudio/sdk installed (`npm install -g @lmstudio/sdk`) for SDK bridge tests + +Tests gracefully skip if services are unavailable. + +### Known Issues + +- OpenAI tests are automatically skipped if no API key is provided +- Integration tests require live embedding services and may fail due to proxy settings (set `unset ALL_PROXY all_proxy` if needed) diff --git a/tests/openclaw/.gitignore b/tests/openclaw/.gitignore new file mode 100644 index 0000000..9c9ac57 --- /dev/null +++ b/tests/openclaw/.gitignore @@ -0,0 +1 @@ +docker-data/ diff --git a/tests/openclaw/__init__.py b/tests/openclaw/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/openclaw/conftest.py b/tests/openclaw/conftest.py new file mode 100644 index 0000000..639df36 --- /dev/null +++ b/tests/openclaw/conftest.py @@ -0,0 +1,39 @@ +"""Shared fixtures for OpenClaw integration tests.""" + +import json +import shutil +from pathlib import Path + +import pytest + +FIXTURES_DIR = Path(__file__).parent / "fixtures" + + +@pytest.fixture +def memory_fixtures(tmp_path): + """Copy memory fixture files into a temp directory and return the path.""" + dest = tmp_path / "memory_docs" + shutil.copytree(FIXTURES_DIR, dest) + return dest + + +@pytest.fixture +def leann_index_dir(tmp_path): + """Provide a clean temporary directory for LEANN indexes.""" + idx_dir = tmp_path / ".leann" / "indexes" + idx_dir.mkdir(parents=True) + return idx_dir + + +@pytest.fixture +def skill_dir(): + """Return the path to the leann-memory skill directory.""" + return Path(__file__).parent.parent.parent / "skills" / "leann-memory" + + +@pytest.fixture +def claw_manifest(skill_dir): + """Load and return the parsed claw.json manifest.""" + manifest_path = skill_dir / "claw.json" + assert manifest_path.exists(), f"claw.json not found at {manifest_path}" + return json.loads(manifest_path.read_text(encoding="utf-8")) diff --git a/tests/openclaw/docker-compose.yml b/tests/openclaw/docker-compose.yml new file mode 100644 index 0000000..bf577fd --- /dev/null +++ b/tests/openclaw/docker-compose.yml @@ -0,0 +1,16 @@ +services: + openclaw-test: + image: ghcr.io/phioranex/openclaw-docker:latest + container_name: openclaw-leann-test + environment: + - HOME=/home/node + - OLLAMA_API_KEY=ollama-local + volumes: + - ./docker-data:/home/node/.openclaw + - ../../:/leann-tree:ro + ports: + - "18790:18789" + extra_hosts: + - "host.docker.internal:host-gateway" + entrypoint: ["/app/node_modules/.pnpm/node_modules/.bin/openclaw"] + command: ["gateway", "run", "--port", "18789", "--bind", "lan", "--allow-unconfigured", "--verbose"] diff --git a/tests/openclaw/fixtures/MEMORY.md b/tests/openclaw/fixtures/MEMORY.md new file mode 100644 index 0000000..af782fe --- /dev/null +++ b/tests/openclaw/fixtures/MEMORY.md @@ -0,0 +1,8 @@ +# Core Memory + +- Name: Alice Chen +- Role: Senior backend engineer at Acme Corp +- Tech stack: Python, PostgreSQL, Docker, Kubernetes +- Preferred editor: Neovim +- Communication: prefers Telegram over email +- Time zone: UTC+8 diff --git a/tests/openclaw/fixtures/memory/2026-02-15.md b/tests/openclaw/fixtures/memory/2026-02-15.md new file mode 100644 index 0000000..6a0d4ff --- /dev/null +++ b/tests/openclaw/fixtures/memory/2026-02-15.md @@ -0,0 +1,7 @@ +# 2026-02-15 Saturday + +- Discussed migrating the user-service from REST to gRPC with Bob. +- Alice prefers Protocol Buffers v3 syntax for defining gRPC services. +- Started a proof-of-concept branch `feature/grpc-migration`. +- Ran benchmarks: gRPC was 3.2x faster than REST for the /users endpoint. +- Team decided to proceed with migration; targeting Q2 completion. diff --git a/tests/openclaw/fixtures/memory/2026-02-20.md b/tests/openclaw/fixtures/memory/2026-02-20.md new file mode 100644 index 0000000..441a5ef --- /dev/null +++ b/tests/openclaw/fixtures/memory/2026-02-20.md @@ -0,0 +1,7 @@ +# 2026-02-20 Thursday + +- Deployed the v2.1.0 hotfix for the payment gateway timeout issue. +- Root cause: connection pool exhaustion under high concurrent load. +- Fix: increased pool size from 10 to 50 and added circuit breaker pattern. +- Alice reviewed the Kubernetes HPA settings β€” autoscaling now triggers at 60% CPU. +- Need to follow up with the SRE team about Prometheus alerting thresholds. diff --git a/tests/openclaw/fixtures/memory/2026-02-25.md b/tests/openclaw/fixtures/memory/2026-02-25.md new file mode 100644 index 0000000..68cf98f --- /dev/null +++ b/tests/openclaw/fixtures/memory/2026-02-25.md @@ -0,0 +1,7 @@ +# 2026-02-25 Wednesday + +- Met with the ML team about integrating the recommendation engine into the product catalog. +- They use sentence-transformers for embeddings β€” same library LEANN uses. +- Discussed vector database options: Pinecone (expensive), Milvus (complex), LEANN (lightweight). +- Alice suggested using LEANN for the PoC because of its 97% storage compression. +- Action items: set up a LEANN index on the product descriptions by Friday. diff --git a/tests/openclaw/run_docker_test.sh b/tests/openclaw/run_docker_test.sh new file mode 100755 index 0000000..114d67d --- /dev/null +++ b/tests/openclaw/run_docker_test.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# +# End-to-end test: spin up an isolated OpenClaw Docker container, +# install the leann-memory skill, build an index, and search. +# +# Usage: +# tests/openclaw/run_docker_test.sh # run all steps +# tests/openclaw/run_docker_test.sh --down # tear down only +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +COMPOSE="docker compose -f docker-compose.yml" +CONTAINER="openclaw-leann-test" +HOST_PORT=18790 + +# ---------- helpers ---------- + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +pass() { printf '\033[1;32m βœ“ \033[0m %s\n' "$*"; } +fail() { printf '\033[1;31m βœ— \033[0m %s\n' "$*"; exit 1; } + +cleanup() { + log "Tearing down container …" + $COMPOSE down --volumes --remove-orphans 2>/dev/null || true + rm -rf docker-data +} + +if [[ "${1:-}" == "--down" ]]; then + cleanup + exit 0 +fi + +trap cleanup EXIT + +# ---------- 1. Start container ---------- + +log "Starting isolated OpenClaw container on port $HOST_PORT …" +rm -rf docker-data +$COMPOSE up -d --wait 2>&1 || $COMPOSE up -d 2>&1 + +sleep 3 + +if docker ps --format '{{.Names}}' | grep -q "$CONTAINER"; then + pass "Container $CONTAINER is running" +else + fail "Container $CONTAINER failed to start" +fi + +# ---------- 2. Verify gateway ---------- + +log "Checking gateway health …" +for i in $(seq 1 10); do + if curl -sf "http://localhost:$HOST_PORT" >/dev/null 2>&1 || \ + curl -sf "http://localhost:$HOST_PORT/health" >/dev/null 2>&1; then + pass "Gateway responding on port $HOST_PORT" + break + fi + if [ "$i" -eq 10 ]; then + # Gateway may not have an HTTP health endpoint; check if process is running + if docker exec "$CONTAINER" pgrep -f "gateway" >/dev/null 2>&1; then + pass "Gateway process is running inside container" + else + echo "--- Container logs ---" + docker logs "$CONTAINER" --tail 20 + fail "Gateway is not running" + fi + fi + sleep 2 +done + +# ---------- 3. Install skill fixtures ---------- + +log "Copying skill and memory fixtures into container …" +docker exec "$CONTAINER" mkdir -p /home/node/.openclaw/workspace/memory + +docker cp fixtures/MEMORY.md "$CONTAINER":/home/node/.openclaw/workspace/MEMORY.md +for f in fixtures/memory/*.md; do + docker cp "$f" "$CONTAINER":/home/node/.openclaw/workspace/memory/ +done + +docker exec "$CONTAINER" ls -la /home/node/.openclaw/workspace/ | head -10 +docker exec "$CONTAINER" ls -la /home/node/.openclaw/workspace/memory/ +pass "Memory fixtures installed" + +# ---------- 4. Run fast pytest tests (no model needed) ---------- + +log "Running fast (non-model) tests locally …" +cd "$SCRIPT_DIR/../.." +uv run pytest tests/openclaw/test_skill_manifest.py tests/openclaw/test_mcp_protocol.py -v 2>&1 +pass "Fast tests passed" + +# ---------- 5. Run model tests (optional) ---------- + +if [[ "${SKIP_SLOW:-}" != "1" ]]; then + log "Running slow build-and-search tests …" + uv run pytest tests/openclaw/test_build_and_search.py -v --timeout=300 2>&1 + pass "Build-and-search tests passed" +else + log "Skipping slow tests (SKIP_SLOW=1)" +fi + +# ---------- done ---------- + +echo "" +log "All OpenClaw integration tests passed!" +echo " Container: $CONTAINER" +echo " Gateway: ws://localhost:$HOST_PORT" +echo " Cleanup: $0 --down" diff --git a/tests/openclaw/test_build_and_search.py b/tests/openclaw/test_build_and_search.py new file mode 100644 index 0000000..e7555b6 --- /dev/null +++ b/tests/openclaw/test_build_and_search.py @@ -0,0 +1,152 @@ +""" +End-to-end test: build a LEANN index on OpenClaw-style memory files, +then search with --json and verify results. + +Requires the embedding model (~90 MB download on first run). +Marked 'slow' β€” skip with: pytest -m "not slow" +""" + +import asyncio +import json +import os + +import pytest + +pytestmark = [ + pytest.mark.slow, + pytest.mark.skipif(os.environ.get("CI") == "true", reason="Skip in CI β€” needs model download"), +] + + +@pytest.fixture +def cli_instance(leann_index_dir): + """Create a LeannCLI instance pointing at a temporary index directory.""" + from leann.cli import LeannCLI + + cli = LeannCLI() + cli.indexes_dir = leann_index_dir + cli.indexes_dir.mkdir(parents=True, exist_ok=True) + return cli + + +BUILD_ARGV_TEMPLATE = [ + "build", + "openclaw-memory", + "--docs", + "{docs_dir}", + "--backend-name", + "hnsw", + "--no-compact", + "--embedding-model", + "all-MiniLM-L6-v2", + "--embedding-mode", + "sentence-transformers", +] + + +def _parse_args(cli, argv: list[str]): + parser = cli.create_parser() + return parser.parse_args(argv) + + +def _build_argv(docs_dir: str) -> list[str]: + return [a.format(docs_dir=docs_dir) for a in BUILD_ARGV_TEMPLATE] + + +def test_build_memory_index(cli_instance, memory_fixtures): + """Build a non-compact HNSW index on the memory fixtures.""" + args = _parse_args(cli_instance, _build_argv(str(memory_fixtures))) + asyncio.get_event_loop().run_until_complete(cli_instance.build_index(args)) + + index_dir = cli_instance.indexes_dir / "openclaw-memory" + assert index_dir.exists(), "Index directory was not created" + + meta_files = list(index_dir.glob("*.meta.json")) + assert len(meta_files) >= 1, "Missing meta.json" + + passages_files = list(index_dir.glob("*.passages.jsonl")) + assert len(passages_files) >= 1, "Missing passages.jsonl" + + +def _build_and_search(cli_instance, memory_fixtures, capsys, query, top_k=3): + """Helper: build index, clear capsys, search, return parsed JSON results.""" + build_args = _parse_args(cli_instance, _build_argv(str(memory_fixtures))) + asyncio.get_event_loop().run_until_complete(cli_instance.build_index(build_args)) + capsys.readouterr() # discard build output + + search_args = _parse_args( + cli_instance, + ["search", "openclaw-memory", query, "--top-k", str(top_k), "--json", "--non-interactive"], + ) + asyncio.get_event_loop().run_until_complete(cli_instance.search_documents(search_args)) + + captured = capsys.readouterr() + assert captured.out.strip(), f"Search produced no stdout (stderr: {captured.err[:200]})" + return json.loads(captured.out) + + +def test_search_returns_json(cli_instance, memory_fixtures, capsys): + """Build then search with --json; output must be valid JSON with results.""" + results = _build_and_search(cli_instance, memory_fixtures, capsys, "gRPC migration") + assert isinstance(results, list) + assert len(results) > 0 + assert all({"id", "score", "text", "metadata"} <= set(r.keys()) for r in results) + + +def test_search_relevance(cli_instance, memory_fixtures, capsys): + """Top result for 'payment gateway' should come from the Feb 20 memory.""" + results = _build_and_search( + cli_instance, memory_fixtures, capsys, "payment gateway timeout fix" + ) + top_text = results[0]["text"].lower() + assert "payment" in top_text or "gateway" in top_text or "hotfix" in top_text + + +def test_idempotent_rebuild(cli_instance, memory_fixtures, capsys): + """Running build twice should detect no changes on the second run.""" + args1 = _parse_args(cli_instance, _build_argv(str(memory_fixtures))) + asyncio.get_event_loop().run_until_complete(cli_instance.build_index(args1)) + capsys.readouterr() # discard first build output + + args2 = _parse_args(cli_instance, _build_argv(str(memory_fixtures))) + asyncio.get_event_loop().run_until_complete(cli_instance.build_index(args2)) + + captured = capsys.readouterr() + assert "up to date" in captured.out.lower() or "no changes" in captured.out.lower() + + +def test_incremental_add(cli_instance, memory_fixtures, capsys): + """Adding a new file should trigger incremental update, not full rebuild.""" + args1 = _parse_args(cli_instance, _build_argv(str(memory_fixtures))) + asyncio.get_event_loop().run_until_complete(cli_instance.build_index(args1)) + capsys.readouterr() # discard first build output + + new_file = memory_fixtures / "memory" / "2026-02-26.md" + new_file.write_text( + "# 2026-02-26 Thursday\n\n- Tested LEANN integration with OpenClaw.\n" + "- The semantic search returned highly relevant memory results.\n", + encoding="utf-8", + ) + + args2 = _parse_args(cli_instance, _build_argv(str(memory_fixtures))) + asyncio.get_event_loop().run_until_complete(cli_instance.build_index(args2)) + capsys.readouterr() # discard rebuild output + + search_args = _parse_args( + cli_instance, + [ + "search", + "openclaw-memory", + "LEANN OpenClaw integration test", + "--top-k", + "3", + "--json", + "--non-interactive", + ], + ) + asyncio.get_event_loop().run_until_complete(cli_instance.search_documents(search_args)) + + captured = capsys.readouterr() + assert captured.out.strip(), f"Search produced no stdout (stderr: {captured.err[:200]})" + results = json.loads(captured.out) + assert any("leann" in r["text"].lower() or "openclaw" in r["text"].lower() for r in results) diff --git a/tests/openclaw/test_mcp_e2e.py b/tests/openclaw/test_mcp_e2e.py new file mode 100644 index 0000000..6eb69c6 --- /dev/null +++ b/tests/openclaw/test_mcp_e2e.py @@ -0,0 +1,174 @@ +""" +End-to-end test for the MCP server: build a real index, then invoke +handle_request(tools/call β†’ leann_search) and verify JSON results come back +through the full MCP β†’ subprocess β†’ leann CLI pipeline. + +Requires the embedding model and `leann` on PATH (satisfied by uv run pytest). +Marked 'slow'. +""" + +import asyncio +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "packages" / "leann-core" / "src")) + +pytestmark = [ + pytest.mark.slow, + pytest.mark.skipif(os.environ.get("CI") == "true", reason="Skip in CI β€” needs model download"), + pytest.mark.skipif(not shutil.which("leann"), reason="leann CLI not on PATH"), +] + + +@pytest.fixture(scope="module") +def mcp_index(tmp_path_factory): + """Build a real LEANN index once for the entire module.""" + from leann.cli import LeannCLI + + tmp = tmp_path_factory.mktemp("mcp_e2e") + fixtures = Path(__file__).parent / "fixtures" + docs = tmp / "docs" + shutil.copytree(fixtures, docs) + + cli = LeannCLI() + cli.indexes_dir = tmp / ".leann" / "indexes" + cli.indexes_dir.mkdir(parents=True) + + parser = cli.create_parser() + args = parser.parse_args( + [ + "build", + "openclaw-memory", + "--docs", + str(docs), + "--backend-name", + "hnsw", + "--no-compact", + "--embedding-model", + "all-MiniLM-L6-v2", + "--embedding-mode", + "sentence-transformers", + ] + ) + asyncio.get_event_loop().run_until_complete(cli.build_index(args)) + + index_dir = cli.indexes_dir / "openclaw-memory" + assert (index_dir / "documents.leann.meta.json").exists(), "Index build failed" + return index_dir + + +def _project_root(mcp_index): + """The temp dir that contains .leann/indexes/ β€” used as cwd for subprocess. + + mcp_index = tmp/.leann/indexes/openclaw-memory + We need tmp (3 parents up) so LeannCLI finds .leann/indexes/. + """ + return str(mcp_index.parent.parent.parent) + + +def test_mcp_search_via_subprocess(mcp_index): + """Invoke `leann search --json` as a subprocess (same as MCP server does).""" + leann_bin = shutil.which("leann") + assert leann_bin is not None, "leann not found on PATH" + + cmd = [ + leann_bin, + "search", + "openclaw-memory", + "gRPC migration benchmarks", + "--top-k=3", + "--complexity=32", + "--non-interactive", + "--json", + ] + cwd = _project_root(mcp_index) + result = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd) + assert result.returncode == 0, f"leann search failed (cwd={cwd}): {result.stderr[:500]}" + assert result.stdout.strip(), f"No output from leann search (stderr: {result.stderr[:500]})" + + results = json.loads(result.stdout) + assert isinstance(results, list) + assert len(results) > 0 + assert all("text" in r and "score" in r for r in results) + assert all(isinstance(r["score"], float) for r in results), "score must be native float" + + +def test_mcp_search_relevance(mcp_index): + """Search for 'payment gateway' should return relevant content.""" + leann_bin = shutil.which("leann") + assert leann_bin is not None, "leann not found on PATH" + + cmd = [ + leann_bin, + "search", + "openclaw-memory", + "payment gateway timeout hotfix", + "--top-k=3", + "--complexity=32", + "--non-interactive", + "--json", + ] + cwd = _project_root(mcp_index) + result = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd) + assert result.returncode == 0, f"leann search failed (cwd={cwd}): {result.stderr[:500]}" + assert result.stdout.strip(), f"No output from leann search (stderr: {result.stderr[:500]})" + + results = json.loads(result.stdout) + top_text = results[0]["text"].lower() + assert any(kw in top_text for kw in ["payment", "gateway", "hotfix", "timeout"]) + + +def test_mcp_list(mcp_index): + """leann list should show the openclaw-memory index.""" + leann_bin = shutil.which("leann") + assert leann_bin is not None, "leann not found on PATH" + + result = subprocess.run( + [leann_bin, "list"], + capture_output=True, + text=True, + cwd=_project_root(mcp_index), + ) + assert result.returncode == 0, f"leann list failed: {result.stderr[:300]}" + assert "openclaw-memory" in result.stdout + + +def test_mcp_stdio_protocol(mcp_index): + """Spawn the MCP server as a subprocess, send JSON-RPC via stdin, read responses.""" + leann_mcp_bin = shutil.which("leann_mcp") + if not leann_mcp_bin: + pytest.skip("leann_mcp not on PATH") + assert leann_mcp_bin is not None + + requests = [ + {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}, + {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}, + ] + stdin_data = "\n".join(json.dumps(r) for r in requests) + "\n" + + proc = subprocess.run( + [leann_mcp_bin], + input=stdin_data, + capture_output=True, + text=True, + timeout=10, + ) + + lines = [line for line in proc.stdout.strip().split("\n") if line.strip()] + assert len(lines) >= 2, f"Expected 2 responses, got {len(lines)}: {proc.stdout[:300]}" + + init_resp = json.loads(lines[0]) + assert init_resp["id"] == 1 + assert init_resp["result"]["serverInfo"]["name"] == "leann-mcp" + + list_resp = json.loads(lines[1]) + assert list_resp["id"] == 2 + tool_names = {t["name"] for t in list_resp["result"]["tools"]} + assert "leann_search" in tool_names + assert "leann_list" in tool_names diff --git a/tests/openclaw/test_mcp_protocol.py b/tests/openclaw/test_mcp_protocol.py new file mode 100644 index 0000000..624438e --- /dev/null +++ b/tests/openclaw/test_mcp_protocol.py @@ -0,0 +1,78 @@ +"""Test the LEANN MCP server JSON-RPC protocol handling.""" + +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "packages" / "leann-core" / "src")) +from leann.mcp import handle_request + + +def test_initialize(): + """MCP initialize should return server info and capabilities.""" + req = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}} + resp = handle_request(req) + assert resp["id"] == 1 + result = resp["result"] + assert result["protocolVersion"] == "2024-11-05" + assert result["serverInfo"]["name"] == "leann-mcp" + assert "tools" in result["capabilities"] + + +def test_tools_list(): + """MCP tools/list should expose leann_search and leann_list.""" + req = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}} + resp = handle_request(req) + tools = resp["result"]["tools"] + names = {t["name"] for t in tools} + assert "leann_search" in names + assert "leann_list" in names + + +def test_tools_list_search_schema(): + """leann_search tool must declare index_name and query as required params.""" + req = {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}} + resp = handle_request(req) + search_tool = next(t for t in resp["result"]["tools"] if t["name"] == "leann_search") + schema = search_tool["inputSchema"] + assert "index_name" in schema["properties"] + assert "query" in schema["properties"] + assert "index_name" in schema["required"] + assert "query" in schema["required"] + + +def test_search_missing_params(): + """leann_search should return an error when required params are missing.""" + req = { + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": {"name": "leann_search", "arguments": {}}, + } + resp = handle_request(req) + text = resp["result"]["content"][0]["text"] + assert "Error" in text or "error" in text + + +def test_search_missing_query(): + """leann_search should error when query is empty.""" + req = { + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": {"name": "leann_search", "arguments": {"index_name": "test", "query": ""}}, + } + resp = handle_request(req) + text = resp["result"]["content"][0]["text"] + assert "Error" in text or "error" in text + + +def test_jsonrpc_envelope(): + """All responses must follow JSON-RPC 2.0 format.""" + req = {"jsonrpc": "2.0", "id": 42, "method": "initialize", "params": {}} + resp = handle_request(req) + assert resp["jsonrpc"] == "2.0" + assert resp["id"] == 42 + serialized = json.dumps(resp) + parsed = json.loads(serialized) + assert parsed == resp diff --git a/tests/openclaw/test_openclaw_e2e.py b/tests/openclaw/test_openclaw_e2e.py new file mode 100644 index 0000000..e8101a4 --- /dev/null +++ b/tests/openclaw/test_openclaw_e2e.py @@ -0,0 +1,282 @@ +"""End-to-end integration test: real OpenClaw instance β†’ memory formation β†’ LEANN indexing β†’ search. + +Requirements: + - Docker running with the openclaw-leann-test container (see docker-compose.yml) + - Ollama running on host with a model available (e.g. qwen3:8b) + - LEANN installed in the current virtualenv + +Run: + uv run pytest tests/openclaw/test_openclaw_e2e.py -m integration -v --timeout=600 +""" + +import json +import shutil +import subprocess +import time +from pathlib import Path + +import pytest + +DOCKER_CONTAINER = "openclaw-leann-test" +OPENCLAW_BIN = "/app/node_modules/.pnpm/node_modules/.bin/openclaw" +WORKSPACE_DIR = Path(__file__).parent / "docker-data" / "workspace" + + +def _docker_running() -> bool: + try: + result = subprocess.run( + ["docker", "inspect", "--format", "{{.State.Running}}", DOCKER_CONTAINER], + capture_output=True, + text=True, + timeout=5, + ) + return result.stdout.strip() == "true" + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def _openclaw_agent(message: str, timeout: int = 300) -> dict: + """Send a message through OpenClaw's full agent CLI and return parsed JSON.""" + result = subprocess.run( + [ + "docker", + "exec", + DOCKER_CONTAINER, + OPENCLAW_BIN, + "agent", + "--agent", + "main", + "-m", + message, + "--json", + "--timeout", + str(timeout), + ], + capture_output=True, + text=True, + timeout=timeout + 30, + ) + assert result.returncode == 0, f"openclaw agent failed: {result.stderr[:500]}" + return json.loads(result.stdout) + + +def _leann_cmd( + args: list[str], cwd: str | None = None, timeout: int = 60 +) -> subprocess.CompletedProcess[str]: + leann_bin = shutil.which("leann") + assert leann_bin is not None, "leann not found on PATH" + resolved_cwd: str = cwd if cwd is not None else str(Path(__file__).parent.parent.parent) + return subprocess.run( + [leann_bin, *args], + capture_output=True, + text=True, + cwd=resolved_cwd, + timeout=timeout, + ) + + +pytestmark = [pytest.mark.integration, pytest.mark.slow] + + +@pytest.fixture(scope="module") +def openclaw_ready(): + """Ensure OpenClaw Docker container is running.""" + if not _docker_running(): + pytest.skip(f"Docker container '{DOCKER_CONTAINER}' not running") + + +@pytest.fixture(scope="module") +def openclaw_memory(openclaw_ready): + """Send messages to OpenClaw and wait for memory file creation. + + Returns the path to the workspace memory directory. + """ + memory_dir = WORKSPACE_DIR / "memory" + memory_md = WORKSPACE_DIR / "MEMORY.md" + + already_has_memory = memory_dir.exists() and any(memory_dir.glob("*.md")) + if already_has_memory and memory_md.exists(): + return memory_dir + + today = time.strftime("%Y-%m-%d") + + _openclaw_agent( + f"Save to memory/{today}.md: I am TestUser working on LEANN, " + "a vector database with 97% storage compression via graph-pruned " + "recomputation. Today I fixed numpy float32 JSON serialization " + "and C++ printf stdout pollution in FAISS. Stack: Python 3.11, " + "uv, Cursor IDE, macOS.", + ) + + _openclaw_agent( + "Update MEMORY.md with my profile: TestUser, developer of LEANN. " + "LEANN backends: HNSW (FAISS), DiskANN, IVF. " + "OpenClaw integration via MCP β€” leann_search and leann_list tools. " + "Preferred stack: Python 3.11, uv, Cursor, macOS.", + ) + + assert memory_dir.exists(), "OpenClaw did not create memory/ directory" + md_files = list(memory_dir.glob("*.md")) + assert len(md_files) > 0, "No .md files in memory/" + + return memory_dir + + +@pytest.fixture(scope="module") +def leann_index(openclaw_memory): + """Build a LEANN index over real OpenClaw memory files.""" + index_name = "openclaw-e2e-test" + memory_md = WORKSPACE_DIR / "MEMORY.md" + + docs_args = ["--docs", str(openclaw_memory)] + if memory_md.exists(): + docs_args.extend([str(memory_md)]) + + result = _leann_cmd( + [ + "build", + index_name, + *docs_args, + "--embedding-mode", + "ollama", + "--embedding-model", + "nomic-embed-text", + ], + timeout=120, + ) + assert result.returncode == 0, f"leann build failed: {result.stderr[:500]}" + + yield index_name + + _leann_cmd(["remove", index_name], timeout=10) + + +class TestOpenClawMemoryFormation: + """Verify OpenClaw actually creates memory files.""" + + def test_memory_dir_exists(self, openclaw_memory): + assert openclaw_memory.exists() + + def test_daily_log_created(self, openclaw_memory): + md_files = list(openclaw_memory.glob("*.md")) + assert len(md_files) > 0 + content = md_files[0].read_text(encoding="utf-8") + assert len(content) > 50, "Daily log is too short to be real" + + def test_daily_log_content(self, openclaw_memory): + md_files = list(openclaw_memory.glob("*.md")) + content = md_files[0].read_text(encoding="utf-8").lower() + assert any(kw in content for kw in ["leann", "testuser", "vector"]), ( + f"Daily log missing expected keywords: {content[:200]}" + ) + + def test_memory_md_exists(self, openclaw_memory): + memory_md = WORKSPACE_DIR / "MEMORY.md" + assert memory_md.exists(), "MEMORY.md not created by OpenClaw" + + def test_memory_md_content(self, openclaw_memory): + memory_md = WORKSPACE_DIR / "MEMORY.md" + if not memory_md.exists(): + pytest.skip("MEMORY.md not created") + content = memory_md.read_text(encoding="utf-8").lower() + assert any(kw in content for kw in ["leann", "hnsw", "mcp"]), ( + f"MEMORY.md missing expected keywords: {content[:200]}" + ) + + +class TestLeannIndexOnRealMemory: + """Verify LEANN can index and search real OpenClaw memory.""" + + def test_index_built(self, leann_index): + result = _leann_cmd(["list"]) + assert result.returncode == 0 + assert leann_index in result.stdout + + def test_search_returns_results(self, leann_index): + result = _leann_cmd( + [ + "search", + leann_index, + "vector database storage compression", + "--top-k=3", + "--non-interactive", + "--json", + ] + ) + assert result.returncode == 0, f"search failed: {result.stderr[:300]}" + results = json.loads(result.stdout) + assert isinstance(results, list) + assert len(results) > 0 + + def test_search_relevance_bug_fix(self, leann_index): + result = _leann_cmd( + [ + "search", + leann_index, + "numpy float32 JSON serialization bug", + "--top-k=3", + "--non-interactive", + "--json", + ] + ) + assert result.returncode == 0 + results = json.loads(result.stdout) + assert len(results) > 0 + top_text = results[0]["text"].lower() + assert any(kw in top_text for kw in ["numpy", "float", "json", "serializ"]), ( + f"Top result not relevant: {top_text[:200]}" + ) + + def test_search_relevance_project_info(self, leann_index): + result = _leann_cmd( + [ + "search", + leann_index, + "what backends does LEANN support", + "--top-k=3", + "--non-interactive", + "--json", + ] + ) + assert result.returncode == 0 + results = json.loads(result.stdout) + assert len(results) > 0 + all_text = " ".join(r["text"].lower() for r in results) + assert any(kw in all_text for kw in ["hnsw", "diskann", "backend"]), ( + f"Results don't mention backends: {all_text[:300]}" + ) + + def test_search_score_is_native_float(self, leann_index): + result = _leann_cmd( + [ + "search", + leann_index, + "LEANN project", + "--top-k=3", + "--non-interactive", + "--json", + ] + ) + assert result.returncode == 0 + results = json.loads(result.stdout) + assert all(isinstance(r["score"], float) for r in results), ( + "score must be native float, not numpy.float32" + ) + + def test_search_metadata_has_file_path(self, leann_index): + result = _leann_cmd( + [ + "search", + leann_index, + "TestUser developer", + "--top-k=1", + "--non-interactive", + "--json", + ] + ) + assert result.returncode == 0 + results = json.loads(result.stdout) + assert len(results) > 0 + meta = results[0].get("metadata", {}) + assert "file_name" in meta + assert meta["file_name"].endswith(".md") diff --git a/tests/openclaw/test_skill_manifest.py b/tests/openclaw/test_skill_manifest.py new file mode 100644 index 0000000..514522e --- /dev/null +++ b/tests/openclaw/test_skill_manifest.py @@ -0,0 +1,61 @@ +"""Validate the ClawHub skill manifest and instructions.""" + + +def test_claw_json_required_fields(claw_manifest): + """claw.json must contain all fields required by ClawHub.""" + required = {"name", "version", "description", "author", "license", "permissions", "entry"} + missing = required - claw_manifest.keys() + assert not missing, f"Missing required fields: {missing}" + + +def test_claw_json_name(claw_manifest): + assert claw_manifest["name"] == "leann-memory" + + +def test_claw_json_permissions(claw_manifest): + """Skill requires shell permission to invoke the leann CLI.""" + assert "shell" in claw_manifest["permissions"] + + +def test_claw_json_entry_exists(skill_dir, claw_manifest): + """The entry file referenced in claw.json must exist.""" + entry = skill_dir / claw_manifest["entry"] + assert entry.exists(), f"Entry file {entry} does not exist" + + +def test_claw_json_tags(claw_manifest): + """Should include relevant tags for discoverability.""" + tags = set(claw_manifest.get("tags", [])) + assert "memory" in tags + assert "search" in tags + + +def test_claw_json_models(claw_manifest): + """Should declare compatible model families.""" + models = claw_manifest.get("models", []) + assert len(models) >= 1, "Must declare at least one compatible model" + + +def test_instructions_contains_build_command(skill_dir, claw_manifest): + """instructions.md should tell the agent how to build an index.""" + instructions = (skill_dir / claw_manifest["entry"]).read_text(encoding="utf-8") + assert "leann build" in instructions + + +def test_instructions_contains_search_command(skill_dir, claw_manifest): + """instructions.md should tell the agent how to search.""" + instructions = (skill_dir / claw_manifest["entry"]).read_text(encoding="utf-8") + assert "leann search" in instructions + assert "--json" in instructions + + +def test_instructions_contains_install_check(skill_dir, claw_manifest): + """instructions.md should have a prerequisite check for leann installation.""" + instructions = (skill_dir / claw_manifest["entry"]).read_text(encoding="utf-8") + assert "which leann" in instructions or "leann --version" in instructions + + +def test_readme_exists(skill_dir): + """README.md should exist for human-facing documentation.""" + readme = skill_dir / "README.md" + assert readme.exists(), "README.md missing from skill directory" diff --git a/tests/support/fake_embedding_server_module.py b/tests/support/fake_embedding_server_module.py new file mode 100644 index 0000000..a4c64cf --- /dev/null +++ b/tests/support/fake_embedding_server_module.py @@ -0,0 +1,59 @@ +import argparse +import signal +import socket +import time + + +def main() -> None: + parser = argparse.ArgumentParser(description="Fake embedding server for e2e tests") + parser.add_argument("--zmq-port", type=int, required=True) + parser.add_argument("--model-name", type=str, required=True) + parser.add_argument("--passages-file", type=str, default="") + parser.add_argument("--distance-metric", type=str, default="") + parser.add_argument("--embedding-mode", type=str, default="sentence-transformers") + parser.add_argument("--enable-warmup", action="store_true") + parser.add_argument("--daemon-mode", action="store_true") + parser.add_argument("--daemon-ttl", type=int, default=0) + args = parser.parse_args() + + stop = {"value": False} + last_activity = time.time() + + def _handle_signal(signum, frame): + stop["value"] = True + + signal.signal(signal.SIGTERM, _handle_signal) + signal.signal(signal.SIGINT, _handle_signal) + + # Bind and keep accepting connections so _check_port sees the process as alive. + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(("127.0.0.1", args.zmq_port)) + server.listen(16) + server.settimeout(0.2) + + try: + while not stop["value"]: + if args.daemon_mode and args.daemon_ttl > 0: + if (time.time() - last_activity) >= args.daemon_ttl: + break + try: + conn, _addr = server.accept() + # liveness probes connect+close without sending payload; do not + # treat them as activity so TTL expiry can still be observed. + try: + conn.settimeout(0.01) + payload = conn.recv(1) + if payload: + last_activity = time.time() + except Exception: + pass + conn.close() + except socket.timeout: + pass + finally: + server.close() + + +if __name__ == "__main__": + main() diff --git a/tests/test_astchunk_integration.py b/tests/test_astchunk_integration.py new file mode 100644 index 0000000..1f2010b --- /dev/null +++ b/tests/test_astchunk_integration.py @@ -0,0 +1,966 @@ +""" +Test suite for astchunk integration with LEANN. +Tests AST-aware chunking functionality, language detection, and fallback mechanisms. +""" + +import os +import subprocess +import sys +import tempfile +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +# Add apps directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent / "apps")) + +from typing import Optional + +from chunking import ( + create_ast_chunks, + create_text_chunks, + create_traditional_chunks, + detect_code_files, + get_language_from_extension, +) + + +class MockDocument: + """Mock LlamaIndex Document for testing.""" + + def __init__(self, content: str, file_path: str = "", metadata: Optional[dict] = None): + self.content = content + self.metadata = metadata or {} + if file_path: + self.metadata["file_path"] = file_path + + def get_content(self) -> str: + return self.content + + +class TestCodeFileDetection: + """Test code file detection and language mapping.""" + + def test_detect_code_files_python(self): + """Test detection of Python files.""" + docs = [ + MockDocument("print('hello')", "/path/to/file.py"), + MockDocument("This is text", "/path/to/file.txt"), + ] + + code_docs, text_docs = detect_code_files(docs) + + assert len(code_docs) == 1 + assert len(text_docs) == 1 + assert code_docs[0].metadata["language"] == "python" + assert code_docs[0].metadata["is_code"] is True + assert text_docs[0].metadata["is_code"] is False + + def test_detect_code_files_multiple_languages(self): + """Test detection of multiple programming languages.""" + docs = [ + MockDocument("def func():", "/path/to/script.py"), + MockDocument("public class Test {}", "/path/to/Test.java"), + MockDocument("interface ITest {}", "/path/to/test.ts"), + MockDocument("using System;", "/path/to/Program.cs"), + MockDocument("Regular text content", "/path/to/document.txt"), + ] + + code_docs, text_docs = detect_code_files(docs) + + assert len(code_docs) == 4 + assert len(text_docs) == 1 + + languages = [doc.metadata["language"] for doc in code_docs] + assert "python" in languages + assert "java" in languages + assert "typescript" in languages + assert "csharp" in languages + + def test_detect_code_files_no_file_path(self): + """Test handling of documents without file paths.""" + docs = [ + MockDocument("some content"), + MockDocument("other content", metadata={"some_key": "value"}), + ] + + code_docs, text_docs = detect_code_files(docs) + + assert len(code_docs) == 0 + assert len(text_docs) == 2 + for doc in text_docs: + assert doc.metadata["is_code"] is False + + def test_get_language_from_extension(self): + """Test language detection from file extensions.""" + assert get_language_from_extension("test.py") == "python" + assert get_language_from_extension("Test.java") == "java" + assert get_language_from_extension("component.tsx") == "typescript" + assert get_language_from_extension("Program.cs") == "csharp" + assert get_language_from_extension("document.txt") is None + assert get_language_from_extension("") is None + + +class TestChunkingFunctions: + """Test various chunking functionality.""" + + def test_create_traditional_chunks(self): + """Test traditional text chunking.""" + docs = [ + MockDocument( + "This is a test document. It has multiple sentences. We want to test chunking." + ) + ] + + chunks = create_traditional_chunks(docs, chunk_size=50, chunk_overlap=10) + + assert len(chunks) > 0 + # Traditional chunks now return dict format for consistency + assert all(isinstance(chunk, dict) for chunk in chunks) + assert all("text" in chunk and "metadata" in chunk for chunk in chunks) + assert all(len(chunk["text"].strip()) > 0 for chunk in chunks) + + def test_create_traditional_chunks_empty_docs(self): + """Test traditional chunking with empty documents.""" + chunks = create_traditional_chunks([], chunk_size=50, chunk_overlap=10) + assert chunks == [] + + @pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip astchunk tests in CI - dependency may not be available", + ) + def test_create_ast_chunks_with_astchunk_available(self): + """Test AST chunking when astchunk is available.""" + python_code = ''' +def hello_world(): + """Print hello world message.""" + print("Hello, World!") + +def add_numbers(a, b): + """Add two numbers and return the result.""" + return a + b + +class Calculator: + """A simple calculator class.""" + + def __init__(self): + self.history = [] + + def add(self, a, b): + result = a + b + self.history.append(f"{a} + {b} = {result}") + return result +''' + + docs = [MockDocument(python_code, "/test/calculator.py", {"language": "python"})] + + try: + chunks = create_ast_chunks(docs, max_chunk_size=200, chunk_overlap=50) + + # Should have multiple chunks due to different functions/classes + assert len(chunks) > 0 + # R3: Expect dict format with "text" and "metadata" keys + assert all(isinstance(chunk, dict) for chunk in chunks), "All chunks should be dicts" + assert all("text" in chunk and "metadata" in chunk for chunk in chunks), ( + "Each chunk should have 'text' and 'metadata' keys" + ) + assert all(len(chunk["text"].strip()) > 0 for chunk in chunks), ( + "Each chunk text should be non-empty" + ) + + # Check metadata is present + assert all("file_path" in chunk["metadata"] for chunk in chunks), ( + "Each chunk should have file_path metadata" + ) + + # Check that code structure is somewhat preserved + combined_content = " ".join([c["text"] for c in chunks]) + assert "def hello_world" in combined_content + assert "class Calculator" in combined_content + + except ImportError: + # astchunk not available, should fall back to traditional chunking + chunks = create_ast_chunks(docs, max_chunk_size=200, chunk_overlap=50) + assert len(chunks) > 0 # Should still get chunks from fallback + + def test_create_ast_chunks_fallback_to_traditional(self): + """Test AST chunking falls back to traditional when astchunk is not available.""" + docs = [MockDocument("def test(): pass", "/test/script.py", {"language": "python"})] + + # Mock astchunk import to fail + with patch("chunking.create_ast_chunks"): + # First call (actual test) should import astchunk and potentially fail + # Let's call the actual function to test the import error handling + chunks = create_ast_chunks(docs) + + # Should return some chunks (either from astchunk or fallback) + assert isinstance(chunks, list) + + def test_create_text_chunks_traditional_mode(self): + """Test text chunking in traditional mode.""" + docs = [ + MockDocument("def test(): pass", "/test/script.py"), + MockDocument("This is regular text.", "/test/doc.txt"), + ] + + chunks = create_text_chunks(docs, use_ast_chunking=False, chunk_size=50, chunk_overlap=10) + + assert len(chunks) > 0 + # R3: Traditional chunking should also return dict format for consistency + assert all(isinstance(chunk, dict) for chunk in chunks), "All chunks should be dicts" + assert all("text" in chunk and "metadata" in chunk for chunk in chunks), ( + "Each chunk should have 'text' and 'metadata' keys" + ) + + def test_create_text_chunks_ast_mode(self): + """Test text chunking in AST mode.""" + docs = [ + MockDocument("def test(): pass", "/test/script.py"), + MockDocument("This is regular text.", "/test/doc.txt"), + ] + + chunks = create_text_chunks( + docs, + use_ast_chunking=True, + ast_chunk_size=100, + ast_chunk_overlap=20, + chunk_size=50, + chunk_overlap=10, + ) + + assert len(chunks) > 0 + # R3: AST mode should also return dict format + assert all(isinstance(chunk, dict) for chunk in chunks), "All chunks should be dicts" + assert all("text" in chunk and "metadata" in chunk for chunk in chunks), ( + "Each chunk should have 'text' and 'metadata' keys" + ) + + def test_create_text_chunks_custom_extensions(self): + """Test text chunking with custom code file extensions.""" + docs = [ + MockDocument("function test() {}", "/test/script.js"), # Not in default extensions + MockDocument("Regular text", "/test/doc.txt"), + ] + + # First without custom extensions - should treat .js as text + chunks_without = create_text_chunks(docs, use_ast_chunking=True, code_file_extensions=None) + + # Then with custom extensions - should treat .js as code + chunks_with = create_text_chunks( + docs, use_ast_chunking=True, code_file_extensions=[".js", ".jsx"] + ) + + # Both should return chunks + assert len(chunks_without) > 0 + assert len(chunks_with) > 0 + + +class TestIntegrationWithDocumentRAG: + """Integration tests with the document RAG system.""" + + @pytest.fixture + def temp_code_dir(self): + """Create a temporary directory with sample code files.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + temp_path = Path(temp_dir) + + # Create sample Python file + python_file = temp_path / "example.py" + python_file.write_text(''' +def fibonacci(n): + """Calculate fibonacci number.""" + if n <= 1: + return n + return fibonacci(n-1) + fibonacci(n-2) + +class MathUtils: + @staticmethod + def factorial(n): + if n <= 1: + return 1 + return n * MathUtils.factorial(n-1) +''') + + # Create sample text file + text_file = temp_path / "readme.txt" + text_file.write_text("This is a sample text file for testing purposes.") + + yield temp_path + + @pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip integration tests in CI to avoid dependency issues", + ) + @pytest.mark.timeout(0) + def test_document_rag_with_ast_chunking(self, temp_code_dir): + """Test document RAG with AST chunking enabled.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as index_dir: + cmd = [ + sys.executable, + "apps/document_rag.py", + "--llm", + "simulated", + "--embedding-model", + "facebook/contriever", + "--embedding-mode", + "sentence-transformers", + "--index-dir", + index_dir, + "--data-dir", + str(temp_code_dir), + "--enable-code-chunking", + "--query", + "How does the fibonacci function work?", + ] + + env = os.environ.copy() + env["HF_HUB_DISABLE_SYMLINKS"] = "1" + env["TOKENIZERS_PARALLELISM"] = "false" + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=300, # 5 minutes + env=env, + ) + + # Should succeed even if astchunk is not available (fallback) + assert result.returncode == 0, f"Command failed: {result.stderr}" + + output = result.stdout + result.stderr + assert "Index saved to" in output or "Using existing index" in output + + except subprocess.TimeoutExpired: + pytest.skip("Test timed out - likely due to model download in CI") + + @pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip integration tests in CI to avoid dependency issues", + ) + @pytest.mark.timeout(0) + def test_code_rag_application(self, temp_code_dir): + """Test the specialized code RAG application.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as index_dir: + cmd = [ + sys.executable, + "apps/code_rag.py", + "--llm", + "simulated", + "--embedding-model", + "facebook/contriever", + "--index-dir", + index_dir, + "--repo-dir", + str(temp_code_dir), + "--query", + "What classes are defined in this code?", + ] + + env = os.environ.copy() + env["HF_HUB_DISABLE_SYMLINKS"] = "1" + env["TOKENIZERS_PARALLELISM"] = "false" + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, env=env) + + # Should succeed + assert result.returncode == 0, f"Command failed: {result.stderr}" + + output = result.stdout + result.stderr + assert "Using AST-aware chunking" in output or "traditional chunking" in output + + except subprocess.TimeoutExpired: + pytest.skip("Test timed out - likely due to model download in CI") + + +class TestASTContentExtraction: + """Test AST content extraction bug fix. + + These tests verify that astchunk's dict format with 'content' key is handled correctly, + and that the extraction logic doesn't fall through to stringifying entire dicts. + """ + + def test_extract_content_from_astchunk_dict(self): + """Test that astchunk dict format with 'content' key is handled correctly. + + Bug: Current code checks for chunk["text"] but astchunk returns chunk["content"]. + This causes fallthrough to str(chunk), stringifying the entire dict. + + This test will FAIL until the bug is fixed because: + - Current code will stringify the dict: "{'content': '...', 'metadata': {...}}" + - Fixed code should extract just the content value + """ + # Mock the ASTChunkBuilder class + mock_builder = Mock() + + # Astchunk returns this format + astchunk_format_chunk = { + "content": "def hello():\n print('world')", + "metadata": { + "filepath": "test.py", + "line_count": 2, + "start_line_no": 0, + "end_line_no": 1, + "node_count": 1, + }, + } + mock_builder.chunkify.return_value = [astchunk_format_chunk] + + # Create mock document + doc = MockDocument( + "def hello():\n print('world')", "/test/test.py", {"language": "python"} + ) + + # Mock the astchunk module and its ASTChunkBuilder class + mock_astchunk = Mock() + mock_astchunk.ASTChunkBuilder = Mock(return_value=mock_builder) + + # Patch sys.modules to inject our mock before the import + with patch.dict("sys.modules", {"astchunk": mock_astchunk}): + # Call create_ast_chunks + chunks = create_ast_chunks([doc]) + + # R3: Should return dict format with proper metadata + assert len(chunks) > 0, "Should return at least one chunk" + + # R3: Each chunk should be a dict + chunk = chunks[0] + assert isinstance(chunk, dict), "Chunk should be a dict" + assert "text" in chunk, "Chunk should have 'text' key" + assert "metadata" in chunk, "Chunk should have 'metadata' key" + + chunk_text = chunk["text"] + + # CRITICAL: Should NOT contain stringified dict markers in the text field + # These assertions will FAIL with current buggy code + assert "'content':" not in chunk_text, ( + f"Chunk text contains stringified dict - extraction failed! Got: {chunk_text[:100]}..." + ) + assert "'metadata':" not in chunk_text, ( + "Chunk text contains stringified metadata - extraction failed! " + f"Got: {chunk_text[:100]}..." + ) + assert "{" not in chunk_text or "def hello" in chunk_text.split("{")[0], ( + "Chunk text appears to be a stringified dict" + ) + + # Should contain actual content + assert "def hello()" in chunk_text, "Should extract actual code content" + assert "print('world')" in chunk_text, "Should extract complete code content" + + # R3: Should preserve astchunk metadata + assert "filepath" in chunk["metadata"] or "file_path" in chunk["metadata"], ( + "Should preserve file path metadata" + ) + + def test_extract_text_key_fallback(self): + """Test that 'text' key still works for backward compatibility. + + Some chunks might use 'text' instead of 'content' - ensure backward compatibility. + This test should PASS even with current code. + """ + mock_builder = Mock() + + # Some chunks might use "text" key + text_key_chunk = {"text": "def legacy_function():\n return True"} + mock_builder.chunkify.return_value = [text_key_chunk] + + # Create mock document + doc = MockDocument( + "def legacy_function():\n return True", "/test/legacy.py", {"language": "python"} + ) + + # Mock the astchunk module + mock_astchunk = Mock() + mock_astchunk.ASTChunkBuilder = Mock(return_value=mock_builder) + + with patch.dict("sys.modules", {"astchunk": mock_astchunk}): + # Call create_ast_chunks + chunks = create_ast_chunks([doc]) + + # R3: Should extract text correctly as dict format + assert len(chunks) > 0 + chunk = chunks[0] + assert isinstance(chunk, dict), "Chunk should be a dict" + assert "text" in chunk, "Chunk should have 'text' key" + + chunk_text = chunk["text"] + + # Should NOT be stringified + assert "'text':" not in chunk_text, "Should not stringify dict with 'text' key" + + # Should contain actual content + assert "def legacy_function()" in chunk_text + assert "return True" in chunk_text + + def test_handles_string_chunks(self): + """Test that plain string chunks still work. + + Some chunkers might return plain strings - verify these are preserved. + This test should PASS with current code. + """ + mock_builder = Mock() + + # Plain string chunk + plain_string_chunk = "def simple_function():\n pass" + mock_builder.chunkify.return_value = [plain_string_chunk] + + # Create mock document + doc = MockDocument( + "def simple_function():\n pass", "/test/simple.py", {"language": "python"} + ) + + # Mock the astchunk module + mock_astchunk = Mock() + mock_astchunk.ASTChunkBuilder = Mock(return_value=mock_builder) + + with patch.dict("sys.modules", {"astchunk": mock_astchunk}): + # Call create_ast_chunks + chunks = create_ast_chunks([doc]) + + # R3: Should wrap string in dict format + assert len(chunks) > 0 + chunk = chunks[0] + assert isinstance(chunk, dict), "Even string chunks should be wrapped in dict" + assert "text" in chunk, "Chunk should have 'text' key" + + chunk_text = chunk["text"] + + assert chunk_text == plain_string_chunk.strip(), ( + "Should preserve plain string chunk content" + ) + assert "def simple_function()" in chunk_text + assert "pass" in chunk_text + + def test_multiple_chunks_with_mixed_formats(self): + """Test handling of multiple chunks with different formats. + + Real-world scenario: astchunk might return a mix of formats. + This test will FAIL if any chunk with 'content' key gets stringified. + """ + mock_builder = Mock() + + # Mix of formats + mixed_chunks = [ + {"content": "def first():\n return 1", "metadata": {"line_count": 2}}, + "def second():\n return 2", # Plain string + {"text": "def third():\n return 3"}, # Old format + {"content": "class MyClass:\n pass", "metadata": {"node_count": 1}}, + ] + mock_builder.chunkify.return_value = mixed_chunks + + # Create mock document + code = "def first():\n return 1\n\ndef second():\n return 2\n\ndef third():\n return 3\n\nclass MyClass:\n pass" + doc = MockDocument(code, "/test/mixed.py", {"language": "python"}) + + # Mock the astchunk module + mock_astchunk = Mock() + mock_astchunk.ASTChunkBuilder = Mock(return_value=mock_builder) + + with patch.dict("sys.modules", {"astchunk": mock_astchunk}): + # Call create_ast_chunks + chunks = create_ast_chunks([doc]) + + # R3: Should extract all chunks correctly as dicts + assert len(chunks) == 4, "Should extract all 4 chunks" + + # Check each chunk + for i, chunk in enumerate(chunks): + assert isinstance(chunk, dict), f"Chunk {i} should be a dict" + assert "text" in chunk, f"Chunk {i} should have 'text' key" + assert "metadata" in chunk, f"Chunk {i} should have 'metadata' key" + + chunk_text = chunk["text"] + # None should be stringified dicts + assert "'content':" not in chunk_text, f"Chunk {i} text is stringified (has 'content':)" + assert "'metadata':" not in chunk_text, ( + f"Chunk {i} text is stringified (has 'metadata':)" + ) + assert "'text':" not in chunk_text, f"Chunk {i} text is stringified (has 'text':)" + + # Verify actual content is present + combined = "\n".join([c["text"] for c in chunks]) + assert "def first()" in combined + assert "def second()" in combined + assert "def third()" in combined + assert "class MyClass:" in combined + + def test_empty_content_value_handling(self): + """Test handling of chunks with empty content values. + + Edge case: chunk has 'content' key but value is empty. + Should skip these chunks, not stringify them. + """ + mock_builder = Mock() + + chunks_with_empty = [ + {"content": "", "metadata": {"line_count": 0}}, # Empty content + {"content": " ", "metadata": {"line_count": 1}}, # Whitespace only + {"content": "def valid():\n return True", "metadata": {"line_count": 2}}, # Valid + ] + mock_builder.chunkify.return_value = chunks_with_empty + + doc = MockDocument( + "def valid():\n return True", "/test/empty.py", {"language": "python"} + ) + + # Mock the astchunk module + mock_astchunk = Mock() + mock_astchunk.ASTChunkBuilder = Mock(return_value=mock_builder) + + with patch.dict("sys.modules", {"astchunk": mock_astchunk}): + chunks = create_ast_chunks([doc]) + + # R3: Should only have the valid chunk (empty ones filtered out) + assert len(chunks) == 1, "Should filter out empty content chunks" + + chunk = chunks[0] + assert isinstance(chunk, dict), "Chunk should be a dict" + assert "text" in chunk, "Chunk should have 'text' key" + assert "def valid()" in chunk["text"] + + # Should not have stringified the empty dict + assert "'content': ''" not in chunk["text"] + + +class TestASTMetadataPreservation: + """Test metadata preservation in AST chunk dictionaries. + + R3: These tests define the contract for metadata preservation when returning + chunk dictionaries instead of plain strings. Each chunk dict should have: + - "text": str - the actual chunk content + - "metadata": dict - all metadata from document AND astchunk + + These tests will FAIL until G3 implementation changes return type to list[dict]. + """ + + def test_ast_chunks_preserve_file_metadata(self): + """Test that document metadata is preserved in chunk metadata. + + This test verifies that all document-level metadata (file_path, file_name, + creation_date, last_modified_date) is included in each chunk's metadata dict. + + This will FAIL because current code returns list[str], not list[dict]. + """ + # Create mock document with rich metadata + python_code = ''' +def calculate_sum(numbers): + """Calculate sum of numbers.""" + return sum(numbers) + +class DataProcessor: + """Process data records.""" + + def process(self, data): + return [x * 2 for x in data] +''' + doc = MockDocument( + python_code, + file_path="/project/src/utils.py", + metadata={ + "language": "python", + "file_path": "/project/src/utils.py", + "file_name": "utils.py", + "creation_date": "2024-01-15T10:30:00", + "last_modified_date": "2024-10-31T15:45:00", + }, + ) + + # Mock astchunk to return chunks with metadata + mock_builder = Mock() + astchunk_chunks = [ + { + "content": "def calculate_sum(numbers):\n return sum(numbers)", + "metadata": { + "filepath": "/project/src/utils.py", + "line_count": 2, + "start_line_no": 1, + "end_line_no": 2, + "node_count": 1, + }, + }, + { + "content": "class DataProcessor:\n def process(self, data):\n return [x * 2 for x in data]", + "metadata": { + "filepath": "/project/src/utils.py", + "line_count": 3, + "start_line_no": 5, + "end_line_no": 7, + "node_count": 2, + }, + }, + ] + mock_builder.chunkify.return_value = astchunk_chunks + + mock_astchunk = Mock() + mock_astchunk.ASTChunkBuilder = Mock(return_value=mock_builder) + + with patch.dict("sys.modules", {"astchunk": mock_astchunk}): + chunks = create_ast_chunks([doc]) + + # CRITICAL: These assertions will FAIL with current list[str] return type + assert len(chunks) == 2, "Should return 2 chunks" + + for i, chunk in enumerate(chunks): + # Structure assertions - WILL FAIL: current code returns strings + assert isinstance(chunk, dict), f"Chunk {i} should be dict, got {type(chunk)}" + assert "text" in chunk, f"Chunk {i} must have 'text' key" + assert "metadata" in chunk, f"Chunk {i} must have 'metadata' key" + assert isinstance(chunk["metadata"], dict), f"Chunk {i} metadata should be dict" + + # Document metadata preservation - WILL FAIL + metadata = chunk["metadata"] + assert "file_path" in metadata, f"Chunk {i} should preserve file_path" + assert metadata["file_path"] == "/project/src/utils.py", ( + f"Chunk {i} file_path incorrect" + ) + + assert "file_name" in metadata, f"Chunk {i} should preserve file_name" + assert metadata["file_name"] == "utils.py", f"Chunk {i} file_name incorrect" + + assert "creation_date" in metadata, f"Chunk {i} should preserve creation_date" + assert metadata["creation_date"] == "2024-01-15T10:30:00", ( + f"Chunk {i} creation_date incorrect" + ) + + assert "last_modified_date" in metadata, f"Chunk {i} should preserve last_modified_date" + assert metadata["last_modified_date"] == "2024-10-31T15:45:00", ( + f"Chunk {i} last_modified_date incorrect" + ) + + # Verify metadata is consistent across chunks from same document + assert chunks[0]["metadata"]["file_path"] == chunks[1]["metadata"]["file_path"], ( + "All chunks from same document should have same file_path" + ) + + # Verify text content is present and not stringified + assert "def calculate_sum" in chunks[0]["text"] + assert "class DataProcessor" in chunks[1]["text"] + + def test_ast_chunks_include_astchunk_metadata(self): + """Test that astchunk-specific metadata is merged into chunk metadata. + + This test verifies that astchunk's metadata (line_count, start_line_no, + end_line_no, node_count) is merged with document metadata. + + This will FAIL because current code returns list[str], not list[dict]. + """ + python_code = ''' +def function_one(): + """First function.""" + x = 1 + y = 2 + return x + y + +def function_two(): + """Second function.""" + return 42 +''' + doc = MockDocument( + python_code, + file_path="/test/code.py", + metadata={ + "language": "python", + "file_path": "/test/code.py", + "file_name": "code.py", + }, + ) + + # Mock astchunk with detailed metadata + mock_builder = Mock() + astchunk_chunks = [ + { + "content": "def function_one():\n x = 1\n y = 2\n return x + y", + "metadata": { + "filepath": "/test/code.py", + "line_count": 4, + "start_line_no": 1, + "end_line_no": 4, + "node_count": 5, # function, assignments, return + }, + }, + { + "content": "def function_two():\n return 42", + "metadata": { + "filepath": "/test/code.py", + "line_count": 2, + "start_line_no": 7, + "end_line_no": 8, + "node_count": 2, # function, return + }, + }, + ] + mock_builder.chunkify.return_value = astchunk_chunks + + mock_astchunk = Mock() + mock_astchunk.ASTChunkBuilder = Mock(return_value=mock_builder) + + with patch.dict("sys.modules", {"astchunk": mock_astchunk}): + chunks = create_ast_chunks([doc]) + + # CRITICAL: These will FAIL with current list[str] return + assert len(chunks) == 2 + + # First chunk - function_one + chunk1 = chunks[0] + assert isinstance(chunk1, dict), "Chunk should be dict" + assert "metadata" in chunk1 + + metadata1 = chunk1["metadata"] + + # Check astchunk metadata is present + assert "line_count" in metadata1, "Should include astchunk line_count" + assert metadata1["line_count"] == 4, "line_count should be 4" + + assert "start_line_no" in metadata1, "Should include astchunk start_line_no" + assert metadata1["start_line_no"] == 1, "start_line_no should be 1" + + assert "end_line_no" in metadata1, "Should include astchunk end_line_no" + assert metadata1["end_line_no"] == 4, "end_line_no should be 4" + + assert "node_count" in metadata1, "Should include astchunk node_count" + assert metadata1["node_count"] == 5, "node_count should be 5" + + # Second chunk - function_two + chunk2 = chunks[1] + metadata2 = chunk2["metadata"] + + assert metadata2["line_count"] == 2, "line_count should be 2" + assert metadata2["start_line_no"] == 7, "start_line_no should be 7" + assert metadata2["end_line_no"] == 8, "end_line_no should be 8" + assert metadata2["node_count"] == 2, "node_count should be 2" + + # Verify document metadata is ALSO present (merged, not replaced) + assert metadata1["file_path"] == "/test/code.py" + assert metadata1["file_name"] == "code.py" + assert metadata2["file_path"] == "/test/code.py" + assert metadata2["file_name"] == "code.py" + + # Verify text content is correct + assert "def function_one" in chunk1["text"] + assert "def function_two" in chunk2["text"] + + def test_traditional_chunks_as_dicts_helper(self): + """Test the helper function that wraps traditional chunks as dicts. + + This test verifies that when create_traditional_chunks is called, + its plain string chunks are wrapped into dict format with metadata. + + This will FAIL because the helper function _traditional_chunks_as_dicts() + doesn't exist yet, and create_traditional_chunks returns list[str]. + """ + # Create documents with various metadata + docs = [ + MockDocument( + "This is the first paragraph of text. It contains multiple sentences. " + "This should be split into chunks based on size.", + file_path="/docs/readme.txt", + metadata={ + "file_path": "/docs/readme.txt", + "file_name": "readme.txt", + "creation_date": "2024-01-01", + }, + ), + MockDocument( + "Second document with different metadata. It also has content that needs chunking.", + file_path="/docs/guide.md", + metadata={ + "file_path": "/docs/guide.md", + "file_name": "guide.md", + "last_modified_date": "2024-10-31", + }, + ), + ] + + # Call create_traditional_chunks (which should now return list[dict]) + chunks = create_traditional_chunks(docs, chunk_size=50, chunk_overlap=10) + + # CRITICAL: Will FAIL - current code returns list[str] + assert len(chunks) > 0, "Should return chunks" + + for i, chunk in enumerate(chunks): + # Structure assertions - WILL FAIL + assert isinstance(chunk, dict), f"Chunk {i} should be dict, got {type(chunk)}" + assert "text" in chunk, f"Chunk {i} must have 'text' key" + assert "metadata" in chunk, f"Chunk {i} must have 'metadata' key" + + # Text should be non-empty + assert len(chunk["text"].strip()) > 0, f"Chunk {i} text should be non-empty" + + # Metadata should include document info + metadata = chunk["metadata"] + assert "file_path" in metadata, f"Chunk {i} should have file_path in metadata" + assert "file_name" in metadata, f"Chunk {i} should have file_name in metadata" + + # Verify metadata tracking works correctly + # At least one chunk should be from readme.txt + readme_chunks = [c for c in chunks if "readme.txt" in c["metadata"]["file_name"]] + assert len(readme_chunks) > 0, "Should have chunks from readme.txt" + + # At least one chunk should be from guide.md + guide_chunks = [c for c in chunks if "guide.md" in c["metadata"]["file_name"]] + assert len(guide_chunks) > 0, "Should have chunks from guide.md" + + # Verify creation_date is preserved for readme chunks + for chunk in readme_chunks: + assert chunk["metadata"].get("creation_date") == "2024-01-01", ( + "readme.txt chunks should preserve creation_date" + ) + + # Verify last_modified_date is preserved for guide chunks + for chunk in guide_chunks: + assert chunk["metadata"].get("last_modified_date") == "2024-10-31", ( + "guide.md chunks should preserve last_modified_date" + ) + + # Verify text content is present + all_text = " ".join([c["text"] for c in chunks]) + assert "first paragraph" in all_text + assert "Second document" in all_text + + +class TestErrorHandling: + """Test error handling and edge cases.""" + + def test_text_chunking_empty_documents(self): + """Test text chunking with empty document list.""" + chunks = create_text_chunks([]) + assert chunks == [] + + def test_text_chunking_invalid_parameters(self): + """Test text chunking with invalid parameters.""" + docs = [MockDocument("test content")] + + # Should handle negative chunk sizes gracefully + chunks = create_text_chunks( + docs, chunk_size=0, chunk_overlap=0, ast_chunk_size=0, ast_chunk_overlap=0 + ) + + # Should still return some result + assert isinstance(chunks, list) + + def test_create_ast_chunks_no_language(self): + """Test AST chunking with documents missing language metadata.""" + docs = [MockDocument("def test(): pass", "/test/script.py")] # No language set + + chunks = create_ast_chunks(docs) + + # Should fall back to traditional chunking + assert isinstance(chunks, list) + assert len(chunks) >= 0 # May be empty if fallback also fails + + def test_create_ast_chunks_empty_content(self): + """Test AST chunking with empty content.""" + docs = [MockDocument("", "/test/script.py", {"language": "python"})] + + chunks = create_ast_chunks(docs) + + # Should handle empty content gracefully + assert isinstance(chunks, list) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..cbaa078 --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,85 @@ +""" +Basic functionality tests for CI pipeline using pytest. +""" + +import os +import tempfile +from pathlib import Path + +import pytest + + +def test_imports(): + """Test that all packages can be imported.""" + + # Test C++ extensions + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip model tests in CI to avoid MPS memory issues" +) +@pytest.mark.parametrize("backend_name", ["hnsw", "diskann"]) +def test_backend_basic(backend_name): + """Test basic functionality for each backend.""" + from leann.api import LeannBuilder, LeannSearcher, SearchResult + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / f"test.{backend_name}") + + texts = [f"This is document {i} about topic {i % 5}" for i in range(100)] + + if backend_name == "hnsw": + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + M=16, + efConstruction=200, + ) + else: # diskann + builder = LeannBuilder( + backend_name="diskann", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + num_neighbors=32, + search_list_size=50, + ) + + for text in texts: + builder.add_text(text) + + builder.build_index(index_path) + + with LeannSearcher(index_path) as searcher: + results = searcher.search("document about topic 2", top_k=5) + + assert len(results) > 0 + assert isinstance(results[0], SearchResult) + assert "topic 2" in results[0].text or "document" in results[0].text + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip model tests in CI to avoid MPS memory issues" +) +def test_large_index(): + """Test with larger dataset.""" + from leann.api import LeannBuilder, LeannSearcher + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "test_large.hnsw") + texts = [f"Document {i}: {' '.join([f'word{j}' for j in range(50)])}" for i in range(1000)] + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + ) + + for text in texts: + builder.add_text(text) + + builder.build_index(index_path) + + with LeannSearcher(index_path) as searcher: + results = searcher.search("word10 word20", top_k=10) + assert len(results) == 10 diff --git a/tests/test_build_from_arrays.py b/tests/test_build_from_arrays.py new file mode 100644 index 0000000..d917c8f --- /dev/null +++ b/tests/test_build_from_arrays.py @@ -0,0 +1,328 @@ +""" +Tests for LeannBuilder.build_index_from_arrays and its integration with +build_index_from_embeddings (pickle-based path). +""" + +import os +import pickle +import tempfile +from pathlib import Path + +import numpy as np +import pytest + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip model tests in CI to avoid MPS memory issues" +) +def test_build_from_arrays_basic(): + """Generate real embeddings for 5 texts, build via build_index_from_arrays, verify searchable.""" + from leann.api import LeannBuilder, LeannSearcher, compute_embeddings + + texts = [ + "The quick brown fox jumps over the lazy dog", + "Machine learning is a subset of artificial intelligence", + "Python is a high-level programming language", + "Neural networks are inspired by the human brain", + "Natural language processing enables computers to understand text", + ] + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "test_arrays.hnsw") + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + ) + + embeddings = compute_embeddings( + texts, + model_name="facebook/contriever", + mode="sentence-transformers", + use_server=False, + is_build=True, + ) + + for text in texts: + builder.add_text(text) + + ids = list(range(len(texts))) + builder.build_index_from_arrays(index_path, ids, embeddings) + + with LeannSearcher(index_path) as searcher: + results = searcher.search("artificial intelligence machine learning", top_k=3) + assert len(results) > 0 + assert any("intelligence" in r.text or "learning" in r.text for r in results) + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip model tests in CI to avoid MPS memory issues" +) +def test_build_from_arrays_matches_pickle_path(): + """Build same data via both methods, verify both produce searchable indexes.""" + from leann.api import LeannBuilder, LeannSearcher, compute_embeddings + + texts = [ + "The sun rises in the east", + "Water flows downhill due to gravity", + "Birds migrate south in winter", + "Cats are independent animals", + "Mathematics is the language of the universe", + ] + + embeddings = compute_embeddings( + texts, + model_name="facebook/contriever", + mode="sentence-transformers", + use_server=False, + is_build=True, + ) + ids = list(range(len(texts))) + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + # Build via arrays method + arrays_index = str(Path(temp_dir) / "arrays_index.hnsw") + builder_arrays = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + ) + for text in texts: + builder_arrays.add_text(text) + builder_arrays.build_index_from_arrays(arrays_index, ids, embeddings) + + # Build via pickle method + pickle_index = str(Path(temp_dir) / "pickle_index.hnsw") + pickle_path = str(Path(temp_dir) / "embeddings.pkl") + with open(pickle_path, "wb") as f: + pickle.dump((ids, embeddings), f) + + builder_pickle = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + ) + for text in texts: + builder_pickle.add_text(text) + builder_pickle.build_index_from_embeddings(pickle_index, pickle_path) + + query = "birds animals nature" + + with LeannSearcher(arrays_index) as searcher: + arrays_results = searcher.search(query, top_k=3) + assert len(arrays_results) > 0 + + with LeannSearcher(pickle_index) as searcher: + pickle_results = searcher.search(query, top_k=3) + assert len(pickle_results) > 0 + + # Both should return results (texts may differ slightly due to HNSW non-determinism, + # but both indexes should be functional) + assert len(arrays_results) == len(pickle_results) + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip model tests in CI to avoid MPS memory issues" +) +def test_build_from_arrays_with_text_chunks(): + """Call add_text first, then build_index_from_arrays; verify passages contain actual text.""" + from leann.api import LeannBuilder, LeannSearcher, compute_embeddings + + texts = [ + "Elephants are the largest land animals", + "The Amazon rainforest is the world's largest tropical rainforest", + "Quantum computing uses quantum mechanical phenomena", + ] + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "with_chunks.hnsw") + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + ) + + for text in texts: + builder.add_text(text) + + embeddings = compute_embeddings( + texts, + model_name="facebook/contriever", + mode="sentence-transformers", + use_server=False, + is_build=True, + ) + ids = list(range(len(texts))) + builder.build_index_from_arrays(index_path, ids, embeddings) + + # Check that the passages JSONL contains real text, not placeholders + passages_file = Path(index_path).parent / f"{Path(index_path).name}.passages.jsonl" + assert passages_file.exists() + import json + + passage_texts = [] + with open(passages_file, encoding="utf-8") as f: + for line in f: + line = line.strip() + if line: + passage_texts.append(json.loads(line)["text"]) + + assert len(passage_texts) == len(texts) + # Actual texts, not placeholders like "Document 0" + for actual_text in texts: + assert actual_text in passage_texts + + with LeannSearcher(index_path) as searcher: + results = searcher.search("large animals nature", top_k=2) + assert len(results) > 0 + assert not any(r.text.startswith("Document ") for r in results) + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip model tests in CI to avoid MPS memory issues" +) +def test_build_from_arrays_dimension_mismatch(): + """Set builder dimensions to 100, pass 768-dim embeddings, expect ValueError.""" + from leann.api import LeannBuilder + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "dim_mismatch.hnsw") + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + dimensions=100, + ) + + ids = [0, 1, 2] + # 768-dim embeddings (contriever default) when builder expects 100 + embeddings = np.random.rand(3, 768).astype(np.float32) + + with pytest.raises(ValueError, match="[Dd]imension"): + builder.build_index_from_arrays(index_path, ids, embeddings) + + +def test_build_from_arrays_count_mismatch(): + """Pass 3 ids but 5 embeddings, expect ValueError.""" + from leann.api import LeannBuilder + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "count_mismatch.hnsw") + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + ) + + ids = [0, 1, 2] # 3 ids + embeddings = np.random.rand(5, 768).astype(np.float32) # 5 embeddings + + with pytest.raises(ValueError, match="[Mm]ismatch"): + builder.build_index_from_arrays(index_path, ids, embeddings) + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip model tests in CI to avoid MPS memory issues" +) +def test_build_from_arrays_without_chunks_creates_placeholders(): + """Call build_index_from_arrays without prior add_text; verify placeholder entries created.""" + from leann.api import LeannBuilder, LeannSearcher, compute_embeddings + + texts_for_embedding = [ + "Placeholder document alpha", + "Placeholder document beta", + "Placeholder document gamma", + ] + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "no_chunks.hnsw") + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + ) + + embeddings = compute_embeddings( + texts_for_embedding, + model_name="facebook/contriever", + mode="sentence-transformers", + use_server=False, + is_build=True, + ) + ids = ["doc-a", "doc-b", "doc-c"] + + # No add_text calls β€” builder has no chunks + builder.build_index_from_arrays(index_path, ids, embeddings) + + # Check passages file has placeholder entries + import json + + passages_file = Path(index_path).parent / f"{Path(index_path).name}.passages.jsonl" + assert passages_file.exists() + passage_texts = [] + with open(passages_file, encoding="utf-8") as f: + for line in f: + line = line.strip() + if line: + passage_texts.append(json.loads(line)["text"]) + + assert len(passage_texts) == len(ids) + # All entries should be placeholders ("Document ") + for text in passage_texts: + assert text.startswith("Document ") + + # Index should still be searchable + with LeannSearcher(index_path) as searcher: + results = searcher.search("document placeholder", top_k=2) + assert len(results) > 0 + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip model tests in CI to avoid MPS memory issues" +) +def test_pickle_method_delegates_to_arrays(): + """Verify build_index_from_embeddings still works after refactor (regression test).""" + from leann.api import LeannBuilder, LeannSearcher, compute_embeddings + + texts = [ + "Regression test document one about science", + "Regression test document two about history", + "Regression test document three about art", + ] + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "regression.hnsw") + pickle_path = str(Path(temp_dir) / "embeddings.pkl") + + embeddings = compute_embeddings( + texts, + model_name="facebook/contriever", + mode="sentence-transformers", + use_server=False, + is_build=True, + ) + ids = list(range(len(texts))) + + with open(pickle_path, "wb") as f: + pickle.dump((ids, embeddings), f) + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + ) + for text in texts: + builder.add_text(text) + + # This should still work exactly as before + builder.build_index_from_embeddings(index_path, pickle_path) + + with LeannSearcher(index_path) as searcher: + results = searcher.search("science history", top_k=2) + assert len(results) > 0 + assert isinstance(results[0].text, str) diff --git a/tests/test_ci_minimal.py b/tests/test_ci_minimal.py new file mode 100644 index 0000000..b884cbe --- /dev/null +++ b/tests/test_ci_minimal.py @@ -0,0 +1,49 @@ +""" +Minimal tests for CI that don't require model loading or significant memory. +""" + +import subprocess +import sys + + +def test_package_imports(): + """Test that all core packages can be imported.""" + # Core package + + # Backend packages + + # Core modules + + assert True # If we get here, imports worked + + +def test_cli_help(): + """Test that CLI example shows help.""" + result = subprocess.run( + [sys.executable, "apps/document_rag.py", "--help"], capture_output=True, text=True + ) + + assert result.returncode == 0 + assert "usage:" in result.stdout.lower() or "usage:" in result.stderr.lower() + assert "--llm" in result.stdout or "--llm" in result.stderr + + +def test_backend_registration(): + """Test that backends are properly registered.""" + from leann.api import get_registered_backends + + backends = get_registered_backends() + assert "hnsw" in backends + assert "diskann" in backends + + +def test_version_info(): + """Test that packages have version information.""" + import leann + import leann_backend_diskann + import leann_backend_hnsw + + # Check that packages have __version__ or can be imported + assert hasattr(leann, "__version__") or True + assert hasattr(leann_backend_hnsw, "__version__") or True + assert hasattr(leann_backend_diskann, "__version__") or True diff --git a/tests/test_cli_ask.py b/tests/test_cli_ask.py new file mode 100644 index 0000000..6cac456 --- /dev/null +++ b/tests/test_cli_ask.py @@ -0,0 +1,80 @@ +import asyncio +import json + +from leann.cli import LeannCLI + + +def test_cli_ask_accepts_positional_query(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + cli = LeannCLI() + parser = cli.create_parser() + + args = parser.parse_args(["ask", "my-docs", "Where are prompts configured?"]) + + assert args.command == "ask" + assert args.index_name == "my-docs" + assert args.query == "Where are prompts configured?" + + +def test_cli_ask_parses_metadata_filters_flag(): + cli = LeannCLI() + parser = cli.create_parser() + + filters_json = '{"chapter": {"<=": 5}, "genre": {"==": "fiction"}}' + args = parser.parse_args( + ["ask", "my-docs", "Summarize early chapters", "--metadata-filters", filters_json] + ) + + assert args.command == "ask" + assert args.metadata_filters == filters_json + # The raw string parses to the expected dict so downstream consumers can rely on it. + assert json.loads(args.metadata_filters) == { + "chapter": {"<=": 5}, + "genre": {"==": "fiction"}, + } + + +def test_cli_ask_metadata_filters_default_is_none(): + cli = LeannCLI() + parser = cli.create_parser() + + args = parser.parse_args(["ask", "my-docs", "any query"]) + + assert args.metadata_filters is None + + +def test_cli_ask_rejects_invalid_metadata_filters_json(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + + cli = LeannCLI() + parser = cli.create_parser() + # Set up an empty index dir so the existence check passes and we reach the JSON parser. + index_dir = tmp_path / ".leann" / "indexes" / "my-docs" + index_dir.mkdir(parents=True) + (index_dir / "documents.leann.meta.json").write_text("{}") + + args = parser.parse_args(["ask", "my-docs", "any query", "--metadata-filters", "not-json"]) + + asyncio.run(cli.ask_questions(args)) + + captured = capsys.readouterr() + assert "--metadata-filters is not valid JSON" in captured.out + + +def test_cli_ask_rejects_non_object_metadata_filters(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + + cli = LeannCLI() + parser = cli.create_parser() + index_dir = tmp_path / ".leann" / "indexes" / "my-docs" + index_dir.mkdir(parents=True) + (index_dir / "documents.leann.meta.json").write_text("{}") + + # A valid JSON value that is not an object (dict) β€” must be rejected. + args = parser.parse_args(["ask", "my-docs", "any query", "--metadata-filters", "[1, 2, 3]"]) + + asyncio.run(cli.ask_questions(args)) + + captured = capsys.readouterr() + assert "--metadata-filters must be a JSON object" in captured.out diff --git a/tests/test_cli_daemon_workflow.py b/tests/test_cli_daemon_workflow.py new file mode 100644 index 0000000..43323e0 --- /dev/null +++ b/tests/test_cli_daemon_workflow.py @@ -0,0 +1,212 @@ +import argparse +import asyncio +from pathlib import Path +from typing import Any + +from leann.cli import LeannCLI + + +def test_search_passes_daemon_flags_to_searcher(monkeypatch): + cli = LeannCLI() + monkeypatch.setattr( + cli, + "_resolve_index_path", + lambda *args, **kwargs: "/tmp/demo/documents.leann", + ) + + captured: dict[str, dict[str, Any]] = {"init": {}, "search": {}} + + class DummySearcher: + def __init__(self, *args, **kwargs): + captured["init"] = kwargs + + def search(self, *args, **kwargs): + captured["search"] = kwargs + return [] + + monkeypatch.setattr("leann.cli.LeannSearcher", DummySearcher) + + args = argparse.Namespace( + index_name="demo", + query="hello", + top_k=3, + complexity=64, + beam_width=1, + prune_ratio=0.0, + recompute_embeddings=True, + pruning_strategy="global", + non_interactive=True, + show_metadata=False, + embedding_prompt_template=None, + use_daemon=True, + daemon_ttl=222, + enable_warmup=True, + ) + asyncio.run(cli.search_documents(args)) + + assert captured["init"]["enable_warmup"] is True + assert captured["init"]["use_daemon"] is True + assert captured["init"]["daemon_ttl_seconds"] == 222 + assert captured["search"]["recompute_embeddings"] is True + + +def test_warmup_command_calls_searcher_warmup(monkeypatch): + cli = LeannCLI() + monkeypatch.setattr( + cli, + "_resolve_index_path", + lambda *args, **kwargs: "/tmp/demo/documents.leann", + ) + + state: dict[str, int] = {"warmup_called": 0} + + class DummySearcher: + def __init__(self, *args, **kwargs): + pass + + def warmup(self): + state["warmup_called"] += 1 + + monkeypatch.setattr("leann.cli.LeannSearcher", DummySearcher) + + args = argparse.Namespace( + index_name="demo", + use_daemon=True, + daemon_ttl=120, + enable_warmup=True, + ) + asyncio.run(cli.warmup_index(args)) + assert state["warmup_called"] == 1 + + +def test_daemon_status_filters_by_index(monkeypatch, capsys): + cli = LeannCLI() + monkeypatch.setattr( + cli, + "_resolve_index_path", + lambda *args, **kwargs: "/tmp/demo/documents.leann", + ) + meta_path = str(Path("/tmp/demo/documents.leann.meta.json").resolve()) + + monkeypatch.setattr( + "leann.cli.EmbeddingServerManager.list_daemons", + lambda: [ + { + "pid": 101, + "port": 5557, + "backend_module_name": "leann_backend_hnsw.hnsw_embedding_server", + "config_signature": {"passages_file": meta_path, "model_name": "m1"}, + }, + { + "pid": 202, + "port": 5558, + "backend_module_name": "leann_backend_hnsw.hnsw_embedding_server", + "config_signature": { + "passages_file": "/tmp/other/doc.meta.json", + "model_name": "m2", + }, + }, + ], + ) + + args = argparse.Namespace(daemon_command="status", index_name="demo") + asyncio.run(cli.daemon_command(args)) + out = capsys.readouterr().out + assert "Active embedding daemons: 1" in out + assert "pid=101" in out + assert "pid=202" not in out + + +def test_daemon_stop_by_index_calls_stop_daemons(monkeypatch): + cli = LeannCLI() + monkeypatch.setattr( + cli, + "_resolve_index_path", + lambda *args, **kwargs: "/tmp/demo/documents.leann", + ) + captured: dict[str, dict[str, Any]] = {"kwargs": {}} + + def fake_stop_daemons(**kwargs): + captured["kwargs"] = kwargs + return 1 + + monkeypatch.setattr("leann.cli.EmbeddingServerManager.stop_daemons", fake_stop_daemons) + + args = argparse.Namespace(daemon_command="stop", index_name="demo", all=False) + asyncio.run(cli.daemon_command(args)) + + assert captured["kwargs"]["passages_file"].endswith("documents.leann.meta.json") + + +def test_daemon_start_calls_searcher_warmup(monkeypatch): + cli = LeannCLI() + monkeypatch.setattr( + cli, + "_resolve_index_path", + lambda *args, **kwargs: "/tmp/demo/documents.leann", + ) + + state: dict[str, Any] = {"warmup_called": 0, "init_kwargs": {}} + + class DummySearcher: + def __init__(self, *args, **kwargs): + state["init_kwargs"] = kwargs + + def warmup(self): + state["warmup_called"] += 1 + + monkeypatch.setattr("leann.cli.LeannSearcher", DummySearcher) + + args = argparse.Namespace( + daemon_command="start", + index_name="demo", + daemon_ttl=88, + enable_warmup=True, + ) + asyncio.run(cli.daemon_command(args)) + + assert state["warmup_called"] == 1 + assert state["init_kwargs"]["use_daemon"] is True + assert state["init_kwargs"]["daemon_ttl_seconds"] == 88 + + +def test_daemon_status_all_lists_records(monkeypatch, capsys): + cli = LeannCLI() + monkeypatch.setattr( + "leann.cli.EmbeddingServerManager.list_daemons", + lambda: [ + { + "pid": 301, + "port": 6001, + "backend_module_name": "leann_backend_hnsw.hnsw_embedding_server", + "config_signature": {"model_name": "m-a"}, + }, + { + "pid": 302, + "port": 6002, + "backend_module_name": "leann_backend_diskann.diskann_embedding_server", + "config_signature": {"model_name": "m-b"}, + }, + ], + ) + args = argparse.Namespace(daemon_command="status", index_name=None) + asyncio.run(cli.daemon_command(args)) + out = capsys.readouterr().out + assert "Active embedding daemons: 2" in out + assert "pid=301" in out + assert "pid=302" in out + + +def test_daemon_stop_all_calls_manager(monkeypatch): + cli = LeannCLI() + captured = {"called": False} + + def fake_stop_daemons(**kwargs): + captured["called"] = True + assert kwargs == {} + return 2 + + monkeypatch.setattr("leann.cli.EmbeddingServerManager.stop_daemons", fake_stop_daemons) + args = argparse.Namespace(daemon_command="stop", index_name=None, all=True) + asyncio.run(cli.daemon_command(args)) + assert captured["called"] is True diff --git a/tests/test_cli_prompt_template.py b/tests/test_cli_prompt_template.py new file mode 100644 index 0000000..774e29f --- /dev/null +++ b/tests/test_cli_prompt_template.py @@ -0,0 +1,533 @@ +""" +Tests for CLI argument integration of --embedding-prompt-template. + +These tests verify that: +1. The --embedding-prompt-template flag is properly registered on build and search commands +2. The template value flows from CLI args to embedding_options dict +3. The template is passed through to compute_embeddings() function +4. Default behavior (no flag) is handled correctly +""" + +from unittest.mock import Mock, patch + +from leann.cli import LeannCLI + + +class TestCLIPromptTemplateArgument: + """Tests for --embedding-prompt-template on build and search commands.""" + + def test_commands_accept_prompt_template_argument(self): + """Verify that build and search parsers accept --embedding-prompt-template flag.""" + cli = LeannCLI() + parser = cli.create_parser() + template_value = "search_query: " + + # Test build command + build_args = parser.parse_args( + [ + "build", + "test-index", + "--docs", + "/tmp/test-docs", + "--embedding-prompt-template", + template_value, + ] + ) + assert build_args.command == "build" + assert hasattr(build_args, "embedding_prompt_template"), ( + "build command should have embedding_prompt_template attribute" + ) + assert build_args.embedding_prompt_template == template_value + + # Test search command + search_args = parser.parse_args( + ["search", "test-index", "my query", "--embedding-prompt-template", template_value] + ) + assert search_args.command == "search" + assert hasattr(search_args, "embedding_prompt_template"), ( + "search command should have embedding_prompt_template attribute" + ) + assert search_args.embedding_prompt_template == template_value + + def test_commands_default_to_none(self): + """Verify default value is None when flag not provided (backward compatibility).""" + cli = LeannCLI() + parser = cli.create_parser() + + # Test build command default + build_args = parser.parse_args(["build", "test-index", "--docs", "/tmp/test-docs"]) + assert hasattr(build_args, "embedding_prompt_template"), ( + "build command should have embedding_prompt_template attribute" + ) + assert build_args.embedding_prompt_template is None, ( + "Build default value should be None when flag not provided" + ) + + # Test search command default + search_args = parser.parse_args(["search", "test-index", "my query"]) + assert hasattr(search_args, "embedding_prompt_template"), ( + "search command should have embedding_prompt_template attribute" + ) + assert search_args.embedding_prompt_template is None, ( + "Search default value should be None when flag not provided" + ) + + +class TestBuildCommandPromptTemplateArgumentExtras: + """Additional build-specific tests for prompt template argument.""" + + def test_build_command_prompt_template_with_multiword_value(self): + """ + Verify that template values with spaces are handled correctly. + + Templates like "search_document: " or "Represent this sentence for searching: " + should be accepted as a single string argument. + """ + cli = LeannCLI() + parser = cli.create_parser() + + template = "Represent this sentence for searching: " + args = parser.parse_args( + [ + "build", + "test-index", + "--docs", + "/tmp/test-docs", + "--embedding-prompt-template", + template, + ] + ) + + assert args.embedding_prompt_template == template + + +class TestPromptTemplateStoredInEmbeddingOptions: + """Tests for template storage in embedding_options dict.""" + + @patch("leann.cli.LeannBuilder") + def test_prompt_template_stored_in_embedding_options_on_build( + self, mock_builder_class, tmp_path + ): + """ + Verify that when --embedding-prompt-template is provided to build command, + the value is stored in embedding_options dict passed to LeannBuilder. + + This test will fail because the CLI doesn't currently process this argument + and add it to embedding_options. + """ + # Setup mocks + mock_builder = Mock() + mock_builder_class.return_value = mock_builder + + # Create CLI and run build command + cli = LeannCLI() + + # Mock load_documents to return a document so builder is created + cli.load_documents = Mock(return_value=[{"text": "test content", "metadata": {}}]) # type: ignore[assignment] + + parser = cli.create_parser() + + template = "search_query: " + args = parser.parse_args( + [ + "build", + "test-index", + "--docs", + str(tmp_path), + "--embedding-prompt-template", + template, + "--force", # Force rebuild to ensure LeannBuilder is called + ] + ) + + # Run the build command + import asyncio + + asyncio.run(cli.build_index(args)) + + # Check that LeannBuilder was called with embedding_options containing prompt_template + call_kwargs = mock_builder_class.call_args.kwargs + assert "embedding_options" in call_kwargs, "LeannBuilder should receive embedding_options" + + embedding_options = call_kwargs["embedding_options"] + assert embedding_options is not None, ( + "embedding_options should not be None when template provided" + ) + assert "prompt_template" in embedding_options, ( + "embedding_options should contain 'prompt_template' key" + ) + assert embedding_options["prompt_template"] == template, ( + f"Template should be '{template}', got {embedding_options.get('prompt_template')}" + ) + + @patch("leann.cli.LeannBuilder") + def test_prompt_template_not_in_options_when_not_provided(self, mock_builder_class, tmp_path): + """ + Verify that when --embedding-prompt-template is NOT provided, + embedding_options either doesn't have the key or it's None. + + This ensures we don't pass empty/None values unnecessarily. + """ + # Setup mocks + mock_builder = Mock() + mock_builder_class.return_value = mock_builder + + cli = LeannCLI() + + # Mock load_documents to return a document so builder is created + cli.load_documents = Mock(return_value=[{"text": "test content", "metadata": {}}]) # type: ignore[assignment] + + parser = cli.create_parser() + + args = parser.parse_args( + [ + "build", + "test-index", + "--docs", + str(tmp_path), + "--force", # Force rebuild to ensure LeannBuilder is called + ] + ) + + import asyncio + + asyncio.run(cli.build_index(args)) + + # Check that if embedding_options is passed, it doesn't have prompt_template + call_kwargs = mock_builder_class.call_args.kwargs + if call_kwargs.get("embedding_options"): + embedding_options = call_kwargs["embedding_options"] + # Either the key shouldn't exist, or it should be None + assert ( + "prompt_template" not in embedding_options + or embedding_options["prompt_template"] is None + ), "prompt_template should not be set when flag not provided" + + # R1 Tests: Build-time separate template storage + @patch("leann.cli.LeannBuilder") + def test_build_stores_separate_templates(self, mock_builder_class, tmp_path): + """ + R1 Test 1: Verify that when both --embedding-prompt-template and + --query-prompt-template are provided to build command, both values + are stored separately in embedding_options dict as build_prompt_template + and query_prompt_template. + + This test will fail because: + 1. CLI doesn't accept --query-prompt-template flag yet + 2. CLI doesn't store templates as separate build_prompt_template and + query_prompt_template keys + + Expected behavior after implementation: + - .meta.json contains: {"embedding_options": { + "build_prompt_template": "doc: ", + "query_prompt_template": "query: " + }} + """ + # Setup mocks + mock_builder = Mock() + mock_builder_class.return_value = mock_builder + + cli = LeannCLI() + + # Mock load_documents to return a document so builder is created + cli.load_documents = Mock(return_value=[{"text": "test content", "metadata": {}}]) # type: ignore[assignment] + + parser = cli.create_parser() + + build_template = "doc: " + query_template = "query: " + args = parser.parse_args( + [ + "build", + "test-index", + "--docs", + str(tmp_path), + "--embedding-prompt-template", + build_template, + "--query-prompt-template", + query_template, + "--force", + ] + ) + + # Run the build command + import asyncio + + asyncio.run(cli.build_index(args)) + + # Check that LeannBuilder was called with separate template keys + call_kwargs = mock_builder_class.call_args.kwargs + assert "embedding_options" in call_kwargs, "LeannBuilder should receive embedding_options" + + embedding_options = call_kwargs["embedding_options"] + assert embedding_options is not None, ( + "embedding_options should not be None when templates provided" + ) + + assert "build_prompt_template" in embedding_options, ( + "embedding_options should contain 'build_prompt_template' key" + ) + assert embedding_options["build_prompt_template"] == build_template, ( + f"build_prompt_template should be '{build_template}'" + ) + + assert "query_prompt_template" in embedding_options, ( + "embedding_options should contain 'query_prompt_template' key" + ) + assert embedding_options["query_prompt_template"] == query_template, ( + f"query_prompt_template should be '{query_template}'" + ) + + # Old key should NOT be present when using new separate template format + assert "prompt_template" not in embedding_options, ( + "Old 'prompt_template' key should not be present with separate templates" + ) + + @patch("leann.cli.LeannBuilder") + def test_build_backward_compat_single_template(self, mock_builder_class, tmp_path): + """ + R1 Test 2: Verify backward compatibility - when only + --embedding-prompt-template is provided (old behavior), it should + still be stored as 'prompt_template' in embedding_options. + + This ensures existing workflows continue to work unchanged. + + This test currently passes because it matches existing behavior, but it + documents the requirement that this behavior must be preserved after + implementing the separate template feature. + + Expected behavior: + - .meta.json contains: {"embedding_options": {"prompt_template": "prompt: "}} + - No build_prompt_template or query_prompt_template keys + """ + # Setup mocks + mock_builder = Mock() + mock_builder_class.return_value = mock_builder + + cli = LeannCLI() + + # Mock load_documents to return a document so builder is created + cli.load_documents = Mock(return_value=[{"text": "test content", "metadata": {}}]) # type: ignore[assignment] + + parser = cli.create_parser() + + template = "prompt: " + args = parser.parse_args( + [ + "build", + "test-index", + "--docs", + str(tmp_path), + "--embedding-prompt-template", + template, + "--force", + ] + ) + + # Run the build command + import asyncio + + asyncio.run(cli.build_index(args)) + + # Check that LeannBuilder was called with old format + call_kwargs = mock_builder_class.call_args.kwargs + assert "embedding_options" in call_kwargs, "LeannBuilder should receive embedding_options" + + embedding_options = call_kwargs["embedding_options"] + assert embedding_options is not None, ( + "embedding_options should not be None when template provided" + ) + + assert "prompt_template" in embedding_options, ( + "embedding_options should contain old 'prompt_template' key for backward compat" + ) + assert embedding_options["prompt_template"] == template, ( + f"prompt_template should be '{template}'" + ) + + # New keys should NOT be present in backward compat mode + assert "build_prompt_template" not in embedding_options, ( + "build_prompt_template should not be present with single template flag" + ) + assert "query_prompt_template" not in embedding_options, ( + "query_prompt_template should not be present with single template flag" + ) + + @patch("leann.cli.LeannBuilder") + def test_build_no_templates(self, mock_builder_class, tmp_path): + """ + R1 Test 3: Verify that when no template flags are provided, + embedding_options has no prompt template keys. + + This ensures clean defaults and no unnecessary keys in .meta.json. + + This test currently passes because it matches existing behavior, but it + documents the requirement that this behavior must be preserved after + implementing the separate template feature. + + Expected behavior: + - .meta.json has no prompt_template, build_prompt_template, or + query_prompt_template keys (or embedding_options is empty/None) + """ + # Setup mocks + mock_builder = Mock() + mock_builder_class.return_value = mock_builder + + cli = LeannCLI() + + # Mock load_documents to return a document so builder is created + cli.load_documents = Mock(return_value=[{"text": "test content", "metadata": {}}]) # type: ignore[assignment] + + parser = cli.create_parser() + + args = parser.parse_args(["build", "test-index", "--docs", str(tmp_path), "--force"]) + + # Run the build command + import asyncio + + asyncio.run(cli.build_index(args)) + + # Check that no template keys are present + call_kwargs = mock_builder_class.call_args.kwargs + if call_kwargs.get("embedding_options"): + embedding_options = call_kwargs["embedding_options"] + + # None of the template keys should be present + assert "prompt_template" not in embedding_options, ( + "prompt_template should not be present when no flags provided" + ) + assert "build_prompt_template" not in embedding_options, ( + "build_prompt_template should not be present when no flags provided" + ) + assert "query_prompt_template" not in embedding_options, ( + "query_prompt_template should not be present when no flags provided" + ) + + +class TestPromptTemplateFlowsToComputeEmbeddings: + """Tests for template flowing through to compute_embeddings function.""" + + @patch("leann.api.compute_embeddings") + def test_prompt_template_flows_to_compute_embeddings_via_provider_options( + self, mock_compute_embeddings, tmp_path + ): + """ + Verify that the prompt template flows from CLI args through LeannBuilder + to compute_embeddings() function via provider_options parameter. + + This is an integration test that verifies the complete flow: + CLI β†’ embedding_options β†’ LeannBuilder β†’ compute_embeddings(provider_options) + + This test will fail because: + 1. CLI doesn't capture the argument yet + 2. embedding_options doesn't include prompt_template + 3. LeannBuilder doesn't pass it through to compute_embeddings + """ + # Mock compute_embeddings to return dummy embeddings as numpy array + import numpy as np + + mock_compute_embeddings.return_value = np.array([[0.1, 0.2, 0.3]], dtype=np.float32) + + # Use real LeannBuilder (not mocked) to test the actual flow + cli = LeannCLI() + + # Mock load_documents to return a simple document + cli.load_documents = Mock(return_value=[{"text": "test content", "metadata": {}}]) # type: ignore[assignment] + + parser = cli.create_parser() + + template = "search_document: " + args = parser.parse_args( + [ + "build", + "test-index", + "--docs", + str(tmp_path), + "--embedding-prompt-template", + template, + "--backend-name", + "hnsw", # Use hnsw backend + "--force", # Force rebuild to ensure index is created + ] + ) + + # This should fail because the flow isn't implemented yet + import asyncio + + asyncio.run(cli.build_index(args)) + + # Verify compute_embeddings was called with provider_options containing prompt_template + assert mock_compute_embeddings.called, "compute_embeddings should have been called" + + # Check the call arguments + call_kwargs = mock_compute_embeddings.call_args.kwargs + assert "provider_options" in call_kwargs, ( + "compute_embeddings should receive provider_options parameter" + ) + + provider_options = call_kwargs["provider_options"] + assert provider_options is not None, "provider_options should not be None" + assert "prompt_template" in provider_options, ( + "provider_options should contain prompt_template key" + ) + assert provider_options["prompt_template"] == template, ( + f"Template should be '{template}', got {provider_options.get('prompt_template')}" + ) + + +class TestPromptTemplateArgumentHelp: + """Tests for argument help text and documentation.""" + + def test_build_command_prompt_template_has_help_text(self): + """ + Verify that --embedding-prompt-template has descriptive help text. + + Good help text is crucial for CLI usability. + """ + cli = LeannCLI() + parser = cli.create_parser() + + # Get the build subparser + # This is a bit tricky - we need to parse to get the help + # We'll check that the help includes relevant keywords + import io + from contextlib import redirect_stdout + + f = io.StringIO() + try: + with redirect_stdout(f): + parser.parse_args(["build", "--help"]) + except SystemExit: + pass # --help causes sys.exit(0) + + help_text = f.getvalue() + assert "--embedding-prompt-template" in help_text, ( + "Help text should mention --embedding-prompt-template" + ) + # Check for keywords that should be in the help + help_lower = help_text.lower() + assert any(keyword in help_lower for keyword in ["template", "prompt", "prepend"]), ( + "Help text should explain what the prompt template does" + ) + + def test_search_command_prompt_template_has_help_text(self): + """ + Verify that search command also has help text for --embedding-prompt-template. + """ + cli = LeannCLI() + parser = cli.create_parser() + + import io + from contextlib import redirect_stdout + + f = io.StringIO() + try: + with redirect_stdout(f): + parser.parse_args(["search", "--help"]) + except SystemExit: + pass # --help causes sys.exit(0) + + help_text = f.getvalue() + assert "--embedding-prompt-template" in help_text, ( + "Search help text should mention --embedding-prompt-template" + ) diff --git a/tests/test_cli_verbosity.py b/tests/test_cli_verbosity.py new file mode 100644 index 0000000..37024a8 --- /dev/null +++ b/tests/test_cli_verbosity.py @@ -0,0 +1,149 @@ +"""Tests for CLI verbosity options. + +This module tests the configurable verbosity functionality that allows +suppressing C++ output from FAISS/HNSW. +See: https://github.com/yichuan-w/LEANN/issues/187 +""" + +import os + +import pytest + + +class TestSuppressCppOutput: + """Test the suppress_cpp_output context manager.""" + + def test_suppress_cpp_output_captures_stdout(self): + """C output to stdout should be suppressed when enabled.""" + from leann.cli import suppress_cpp_output + + with suppress_cpp_output(suppress=True): + # This goes to fd 1, but is redirected to devnull + os.write(1, b"This should be suppressed\n") + + # If we got here without error, suppression worked + # The text was written to devnull + + def test_suppress_cpp_output_captures_stderr(self): + """C output to stderr should be suppressed when enabled.""" + from leann.cli import suppress_cpp_output + + with suppress_cpp_output(suppress=True): + # This goes to fd 2, but is redirected to devnull + os.write(2, b"This error should be suppressed\n") + + def test_suppress_cpp_output_restores_fds(self): + """File descriptors should be restored after context.""" + from leann.cli import suppress_cpp_output + + # Save original fds + original_stdout = os.dup(1) + original_stderr = os.dup(2) + + try: + with suppress_cpp_output(suppress=True): + pass + + # Verify fds are still valid and point to original destinations + # by checking we can write to them + os.write(1, b"") # Should not raise + os.write(2, b"") # Should not raise + finally: + os.close(original_stdout) + os.close(original_stderr) + + def test_suppress_cpp_output_disabled(self): + """When suppress=False, output should not be suppressed.""" + from leann.cli import suppress_cpp_output + + with suppress_cpp_output(suppress=False): + # This should work normally + os.write(1, b"") + os.write(2, b"") + + +class TestCliVerbosityArgs: + """Test CLI argument parsing for verbosity options.""" + + def test_verbose_flag_parsed(self): + """--verbose flag should be parsed correctly.""" + from leann.cli import LeannCLI + + cli = LeannCLI() + parser = cli.create_parser() + + args = parser.parse_args(["--verbose", "list"]) + assert args.verbose is True + assert args.quiet is False + + def test_quiet_flag_parsed(self): + """-q flag should be parsed correctly.""" + from leann.cli import LeannCLI + + cli = LeannCLI() + parser = cli.create_parser() + + args = parser.parse_args(["-q", "list"]) + assert args.quiet is True + assert args.verbose is False + + def test_verbose_short_flag(self): + """-v should work as shorthand for --verbose.""" + from leann.cli import LeannCLI + + cli = LeannCLI() + parser = cli.create_parser() + + args = parser.parse_args(["-v", "list"]) + assert args.verbose is True + + def test_verbose_and_quiet_mutually_exclusive(self): + """--verbose and --quiet should be mutually exclusive.""" + from leann.cli import LeannCLI + + cli = LeannCLI() + parser = cli.create_parser() + + with pytest.raises(SystemExit): + parser.parse_args(["--verbose", "--quiet", "list"]) + + def test_default_is_quiet(self): + """Default behavior should be quiet (suppress C++ output).""" + from leann.cli import LeannCLI + + cli = LeannCLI() + parser = cli.create_parser() + + args = parser.parse_args(["list"]) + assert args.verbose is False + assert args.quiet is False + # When both are False, we suppress by default + + +class TestVerbosityIntegration: + """Integration tests for verbosity in commands.""" + + def test_list_command_does_not_suppress(self): + """List command should work without suppression.""" + import asyncio + + from leann.cli import LeannCLI + + cli = LeannCLI() + parser = cli.create_parser() + + # List command should not raise + args = parser.parse_args(["list"]) + asyncio.run(cli.run(args)) + + def test_verbose_flag_with_list(self): + """Verbose flag should work with list command.""" + import asyncio + + from leann.cli import LeannCLI + + cli = LeannCLI() + parser = cli.create_parser() + + args = parser.parse_args(["-v", "list"]) + asyncio.run(cli.run(args)) diff --git a/tests/test_cpu_only_install.py b/tests/test_cpu_only_install.py new file mode 100644 index 0000000..0d5c2f4 --- /dev/null +++ b/tests/test_cpu_only_install.py @@ -0,0 +1,61 @@ +"""Packaging metadata checks for CPU-only installs.""" + +from pathlib import Path + +import pytest + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - fallback for Python < 3.11 + tomllib = pytest.importorskip("tomli") + + +def _load_leann_pyproject(): + pyproject_path = Path(__file__).resolve().parents[1] / "packages" / "leann" / "pyproject.toml" + return tomllib.loads(pyproject_path.read_text()) + + +def _load_leann_core_pyproject(): + pyproject_path = ( + Path(__file__).resolve().parents[1] / "packages" / "leann-core" / "pyproject.toml" + ) + return tomllib.loads(pyproject_path.read_text()) + + +def test_leann_base_dependencies_include_diskann(): + data = _load_leann_pyproject() + deps = data["project"].get("dependencies", []) + + assert "leann-core>=0.1.0" in deps + assert "leann-backend-hnsw>=0.1.0" in deps + assert "leann-backend-diskann>=0.1.0" in deps + + +def test_leann_core_numpy_is_bounded_below_3(): + data = _load_leann_core_pyproject() + deps = data["project"].get("dependencies", []) + + assert any(dep.startswith("numpy") and ">=1.20.0" in dep and "<3" in dep for dep in deps) + + +def test_leann_core_cpu_extra_pins_cpu_torch(): + data = _load_leann_core_pyproject() + extras = data["project"].get("optional-dependencies", {}) + + cpu_deps = extras.get("cpu", []) + assert cpu_deps, "cpu extra should be defined" + assert any( + dep.startswith("torch") + and "==2.2.2" in dep + and "platform_system == 'Linux'" in dep + and "python_version < '3.13'" in dep + for dep in cpu_deps + ) + + +def test_leann_cpu_extra_defined(): + data = _load_leann_pyproject() + extras = data["project"].get("optional-dependencies", {}) + + assert "cpu" in extras + assert "leann-core[cpu]>=0.1.0" in extras["cpu"] diff --git a/tests/test_diskann_partition.py b/tests/test_diskann_partition.py new file mode 100644 index 0000000..e306f21 --- /dev/null +++ b/tests/test_diskann_partition.py @@ -0,0 +1,336 @@ +""" +Test DiskANN graph partitioning functionality. + +Tests the automatic graph partitioning feature that was implemented to save +storage space by partitioning large DiskANN indices and safely deleting +redundant files while maintaining search functionality. +""" + +import os +import tempfile +from pathlib import Path + +import pytest + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip DiskANN partition tests in CI - requires specific hardware and large memory", +) +def test_diskann_without_partition(): + """Test DiskANN index building without partition (baseline).""" + from leann.api import LeannBuilder, LeannSearcher + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "test_no_partition.leann") + + texts = [ + f"Document {i} discusses topic {i % 10} with detailed analysis of subject {i // 10}." + for i in range(500) + ] + + builder = LeannBuilder( + backend_name="diskann", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + num_neighbors=32, + search_list_size=50, + is_recompute=False, + ) + + for text in texts: + builder.add_text(text) + + builder.build_index(index_path) + + index_dir = Path(index_path).parent + assert index_dir.exists() + + index_prefix = Path(index_path).stem + required_files = [ + f"{index_prefix}_disk.index", + f"{index_prefix}_pq_compressed.bin", + f"{index_prefix}_pq_pivots.bin", + ] + + generated_files = [f.name for f in index_dir.glob(f"{index_prefix}*")] + print(f"Generated files: {generated_files}") + + for required_file in required_files: + file_path = index_dir / required_file + assert file_path.exists(), f"Required file {required_file} not found" + + partition_files = [f"{index_prefix}_disk_graph.index", f"{index_prefix}_partition.bin"] + + for partition_file in partition_files: + file_path = index_dir / partition_file + assert not file_path.exists(), ( + f"Partition file {partition_file} should not exist in non-partition mode" + ) + + with LeannSearcher(index_path) as searcher: + results = searcher.search("topic 3 analysis", top_k=3) + + assert len(results) > 0 + assert all( + result.score is not None and result.score != float("-inf") for result in results + ) + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip DiskANN partition tests in CI - requires specific hardware and large memory", +) +def test_diskann_with_partition(): + """Test DiskANN index building with automatic graph partitioning.""" + from leann.api import LeannBuilder + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "test_with_partition.leann") + + texts = [ + f"Document {i} explores subject {i % 15} with comprehensive coverage of area {i // 15}." + for i in range(500) + ] + + builder = LeannBuilder( + backend_name="diskann", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + num_neighbors=32, + search_list_size=50, + is_recompute=True, + ) + + for text in texts: + builder.add_text(text) + + builder.build_index(index_path) + + index_dir = Path(index_path).parent + assert index_dir.exists() + + index_prefix = Path(index_path).stem + partition_files = [ + f"{index_prefix}_disk_graph.index", + f"{index_prefix}_partition.bin", + f"{index_prefix}_pq_compressed.bin", + f"{index_prefix}_pq_pivots.bin", + ] + + for partition_file in partition_files: + file_path = index_dir / partition_file + assert file_path.exists(), f"Expected partition file {partition_file} not found" + + large_files = [f"{index_prefix}_disk.index", f"{index_prefix}_disk_beam_search.index"] + + for large_file in large_files: + file_path = index_dir / large_file + assert not file_path.exists(), ( + f"Large file {large_file} should have been deleted for storage saving" + ) + + required_files = [ + f"{index_prefix}_disk.index_medoids.bin", + f"{index_prefix}_disk.index_max_base_norm.bin", + ] + + for req_file in required_files: + file_path = index_dir / req_file + assert file_path.exists(), ( + f"Required auxiliary file {req_file} missing for partition mode" + ) + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip DiskANN partition tests in CI - requires specific hardware and large memory", +) +def test_diskann_partition_search_functionality(): + """Test that search works correctly with partitioned indices.""" + from leann.api import LeannBuilder, LeannSearcher + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "test_partition_search.leann") + + texts = [ + "LEANN is a storage-efficient approximate nearest neighbor search system.", + "Graph partitioning helps reduce memory usage in large scale vector search.", + "DiskANN provides high-performance disk-based approximate nearest neighbor search.", + "Vector embeddings enable semantic search over unstructured text data.", + "Approximate nearest neighbor algorithms trade accuracy for speed and storage.", + ] * 100 + + builder = LeannBuilder( + backend_name="diskann", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + is_recompute=True, + ) + + for text in texts: + builder.add_text(text) + + builder.build_index(index_path) + + with LeannSearcher(index_path) as searcher: + test_queries = [ + ("vector search algorithms", 5), + ("LEANN storage efficiency", 3), + ("graph partitioning memory", 4), + ("approximate nearest neighbor", 7), + ] + + for query, top_k in test_queries: + results = searcher.search(query, top_k=top_k) + + assert len(results) == top_k, f"Expected {top_k} results for query '{query}'" + assert all(result.score is not None for result in results), ( + "All results should have scores" + ) + assert all(result.score != float("-inf") for result in results), ( + "No result should have -inf score" + ) + assert all(result.text is not None for result in results), ( + "All results should have text" + ) + + scores = [result.score for result in results] + assert scores == sorted(scores, reverse=True), ( + "Results should be sorted by score descending" + ) + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip DiskANN partition tests in CI - requires specific hardware and large memory", +) +def test_diskann_medoid_and_norm_files(): + """Test that medoid and max_base_norm files are correctly generated and used.""" + import struct + + from leann.api import LeannBuilder, LeannSearcher + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "test_medoid_norm.leann") + + texts = [f"Test document {i} with content about subject {i % 10}." for i in range(200)] + + builder = LeannBuilder( + backend_name="diskann", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + is_recompute=True, + ) + + for text in texts: + builder.add_text(text) + + builder.build_index(index_path) + + index_dir = Path(index_path).parent + index_prefix = Path(index_path).stem + + medoids_file = index_dir / f"{index_prefix}_disk.index_medoids.bin" + assert medoids_file.exists(), "Medoids file should be generated" + + with open(medoids_file, "rb") as f: + nshards = struct.unpack("= 0, "Medoid ID should be valid (not hardcoded 0)" + + norm_file = index_dir / f"{index_prefix}_disk.index_max_base_norm.bin" + assert norm_file.exists(), "Max base norm file should be generated" + + with open(norm_file, "rb") as f: + npts = struct.unpack(" 0, "Norm value should be positive" + assert norm_val != float("inf"), "Norm value should be finite" + + with LeannSearcher(index_path) as searcher: + results = searcher.search("test subject", top_k=3) + + assert len(results) > 0 + assert all(result.score != float("-inf") for result in results), ( + "Scores should not be -inf when norm file is correct" + ) + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip performance comparison in CI - requires significant compute time", +) +def test_diskann_vs_hnsw_performance(): + """Compare DiskANN (with partition) vs HNSW performance.""" + import time + + from leann.api import LeannBuilder, LeannSearcher + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + texts = [ + f"Performance test document {i} covering topic {i % 20} in detail." for i in range(1000) + ] + query = "performance topic test" + + diskann_path = str(Path(temp_dir) / "perf_diskann.leann") + diskann_builder = LeannBuilder( + backend_name="diskann", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + is_recompute=True, + ) + + for text in texts: + diskann_builder.add_text(text) + + start_time = time.time() + diskann_builder.build_index(diskann_path) + + hnsw_path = str(Path(temp_dir) / "perf_hnsw.leann") + hnsw_builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + is_recompute=True, + ) + + for text in texts: + hnsw_builder.add_text(text) + + start_time = time.time() + hnsw_builder.build_index(hnsw_path) + + with ( + LeannSearcher(diskann_path) as diskann_searcher, + LeannSearcher(hnsw_path) as hnsw_searcher, + ): + diskann_searcher.search(query, top_k=5) + hnsw_searcher.search(query, top_k=5) + + start_time = time.time() + diskann_results = diskann_searcher.search(query, top_k=10) + diskann_search_time = time.time() - start_time + + start_time = time.time() + hnsw_results = hnsw_searcher.search(query, top_k=10) + hnsw_search_time = time.time() - start_time + + assert len(diskann_results) == 10 + assert len(hnsw_results) == 10 + assert all(r.score != float("-inf") for r in diskann_results) + assert all(r.score != float("-inf") for r in hnsw_results) + + if hnsw_search_time > 0: + speed_ratio = hnsw_search_time / diskann_search_time + print(f"DiskANN search time: {diskann_search_time:.4f}s") + print(f"HNSW search time: {hnsw_search_time:.4f}s") + print(f"DiskANN is {speed_ratio:.2f}x faster than HNSW") diff --git a/tests/test_document_rag.py b/tests/test_document_rag.py new file mode 100644 index 0000000..e93cf21 --- /dev/null +++ b/tests/test_document_rag.py @@ -0,0 +1,168 @@ +""" +Test document_rag functionality using pytest. +""" + +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +import pytest + + +@pytest.fixture +def test_data_dir(): + """Return the path to test data directory.""" + return Path("data") + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip model tests in CI to avoid MPS memory issues" +) +def test_document_rag_simulated(test_data_dir): + """Test document_rag with simulated LLM.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + # Use a subdirectory that doesn't exist yet to force index creation + index_dir = Path(temp_dir) / "test_index" + cmd = [ + sys.executable, + "apps/document_rag.py", + "--llm", + "simulated", + "--embedding-model", + "facebook/contriever", + "--embedding-mode", + "sentence-transformers", + "--index-dir", + str(index_dir), + "--data-dir", + str(test_data_dir), + "--query", + "What is Pride and Prejudice about?", + ] + + env = os.environ.copy() + env["HF_HUB_DISABLE_SYMLINKS"] = "1" + env["TOKENIZERS_PARALLELISM"] = "false" + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600, env=env) + + # Check return code + assert result.returncode == 0, f"Command failed: {result.stderr}" + + # Verify output + output = result.stdout + result.stderr + assert "Index saved to" in output or "Using existing index" in output + assert "This is a simulated answer" in output + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip AST chunking tests in CI to avoid dependency issues", +) +def test_document_rag_with_ast_chunking(test_data_dir): + """Test document_rag with AST-aware chunking enabled.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + # Use a subdirectory that doesn't exist yet to force index creation + index_dir = Path(temp_dir) / "test_ast_index" + cmd = [ + sys.executable, + "apps/document_rag.py", + "--llm", + "simulated", + "--embedding-model", + "facebook/contriever", + "--embedding-mode", + "sentence-transformers", + "--index-dir", + str(index_dir), + "--data-dir", + str(test_data_dir), + "--enable-code-chunking", # Enable AST chunking + "--query", + "What is Pride and Prejudice about?", + ] + + env = os.environ.copy() + env["HF_HUB_DISABLE_SYMLINKS"] = "1" + env["TOKENIZERS_PARALLELISM"] = "false" + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600, env=env) + + # Check return code + assert result.returncode == 0, f"Command failed: {result.stderr}" + + # Verify output + output = result.stdout + result.stderr + assert "Index saved to" in output or "Using existing index" in output + assert "This is a simulated answer" in output + + # Should mention AST chunking if code files are present + # (might not be relevant for the test data, but command should succeed) + + +@pytest.mark.skipif(not os.environ.get("OPENAI_API_KEY"), reason="OpenAI API key not available") +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip OpenAI tests in CI to avoid API costs" +) +def test_document_rag_openai(test_data_dir): + """Test document_rag with OpenAI embeddings.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + # Use a subdirectory that doesn't exist yet to force index creation + index_dir = Path(temp_dir) / "test_index_openai" + cmd = [ + sys.executable, + "apps/document_rag.py", + "--llm", + "simulated", # Use simulated LLM to avoid GPT-4 costs + "--embedding-model", + "text-embedding-3-small", + "--embedding-mode", + "openai", + "--index-dir", + str(index_dir), + "--data-dir", + str(test_data_dir), + "--query", + "What is Pride and Prejudice about?", + ] + + env = os.environ.copy() + env["TOKENIZERS_PARALLELISM"] = "false" + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600, env=env) + + assert result.returncode == 0, f"Command failed: {result.stderr}" + + # Verify cosine distance was used + output = result.stdout + result.stderr + assert any( + msg in output + for msg in [ + "distance_metric='cosine'", + "Automatically setting distance_metric='cosine'", + "Using cosine distance", + ] + ) + + +def test_document_rag_error_handling(test_data_dir): + """Test document_rag with invalid parameters.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + cmd = [ + sys.executable, + "apps/document_rag.py", + "--llm", + "invalid_llm_type", + "--index-dir", + temp_dir, + "--data-dir", + str(test_data_dir), + ] + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + + # Should fail with invalid LLM type + assert result.returncode != 0 + assert "invalid choice" in result.stderr or "invalid_llm_type" in result.stderr diff --git a/tests/test_embedding_batch_size.py b/tests/test_embedding_batch_size.py new file mode 100644 index 0000000..207b75a --- /dev/null +++ b/tests/test_embedding_batch_size.py @@ -0,0 +1,69 @@ +"""Tests for embedding batch size and CPU thread configuration.""" + +from unittest.mock import patch + +from leann.embedding_compute import ( + _cap_cuda_batch_by_vram, + _parse_positive_int_env, + _resolve_adaptive_batch_size, + _resolve_cpu_thread_count, +) + + +def test_parse_positive_int_env_default(monkeypatch): + monkeypatch.delenv("LEANN_TEST_INT", raising=False) + assert _parse_positive_int_env("LEANN_TEST_INT", 256) == 256 + + +def test_parse_positive_int_env_override(monkeypatch): + monkeypatch.setenv("LEANN_TEST_INT", "32") + assert _parse_positive_int_env("LEANN_TEST_INT", 256) == 32 + + +def test_parse_positive_int_env_invalid(monkeypatch): + monkeypatch.setenv("LEANN_TEST_INT", "not-a-number") + assert _parse_positive_int_env("LEANN_TEST_INT", 256) == 256 + + +def test_resolve_adaptive_batch_size_cuda(monkeypatch): + monkeypatch.setenv("LEANN_CUDA_BATCH_SIZE", "64") + assert _resolve_adaptive_batch_size("cuda", "BAAI/bge-base-en-v1.5") == 64 + + +def test_resolve_adaptive_batch_size_mps_qwen(monkeypatch): + monkeypatch.delenv("LEANN_MPS_BATCH_SIZE", raising=False) + assert _resolve_adaptive_batch_size("mps", "Qwen/Qwen3-Embedding-0.6B") == 32 + + +def test_resolve_cpu_threads(monkeypatch): + monkeypatch.setenv("LEANN_CPU_THREADS", "16") + assert _resolve_cpu_thread_count() == 16 + + +def test_cap_cuda_batch_by_vram_disabled(monkeypatch): + monkeypatch.setenv("LEANN_CUDA_AUTO_BATCH", "0") + with patch("torch.cuda.is_available", return_value=True): + with patch("torch.cuda.mem_get_info", return_value=(100, 1000)): + assert _cap_cuda_batch_by_vram(256) == 256 + + +def test_cap_cuda_batch_by_vram_small_gpu(monkeypatch): + monkeypatch.delenv("LEANN_CUDA_AUTO_BATCH", raising=False) + # Typical free VRAM on a 4 GiB GPU after loading a base-sized encoder. + one_gb = 1024**3 + with patch("torch.cuda.is_available", return_value=True): + with patch("torch.cuda.mem_get_info", return_value=(one_gb, 4 * one_gb)): + capped = _cap_cuda_batch_by_vram(256, max_length=512) + assert capped < 256 + assert capped >= 1 + + +def test_cap_cuda_batch_by_vram_four_gb_gpu(monkeypatch): + """Regression: 4 GiB RTX A1000 reports ~3.2 GiB free; cap should land near 76.""" + monkeypatch.delenv("LEANN_CUDA_AUTO_BATCH", raising=False) + free_vram = int(3.2 * 1024**3) + with patch("torch.cuda.is_available", return_value=True): + with patch("torch.cuda.mem_get_info", return_value=(free_vram, 4 * 1024**3)): + capped = _cap_cuda_batch_by_vram(256, max_length=512) + assert capped <= 85 + assert capped >= 1 diff --git a/tests/test_embedding_prompt_template.py b/tests/test_embedding_prompt_template.py new file mode 100644 index 0000000..f8a56d9 --- /dev/null +++ b/tests/test_embedding_prompt_template.py @@ -0,0 +1,333 @@ +"""Unit tests for prompt template prepending in OpenAI embeddings. + +This test suite defines the contract for prompt template functionality that allows +users to prepend a consistent prompt to all embedding inputs. These tests verify: + +1. Template prepending to all input texts before embedding computation +2. Graceful handling of None/missing provider_options +3. Empty string template behavior (no-op) +4. Logging of template application for observability +5. Template application before token truncation + +All tests are written in Red Phase - they should FAIL initially because the +implementation does not exist yet. +""" + +from unittest.mock import MagicMock, Mock, patch + +import numpy as np +import pytest +from leann.embedding_compute import compute_embeddings_openai + + +class TestPromptTemplatePrepending: + """Tests for prompt template prepending in compute_embeddings_openai.""" + + @pytest.fixture + def mock_openai_client(self): + """Create mock OpenAI client that captures input texts.""" + mock_client = MagicMock() + + # Mock the embeddings.create response + mock_response = Mock() + mock_response.data = [ + Mock(embedding=[0.1, 0.2, 0.3]), + Mock(embedding=[0.4, 0.5, 0.6]), + ] + mock_client.embeddings.create.return_value = mock_response + + return mock_client + + @pytest.fixture + def mock_openai_module(self, mock_openai_client, monkeypatch): + """Mock the openai module to return our mock client.""" + # Mock the API key environment variable + monkeypatch.setenv("OPENAI_API_KEY", "fake-test-key-for-mocking") + + # openai is imported inside the function, so we need to patch it there + with patch("openai.OpenAI", return_value=mock_openai_client) as mock_openai: + yield mock_openai + + def test_prompt_template_prepended_to_all_texts(self, mock_openai_module, mock_openai_client): + """Verify template is prepended to all input texts. + + When provider_options contains "prompt_template", that template should + be prepended to every text in the input list before sending to OpenAI API. + + This is the core functionality: the template acts as a consistent prefix + that provides context or instruction for the embedding model. + """ + texts = ["First document", "Second document"] + template = "search_document: " + provider_options = {"prompt_template": template} + + # Call compute_embeddings_openai with provider_options + result = compute_embeddings_openai( + texts=texts, + model_name="text-embedding-3-small", + provider_options=provider_options, + ) + + # Verify embeddings.create was called with templated texts + mock_openai_client.embeddings.create.assert_called_once() + call_args = mock_openai_client.embeddings.create.call_args + + # Extract the input texts sent to API + sent_texts = call_args.kwargs["input"] + + # Verify template was prepended to all texts + assert len(sent_texts) == 2, "Should send same number of texts" + assert sent_texts[0] == "search_document: First document", ( + "Template should be prepended to first text" + ) + assert sent_texts[1] == "search_document: Second document", ( + "Template should be prepended to second text" + ) + + # Verify result is valid embeddings array + assert isinstance(result, np.ndarray) + assert result.shape == (2, 3), "Should return correct shape" + + def test_template_not_applied_when_missing_or_empty( + self, mock_openai_module, mock_openai_client + ): + """Verify template not applied when provider_options is None, missing key, or empty string. + + This consolidated test covers three scenarios where templates should NOT be applied: + 1. provider_options is None (default behavior) + 2. provider_options exists but missing 'prompt_template' key + 3. prompt_template is explicitly set to empty string "" + + In all cases, texts should be sent to the API unchanged. + """ + # Scenario 1: None provider_options + texts = ["Original text one", "Original text two"] + result = compute_embeddings_openai( + texts=texts, + model_name="text-embedding-3-small", + provider_options=None, + ) + call_args = mock_openai_client.embeddings.create.call_args + sent_texts = call_args.kwargs["input"] + assert sent_texts[0] == "Original text one", ( + "Text should be unchanged with None provider_options" + ) + assert sent_texts[1] == "Original text two" + assert isinstance(result, np.ndarray) + assert result.shape == (2, 3) + + # Reset mock for next scenario + mock_openai_client.reset_mock() + mock_response = Mock() + mock_response.data = [ + Mock(embedding=[0.1, 0.2, 0.3]), + Mock(embedding=[0.4, 0.5, 0.6]), + ] + mock_openai_client.embeddings.create.return_value = mock_response + + # Scenario 2: Missing 'prompt_template' key + texts = ["Text without template", "Another text"] + provider_options = {"base_url": "https://api.openai.com/v1"} + result = compute_embeddings_openai( + texts=texts, + model_name="text-embedding-3-small", + provider_options=provider_options, + ) + call_args = mock_openai_client.embeddings.create.call_args + sent_texts = call_args.kwargs["input"] + assert sent_texts[0] == "Text without template", "Text should be unchanged with missing key" + assert sent_texts[1] == "Another text" + assert isinstance(result, np.ndarray) + + # Reset mock for next scenario + mock_openai_client.reset_mock() + mock_openai_client.embeddings.create.return_value = mock_response + + # Scenario 3: Empty string template + texts = ["Text one", "Text two"] + provider_options = {"prompt_template": ""} + result = compute_embeddings_openai( + texts=texts, + model_name="text-embedding-3-small", + provider_options=provider_options, + ) + call_args = mock_openai_client.embeddings.create.call_args + sent_texts = call_args.kwargs["input"] + assert sent_texts[0] == "Text one", "Empty template should not modify text" + assert sent_texts[1] == "Text two" + assert isinstance(result, np.ndarray) + + def test_prompt_template_with_multiple_batches(self, mock_openai_module, mock_openai_client): + """Verify template is prepended in all batches when texts exceed batch size. + + OpenAI API has batch size limits. When input texts are split into + multiple batches, the template should be prepended to texts in every batch. + + This ensures consistency across all API calls. + """ + # Create many texts that will be split into multiple batches + texts = [f"Document {i}" for i in range(1000)] + template = "passage: " + provider_options = {"prompt_template": template} + + # Mock multiple batch responses + mock_response = Mock() + mock_response.data = [Mock(embedding=[0.1, 0.2, 0.3]) for _ in range(1000)] + mock_openai_client.embeddings.create.return_value = mock_response + + result = compute_embeddings_openai( + texts=texts, + model_name="text-embedding-3-small", + provider_options=provider_options, + ) + + # Verify embeddings.create was called multiple times (batching) + assert mock_openai_client.embeddings.create.call_count >= 2, ( + "Should make multiple API calls for large text list" + ) + + # Verify template was prepended in ALL batches + for call in mock_openai_client.embeddings.create.call_args_list: + sent_texts = call.kwargs["input"] + for text in sent_texts: + assert text.startswith(template), ( + f"All texts in all batches should start with template. Got: {text}" + ) + + # Verify result shape + assert result.shape[0] == 1000, "Should return embeddings for all texts" + + def test_gemini_openai_compat_caps_batch_size_to_100( + self, mock_openai_module, mock_openai_client + ): + texts = [f"Doc {i}" for i in range(250)] + provider_options = {"base_url": "https://generativelanguage.googleapis.com/v1beta/openai"} + + mock_response = Mock() + mock_response.data = [Mock(embedding=[0.1, 0.2, 0.3]) for _ in range(250)] + mock_openai_client.embeddings.create.return_value = mock_response + + result = compute_embeddings_openai( + texts=texts, + model_name="gemini-embedding-001", + provider_options=provider_options, + ) + + # Should chunk into <=100 inputs per request. + assert mock_openai_client.embeddings.create.call_count == 3 + batch_sizes = [ + len(call.kwargs["input"]) + for call in mock_openai_client.embeddings.create.call_args_list + ] + assert batch_sizes == [100, 100, 50] + assert result.shape[0] == 250 + + def test_dashscope_openai_compat_caps_batch_size_to_10( + self, mock_openai_module, mock_openai_client + ): + texts = [f"Doc {i}" for i in range(25)] + provider_options = { + "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", + } + + mock_response = Mock() + mock_response.data = [Mock(embedding=[0.1, 0.2, 0.3]) for _ in range(25)] + mock_openai_client.embeddings.create.return_value = mock_response + + result = compute_embeddings_openai( + texts=texts, + model_name="text-embedding-v4", + provider_options=provider_options, + ) + + # Should chunk into <=10 inputs per request (DashScope hard limit). + assert mock_openai_client.embeddings.create.call_count == 3 + batch_sizes = [ + len(call.kwargs["input"]) + for call in mock_openai_client.embeddings.create.call_args_list + ] + assert batch_sizes == [10, 10, 5] + assert result.shape[0] == 25 + + def test_prompt_template_with_special_characters(self, mock_openai_module, mock_openai_client): + """Verify template with special characters is handled correctly. + + Templates may contain special characters, Unicode, newlines, etc. + These should all be prepended correctly without encoding issues. + """ + texts = ["Document content"] + # Template with various special characters + template = "πŸ” Search query [EN]: " + provider_options = {"prompt_template": template} + + result = compute_embeddings_openai( + texts=texts, + model_name="text-embedding-3-small", + provider_options=provider_options, + ) + + # Verify special characters in template were preserved + call_args = mock_openai_client.embeddings.create.call_args + sent_texts = call_args.kwargs["input"] + + assert sent_texts[0] == "πŸ” Search query [EN]: Document content", ( + "Special characters in template should be preserved" + ) + + assert isinstance(result, np.ndarray) + + def test_prompt_template_integration_with_existing_validation( + self, mock_openai_module, mock_openai_client + ): + """Verify template works with existing input validation. + + compute_embeddings_openai has validation for empty texts and whitespace. + Template prepending should happen AFTER validation, so validation errors + are thrown based on original texts, not templated texts. + + This ensures users get clear error messages about their input. + """ + # Empty text should still raise ValueError even with template + texts = [""] + provider_options = {"prompt_template": "prefix: "} + + with pytest.raises(ValueError, match="empty/invalid"): + compute_embeddings_openai( + texts=texts, + model_name="text-embedding-3-small", + provider_options=provider_options, + ) + + def test_prompt_template_with_api_key_and_base_url( + self, mock_openai_module, mock_openai_client + ): + """Verify template works alongside other provider_options. + + provider_options may contain multiple settings: prompt_template, + base_url, api_key. All should work together correctly. + """ + texts = ["Test document"] + provider_options = { + "prompt_template": "embed: ", + "base_url": "https://custom.api.com/v1", + "api_key": "test-key-123", + } + + result = compute_embeddings_openai( + texts=texts, + model_name="text-embedding-3-small", + provider_options=provider_options, + ) + + # Verify template was applied + call_args = mock_openai_client.embeddings.create.call_args + sent_texts = call_args.kwargs["input"] + assert sent_texts[0] == "embed: Test document" + + # Verify OpenAI client was created with correct base_url + mock_openai_module.assert_called() + client_init_kwargs = mock_openai_module.call_args.kwargs + assert client_init_kwargs["base_url"] == "https://custom.api.com/v1" + assert client_init_kwargs["api_key"] == "test-key-123" + + assert isinstance(result, np.ndarray) diff --git a/tests/test_embedding_server_cli_flags.py b/tests/test_embedding_server_cli_flags.py new file mode 100644 index 0000000..5cc8281 --- /dev/null +++ b/tests/test_embedding_server_cli_flags.py @@ -0,0 +1,39 @@ +import os +import subprocess +import sys +from pathlib import Path + + +def _run_help(module_name: str) -> str: + root = Path(__file__).resolve().parents[1] + extra_paths = [ + str(root / "packages" / "leann-core" / "src"), + str(root / "packages" / "leann-backend-hnsw"), + str(root / "packages" / "leann-backend-diskann"), + ] + env = os.environ.copy() + existing = env.get("PYTHONPATH", "") + env["PYTHONPATH"] = os.pathsep.join(extra_paths + ([existing] if existing else [])) + + proc = subprocess.run( + [sys.executable, "-m", module_name, "--help"], + check=True, + text=True, + capture_output=True, + env=env, + ) + return proc.stdout + + +def test_hnsw_server_help_has_daemon_and_warmup_flags(): + out = _run_help("leann_backend_hnsw.hnsw_embedding_server") + assert "--enable-warmup" in out + assert "--daemon-mode" in out + assert "--daemon-ttl" in out + + +def test_diskann_server_help_has_daemon_and_warmup_flags(): + out = _run_help("leann_backend_diskann.diskann_embedding_server") + assert "--enable-warmup" in out + assert "--daemon-mode" in out + assert "--daemon-ttl" in out diff --git a/tests/test_embedding_server_manager.py b/tests/test_embedding_server_manager.py new file mode 100644 index 0000000..d9b1986 --- /dev/null +++ b/tests/test_embedding_server_manager.py @@ -0,0 +1,648 @@ +import json +import os +import threading +import time +from typing import Any, cast + +import pytest +from leann.embedding_server_manager import EmbeddingServerManager + + +class DummyProcess: + def __init__(self, pid=12345): + self.pid = pid + self._terminated = False + + def poll(self): + return 0 if self._terminated else None + + def terminate(self): + self._terminated = True + + def kill(self): + self._terminated = True + + def wait(self, timeout=None): + self._terminated = True + return 0 + + +@pytest.fixture +def embedding_manager(monkeypatch): + manager = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + + def fake_get_available_port(start_port): + return start_port + + monkeypatch.setattr( + "leann.embedding_server_manager._get_available_port", + fake_get_available_port, + ) + + start_calls = [] + + def fake_start_new_server(self, port, model_name, embedding_mode, **kwargs): + config_signature = kwargs.get("config_signature") + start_calls.append(config_signature) + self.server_process = DummyProcess() + self.server_port = port + self._server_config = config_signature + return True, port + + monkeypatch.setattr( + EmbeddingServerManager, + "_start_new_server", + fake_start_new_server, + ) + + # Ensure stop_server doesn't try to operate on real subprocesses + def fake_stop_server(self): + self.server_process = None + self.server_port = None + self._server_config = None + + monkeypatch.setattr(EmbeddingServerManager, "stop_server", fake_stop_server) + + return manager, start_calls + + +def _write_meta(meta_path, passages_name, index_name, total): + meta_path.write_text( + json.dumps( + { + "backend_name": "hnsw", + "embedding_model": "test-model", + "embedding_mode": "sentence-transformers", + "dimensions": 3, + "backend_kwargs": {}, + "passage_sources": [ + { + "type": "jsonl", + "path": passages_name, + "index_path": index_name, + } + ], + "total_passages": total, + } + ), + encoding="utf-8", + ) + + +def test_server_restarts_when_metadata_changes(tmp_path, embedding_manager): + manager, start_calls = embedding_manager + + meta_path = tmp_path / "example.meta.json" + passages_path = tmp_path / "example.passages.jsonl" + index_path = tmp_path / "example.passages.idx" + + passages_path.write_text("first\n", encoding="utf-8") + index_path.write_bytes(b"index") + _write_meta(meta_path, passages_path.name, index_path.name, total=1) + + # Initial start populates signature + ok, port = manager.start_server( + port=6000, + model_name="test-model", + passages_file=str(meta_path), + use_daemon=False, + ) + assert ok + assert port == 6000 + assert len(start_calls) == 1 + + initial_signature = start_calls[0]["passages_signature"] + + # No metadata change => reuse existing server + ok, port_again = manager.start_server( + port=6000, + model_name="test-model", + passages_file=str(meta_path), + use_daemon=False, + ) + assert ok + assert port_again == 6000 + assert len(start_calls) == 1 + + # Modify passage data and metadata to force signature change + time.sleep(0.01) # Ensure filesystem timestamps move forward + passages_path.write_text("second\n", encoding="utf-8") + _write_meta(meta_path, passages_path.name, index_path.name, total=2) + + ok, port_third = manager.start_server( + port=6000, + model_name="test-model", + passages_file=str(meta_path), + use_daemon=False, + ) + assert ok + assert port_third == 6000 + assert len(start_calls) == 2 + + updated_signature = start_calls[1]["passages_signature"] + assert updated_signature != initial_signature + + +def test_list_daemons_ignores_stale_records(tmp_path, monkeypatch): + registry_dir = tmp_path / "servers" + registry_dir.mkdir() + monkeypatch.setattr(EmbeddingServerManager, "_registry_dir", staticmethod(lambda: registry_dir)) + + stale = registry_dir / "stale.json" + stale.write_text( + json.dumps( + { + "pid": 999999, + "port": 65531, + "backend_module_name": "leann_backend_hnsw.hnsw_embedding_server", + "config_signature": {"passages_file": "/tmp/a.meta.json"}, + } + ), + encoding="utf-8", + ) + + records = EmbeddingServerManager.list_daemons() + assert records == [] + assert not stale.exists() + + +def test_stop_daemons_filters_by_backend_and_passages(tmp_path, monkeypatch): + registry_dir = tmp_path / "servers" + registry_dir.mkdir() + monkeypatch.setattr(EmbeddingServerManager, "_registry_dir", staticmethod(lambda: registry_dir)) + + meta_path = (tmp_path / "x.meta.json").resolve() + record = registry_dir / "daemon.json" + record.write_text( + json.dumps( + { + "pid": 12345, + "port": 6001, + "backend_module_name": "leann_backend_hnsw.hnsw_embedding_server", + "config_signature": {"passages_file": str(meta_path)}, + } + ), + encoding="utf-8", + ) + + monkeypatch.setattr( + EmbeddingServerManager, + "list_daemons", + classmethod( + lambda cls: [ + { + "pid": 12345, + "port": 6001, + "backend_module_name": "leann_backend_hnsw.hnsw_embedding_server", + "config_signature": {"passages_file": str(meta_path)}, + "record_path": str(record), + } + ] + ), + ) + + killed: list[tuple[int, int]] = [] + + def fake_kill(pid: int, sig: int): + killed.append((pid, sig)) + + monkeypatch.setattr(os, "kill", fake_kill) + + stopped = EmbeddingServerManager.stop_daemons( + backend_module_name="leann_backend_hnsw.hnsw_embedding_server", + passages_file=str(meta_path), + ) + assert stopped == 1 + assert killed == [(12345, 15)] + assert not record.exists() + + +def test_daemon_registry_reuse_across_manager_instances(tmp_path, monkeypatch): + """Second manager should adopt the daemon started by first manager.""" + registry_dir = tmp_path / "servers" + registry_dir.mkdir() + monkeypatch.setattr(EmbeddingServerManager, "_registry_dir", staticmethod(lambda: registry_dir)) + monkeypatch.setattr( + "leann.embedding_server_manager._get_available_port", + lambda start_port: start_port, + ) + monkeypatch.setattr("leann.embedding_server_manager._check_port", lambda port: True) + monkeypatch.setattr("leann.embedding_server_manager._pid_is_alive", lambda pid: pid == 22222) + + starts = [] + + def fake_start_new_server(self, port, model_name, embedding_mode, **kwargs): + starts.append((port, model_name, embedding_mode)) + self.server_process = DummyProcess(pid=22222) + self.server_port = port + self._server_config = kwargs.get("config_signature") + return True, port + + monkeypatch.setattr(EmbeddingServerManager, "_start_new_server", fake_start_new_server) + + manager1 = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + ok1, port1 = manager1.start_server( + port=6011, + model_name="test-model", + use_daemon=True, + daemon_ttl_seconds=120, + ) + assert ok1 and port1 == 6011 + assert len(starts) == 1 + + manager2 = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + ok2, port2 = manager2.start_server( + port=6011, + model_name="test-model", + use_daemon=True, + daemon_ttl_seconds=120, + ) + assert ok2 and port2 == 6011 + # No second process spawn: adopted from registry. + assert len(starts) == 1 + assert manager2.server_process is None + + +def test_stale_registry_falls_back_to_fresh_start(tmp_path, monkeypatch): + """If registry points to dead daemon, manager should start a new process.""" + registry_dir = tmp_path / "servers" + registry_dir.mkdir() + monkeypatch.setattr(EmbeddingServerManager, "_registry_dir", staticmethod(lambda: registry_dir)) + monkeypatch.setattr( + "leann.embedding_server_manager._get_available_port", + lambda start_port: start_port, + ) + + starts = [] + + def fake_start_new_server(self, port, model_name, embedding_mode, **kwargs): + starts.append(port) + self.server_process = DummyProcess(pid=33333) + self.server_port = port + self._server_config = kwargs.get("config_signature") + return True, port + + monkeypatch.setattr(EmbeddingServerManager, "_start_new_server", fake_start_new_server) + + manager = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + signature = manager._build_config_signature( + model_name="test-model", + embedding_mode="sentence-transformers", + provider_options=None, + passages_file=None, + distance_metric=None, + ) + stale_file = registry_dir / f"{manager._registry_key(signature)}.json" + stale_file.write_text( + json.dumps( + { + "pid": 999999, + "port": 6012, + "backend_module_name": "leann_backend_hnsw.hnsw_embedding_server", + "config_signature": signature, + } + ), + encoding="utf-8", + ) + + monkeypatch.setattr("leann.embedding_server_manager._pid_is_alive", lambda pid: False) + monkeypatch.setattr("leann.embedding_server_manager._check_port", lambda port: False) + + ok, port = manager.start_server( + port=6012, + model_name="test-model", + use_daemon=True, + ) + assert ok and port == 6012 + assert starts == [6012] + assert stale_file.exists() + refreshed = json.loads(stale_file.read_text(encoding="utf-8")) + assert refreshed["pid"] == 33333 + + +def test_build_server_command_includes_daemon_and_warmup_flags(): + manager = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + command = manager._build_server_command( + port=6020, + model_name="m", + embedding_mode="sentence-transformers", + distance_metric="mips", + enable_warmup=True, + use_daemon=True, + daemon_ttl_seconds=321, + ) + assert "--enable-warmup" in command + assert "--daemon-mode" in command + assert "--daemon-ttl" in command + ttl_idx = command.index("--daemon-ttl") + assert command[ttl_idx + 1] == "321" + + command_no_daemon = manager._build_server_command( + port=6020, + model_name="m", + embedding_mode="sentence-transformers", + enable_warmup=False, + use_daemon=False, + ) + assert "--daemon-mode" not in command_no_daemon + assert "--enable-warmup" not in command_no_daemon + + +def test_corrupted_registry_file_is_recovered_on_start(tmp_path, monkeypatch): + """Invalid registry json should not block startup; file is replaced.""" + registry_dir = tmp_path / "servers" + registry_dir.mkdir() + monkeypatch.setattr(EmbeddingServerManager, "_registry_dir", staticmethod(lambda: registry_dir)) + monkeypatch.setattr( + "leann.embedding_server_manager._get_available_port", + lambda start_port: start_port, + ) + + starts = [] + + def fake_start_new_server(self, port, model_name, embedding_mode, **kwargs): + starts.append(port) + self.server_process = DummyProcess(pid=44444) + self.server_port = port + self._server_config = kwargs.get("config_signature") + return True, port + + monkeypatch.setattr(EmbeddingServerManager, "_start_new_server", fake_start_new_server) + + manager = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + signature = manager._build_config_signature( + model_name="test-model", + embedding_mode="sentence-transformers", + provider_options=None, + passages_file=None, + distance_metric=None, + ) + record_path = registry_dir / f"{manager._registry_key(signature)}.json" + record_path.write_text("{invalid-json", encoding="utf-8") + + monkeypatch.setattr("leann.embedding_server_manager._pid_is_alive", lambda pid: False) + monkeypatch.setattr("leann.embedding_server_manager._check_port", lambda port: False) + + ok, port = manager.start_server(port=6030, model_name="test-model", use_daemon=True) + assert ok and port == 6030 + assert starts == [6030] + data = json.loads(record_path.read_text(encoding="utf-8")) + assert data["pid"] == 44444 + + +def test_stop_server_detaches_when_daemon_mode(monkeypatch): + """Daemon mode should detach manager without terminating shared process.""" + manager = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + manager.server_process = cast(Any, DummyProcess(pid=55555)) + manager.server_port = 6031 + manager._server_config = {"model_name": "m"} + manager._daemon_mode = True + + # If terminate is called this test should fail. + called = {"terminate": 0} + + def fail_terminate(): + called["terminate"] += 1 + raise AssertionError("terminate should not be called in daemon detach path") + + manager.server_process.terminate = fail_terminate # type: ignore[method-assign] + + manager.stop_server() + assert called["terminate"] == 0 + assert manager.server_process is None + assert manager.server_port is None + assert manager._server_config is None + + +def test_concurrent_daemon_start_only_spawns_once(tmp_path, monkeypatch): + """Concurrent calls should be serialized by registry lock for same signature.""" + registry_dir = tmp_path / "servers" + registry_dir.mkdir() + monkeypatch.setattr(EmbeddingServerManager, "_registry_dir", staticmethod(lambda: registry_dir)) + monkeypatch.setattr( + "leann.embedding_server_manager._get_available_port", + lambda start_port: start_port, + ) + monkeypatch.setattr("leann.embedding_server_manager._check_port", lambda port: True) + monkeypatch.setattr("leann.embedding_server_manager._pid_is_alive", lambda pid: pid in (77777,)) + + starts = [] + + def fake_start_new_server(self, port, model_name, embedding_mode, **kwargs): + # Force overlap window between two starters. + time.sleep(0.05) + starts.append((self, port)) + self.server_process = DummyProcess(pid=77777) + self.server_port = port + self._server_config = kwargs.get("config_signature") + return True, port + + monkeypatch.setattr(EmbeddingServerManager, "_start_new_server", fake_start_new_server) + + results = [] + + def runner(): + manager = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + ok, port = manager.start_server(port=6040, model_name="test-model", use_daemon=True) + results.append((ok, port)) + + t1 = threading.Thread(target=runner) + t2 = threading.Thread(target=runner) + t1.start() + t2.start() + t1.join(timeout=5) + t2.join(timeout=5) + + assert len(results) == 2 + assert all(ok and port == 6040 for ok, port in results) + # Exactly one actual process start, one adopts registry record. + assert len(starts) == 1 + + +def test_registry_record_write_is_atomic(tmp_path, monkeypatch): + """Registry writes should go through temp file + os.replace.""" + registry_dir = tmp_path / "servers" + registry_dir.mkdir() + monkeypatch.setattr(EmbeddingServerManager, "_registry_dir", staticmethod(lambda: registry_dir)) + + manager = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + manager.server_process = cast(Any, DummyProcess(pid=88888)) + + calls = [] + real_replace = os.replace + + def tracked_replace(src, dst): + calls.append((str(src), str(dst))) + return real_replace(src, dst) + + monkeypatch.setattr(os, "replace", tracked_replace) + + path = manager._write_registry_record( + port=6060, + config_signature={"model_name": "m"}, + daemon_ttl_seconds=123, + ) + assert path.exists() + assert calls, "os.replace should be used for atomic write" + src, dst = calls[0] + assert src.endswith(".json.tmp") + assert dst.endswith(".json") + + +def test_different_passages_files_start_separate_daemons(tmp_path, monkeypatch): + """Two managers with different passages files must NOT share a daemon (issue #281). + + Simulates sequential test runs where each test builds its own index in a + separate directory but uses the same model and embedding mode. A stale + daemon from run A should never be adopted by run B. + """ + registry_dir = tmp_path / "servers" + registry_dir.mkdir() + monkeypatch.setattr(EmbeddingServerManager, "_registry_dir", staticmethod(lambda: registry_dir)) + + # Each call to _get_available_port returns a strictly increasing port so + # that the two daemons land on different ports when they are not sharing. + port_seq = [6200] + + def fake_get_available_port(start_port): + p = port_seq[0] + port_seq[0] += 1 + return p + + monkeypatch.setattr( + "leann.embedding_server_manager._get_available_port", + fake_get_available_port, + ) + + alive_pids: set[int] = set() + monkeypatch.setattr( + "leann.embedding_server_manager._pid_is_alive", lambda pid: pid in alive_pids + ) + monkeypatch.setattr("leann.embedding_server_manager._check_port", lambda port: True) + + pid_seq = [55001] + starts: list[dict] = [] + + def fake_start_new_server(self, port, model_name, embedding_mode, **kwargs): + pid = pid_seq[0] + pid_seq[0] += 1 + alive_pids.add(pid) + starts.append({"port": port, "config": kwargs.get("config_signature")}) + self.server_process = DummyProcess(pid=pid) + self.server_port = port + self._server_config = kwargs.get("config_signature") + return True, port + + monkeypatch.setattr(EmbeddingServerManager, "_start_new_server", fake_start_new_server) + + # Simulate two separate test runs, each in its own temp subdirectory, + # using the *same* filename patterns but *different* parent directories. + run_a = tmp_path / "run_a" + run_a.mkdir() + meta_a = run_a / "myindex.meta.json" + passages_a = run_a / "myindex.passages.jsonl" + passages_a.write_text("passage: hello world\n", encoding="utf-8") + _write_meta(meta_a, passages_a.name, "myindex.passages.idx", total=1) + + run_b = tmp_path / "run_b" + run_b.mkdir() + meta_b = run_b / "myindex.meta.json" + passages_b = run_b / "myindex.passages.jsonl" + passages_b.write_text("passage: different content\n", encoding="utf-8") + _write_meta(meta_b, passages_b.name, "myindex.passages.idx", total=1) + + # Run A: start daemon with index_a passages + manager_a = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + ok_a, port_a = manager_a.start_server( + port=6200, + model_name="test-model", + passages_file=str(meta_a), + use_daemon=True, + ) + assert ok_a + assert len(starts) == 1, "First manager should start one daemon" + + # Run B: a fresh manager with a DIFFERENT passages file must not adopt run A's daemon + manager_b = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + ok_b, port_b = manager_b.start_server( + port=6200, + model_name="test-model", + passages_file=str(meta_b), + use_daemon=True, + ) + assert ok_b + assert len(starts) == 2, ( + "A separate daemon must be spawned for each distinct passages file; " + "sharing daemons across different indices causes 'Failed to fetch embeddings' errors" + ) + assert port_a != port_b, "Each index must communicate with its own daemon port" + + +def test_same_passages_file_reuses_existing_daemon(tmp_path, monkeypatch): + """Two managers with identical passages files SHOULD share the same daemon.""" + registry_dir = tmp_path / "servers" + registry_dir.mkdir() + monkeypatch.setattr(EmbeddingServerManager, "_registry_dir", staticmethod(lambda: registry_dir)) + monkeypatch.setattr( + "leann.embedding_server_manager._get_available_port", lambda start_port: start_port + ) + + alive_pids: set[int] = {99001} + monkeypatch.setattr( + "leann.embedding_server_manager._pid_is_alive", lambda pid: pid in alive_pids + ) + monkeypatch.setattr("leann.embedding_server_manager._check_port", lambda port: True) + + starts: list[int] = [] + + def fake_start_new_server(self, port, model_name, embedding_mode, **kwargs): + starts.append(port) + self.server_process = DummyProcess(pid=99001) + self.server_port = port + self._server_config = kwargs.get("config_signature") + return True, port + + monkeypatch.setattr(EmbeddingServerManager, "_start_new_server", fake_start_new_server) + + meta = tmp_path / "shared.meta.json" + passages = tmp_path / "shared.passages.jsonl" + passages.write_text("shared passage\n", encoding="utf-8") + _write_meta(meta, passages.name, "shared.passages.idx", total=1) + + manager1 = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + ok1, port1 = manager1.start_server( + port=6300, model_name="test-model", passages_file=str(meta), use_daemon=True + ) + assert ok1 and len(starts) == 1 + + manager2 = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + ok2, port2 = manager2.start_server( + port=6300, model_name="test-model", passages_file=str(meta), use_daemon=True + ) + assert ok2 + assert len(starts) == 1, "Second manager should reuse the existing daemon, not start a new one" + assert port1 == port2, "Both managers should connect to the same daemon port" + + +def test_stale_lock_info_removed_when_pid_dead(tmp_path, monkeypatch): + registry_dir = tmp_path / "servers" + registry_dir.mkdir() + monkeypatch.setattr(EmbeddingServerManager, "_registry_dir", staticmethod(lambda: registry_dir)) + monkeypatch.setattr("leann.embedding_server_manager._pid_is_alive", lambda pid: False) + + manager = EmbeddingServerManager("leann_backend_hnsw.hnsw_embedding_server") + signature = manager._build_config_signature( + model_name="x", + embedding_mode="sentence-transformers", + provider_options=None, + passages_file=None, + distance_metric=None, + ) + key = manager._registry_key(signature) + lock_info = registry_dir / f"{key}.lockinfo.json" + lock_info.write_text(json.dumps({"pid": 999999, "ts": time.time()}), encoding="utf-8") + + with manager._registry_lock(signature): + pass + + assert not lock_info.exists() diff --git a/tests/test_embedding_server_manager_e2e.py b/tests/test_embedding_server_manager_e2e.py new file mode 100644 index 0000000..f2d45f5 --- /dev/null +++ b/tests/test_embedding_server_manager_e2e.py @@ -0,0 +1,82 @@ +import os +import time +from pathlib import Path + +from leann.embedding_server_manager import EmbeddingServerManager + + +def _configure_fake_module_env(monkeypatch): + root = Path(__file__).resolve().parents[1] + support_path = str(root / "tests" / "support") + existing = os.environ.get("PYTHONPATH", "") + joined = support_path if not existing else f"{support_path}{os.pathsep}{existing}" + monkeypatch.setenv("PYTHONPATH", joined) + + +def _wait_until(predicate, timeout=5.0, interval=0.05): + deadline = time.time() + timeout + while time.time() < deadline: + if predicate(): + return True + time.sleep(interval) + return False + + +def test_daemon_reuse_with_real_subprocess(tmp_path, monkeypatch): + _configure_fake_module_env(monkeypatch) + monkeypatch.setattr( + EmbeddingServerManager, "_registry_dir", staticmethod(lambda: tmp_path / "servers") + ) + + manager1 = EmbeddingServerManager("fake_embedding_server_module") + ok1, port1 = manager1.start_server( + port=6151, + model_name="fake-model", + use_daemon=True, + daemon_ttl_seconds=60, + enable_warmup=True, + ) + assert ok1 and port1 == 6151 + assert manager1.server_process is not None + pid1 = manager1.server_process.pid + + manager2 = EmbeddingServerManager("fake_embedding_server_module") + ok2, port2 = manager2.start_server( + port=6151, + model_name="fake-model", + use_daemon=True, + daemon_ttl_seconds=60, + ) + assert ok2 and port2 == 6151 + # Adoption path: no newly spawned process object attached. + assert manager2.server_process is None + + records = EmbeddingServerManager.list_daemons() + assert len(records) == 1 + assert int(records[0]["pid"]) == pid1 + + stopped = EmbeddingServerManager.stop_daemons( + backend_module_name="fake_embedding_server_module" + ) + assert stopped == 1 + + +def test_daemon_ttl_expiry_with_real_subprocess(tmp_path, monkeypatch): + _configure_fake_module_env(monkeypatch) + monkeypatch.setattr( + EmbeddingServerManager, "_registry_dir", staticmethod(lambda: tmp_path / "servers") + ) + + manager = EmbeddingServerManager("fake_embedding_server_module") + ok, port = manager.start_server( + port=6152, + model_name="fake-model", + use_daemon=True, + daemon_ttl_seconds=1, + enable_warmup=False, + ) + assert ok and port == 6152 + + # Fake daemon should self-exit after idle TTL. + expired = _wait_until(lambda: len(EmbeddingServerManager.list_daemons()) == 0, timeout=4.0) + assert expired diff --git a/tests/test_flashlib_ivf_backend.py b/tests/test_flashlib_ivf_backend.py new file mode 100644 index 0000000..d5e04a6 --- /dev/null +++ b/tests/test_flashlib_ivf_backend.py @@ -0,0 +1,125 @@ +""" +Correctness tests for the FlashLib IVF backend (``flashlib_ivf``). + +Registry-level (no embedding model needed): builds a small clustered corpus through +the real LEANN backend builders/searchers and checks that ``flashlib_ivf`` (GPU) +registers, persists/reloads its index, and returns recall comparable to the FAISS +``ivf`` backend and to exact ground truth at a matched ``(nlist, nprobe)``. + +Requires a CUDA GPU + ``flashlib`` (skipped otherwise, e.g. in CI). +""" + +import json +import tempfile +from pathlib import Path + +import numpy as np +import pytest + + +def _cuda_or_skip(): + torch = pytest.importorskip("torch") + pytest.importorskip("flashlib") + if not torch.cuda.is_available(): + pytest.skip("FlashLib IVF backend requires a CUDA GPU") + + +def _registry(): + from leann.registry import BACKEND_REGISTRY, autodiscover_backends + + autodiscover_backends() + return BACKEND_REGISTRY + + +def _clustered(n: int, dim: int, seed: int) -> np.ndarray: + rng = np.random.default_rng(seed) + n_clusters = 64 + centers = rng.standard_normal((n_clusters, dim), dtype=np.float32) + centers /= np.linalg.norm(centers, axis=1, keepdims=True) + assign = rng.integers(0, n_clusters, size=n) + db = centers[assign] + 0.05 * rng.standard_normal((n, dim)).astype(np.float32) + db /= np.linalg.norm(db, axis=1, keepdims=True) + return np.ascontiguousarray(db.astype(np.float32)) + + +def _exact_gt(db: np.ndarray, q: np.ndarray, k: int) -> np.ndarray: + scores = q @ db.T + return np.argsort(-scores, axis=1)[:, :k] + + +def _recall(found: np.ndarray, truth: np.ndarray) -> float: + k = truth.shape[1] + return float(np.mean([len(set(found[i]) & set(truth[i])) / k for i in range(len(truth))])) + + +def _write_meta(index_path: str, backend: str, dim: int) -> None: + Path(f"{index_path}.meta.json").write_text( + json.dumps( + { + "version": "1.0", + "backend_name": backend, + "embedding_model": "synthetic", + "dimensions": dim, + "backend_kwargs": {"distance_metric": "cosine"}, + "embedding_mode": "sentence-transformers", + "passage_sources": [], + } + ) + ) + + +def _labels_to_int(out: dict) -> np.ndarray: + return np.array( + [[int(x) if x.lstrip("-").isdigit() else -1 for x in row] for row in out["labels"]], + dtype=np.int64, + ) + + +def test_flashlib_ivf_recall_parity_with_faiss_ivf(): + _cuda_or_skip() + reg = _registry() + assert "flashlib_ivf" in reg, "flashlib_ivf backend not registered" + if "ivf" not in reg: + pytest.skip("faiss ivf backend not installed") + + dim, n, k, nlist, nprobe = 64, 20_000, 10, 128, 16 + db = _clustered(n, dim, seed=0) + queries = db[:100].copy() + ids = [str(i) for i in range(n)] + truth = _exact_gt(db, queries, k) + + kw = {"dimensions": dim, "distance_metric": "cosine", "nlist": nlist, "nprobe": nprobe} + with tempfile.TemporaryDirectory() as tmp: + gpu_path = str(Path(tmp) / "g.leann") + cpu_path = str(Path(tmp) / "c.leann") + + reg["flashlib_ivf"].builder(**kw).build(db, ids, gpu_path, **kw) + reg["ivf"].builder(**kw).build(db, ids, cpu_path, **kw) + + # Persistence: the GPU index + id map are written to disk. + assert (Path(tmp) / "g.flashlib_ivf.pt").exists() + assert (Path(tmp) / "g.flashlib_ivf_id_map.json").exists() + + _write_meta(gpu_path, "flashlib_ivf", dim) + _write_meta(cpu_path, "ivf", dim) + + gpu = reg["flashlib_ivf"].searcher(gpu_path, enable_warmup=False, use_daemon=False) + cpu = reg["ivf"].searcher(cpu_path, enable_warmup=False, use_daemon=False) + + gpu_found = _labels_to_int( + gpu.search(queries, top_k=k, nprobe=nprobe, recompute_embeddings=False) + ) + cpu_found = _labels_to_int( + cpu.search(queries, top_k=k, nprobe=nprobe, recompute_embeddings=False) + ) + + gpu_recall = _recall(gpu_found, truth) + cpu_recall = _recall(cpu_found, truth) + + # At matched (nlist, nprobe) both IVF backends probe ~the same candidate set, so + # recall should be high and comparable (GPU is often a touch higher: independent + # coarse-quantizer training, not "more exact"). + assert gpu_recall > 0.7, f"flashlib_ivf recall too low: {gpu_recall:.3f}" + assert abs(gpu_recall - cpu_recall) < 0.15, ( + f"recall parity off: gpu {gpu_recall:.3f} vs cpu {cpu_recall:.3f}" + ) diff --git a/tests/test_hybrid_search.py b/tests/test_hybrid_search.py new file mode 100644 index 0000000..0ef16bf --- /dev/null +++ b/tests/test_hybrid_search.py @@ -0,0 +1,166 @@ +""" +Comprehensive tests for hybrid search functionality. + +This module tests the hybrid search feature that combines vector search +with BM25 keyword search using the vector_weight parameter. +""" + +import os +import tempfile +from pathlib import Path + +import pytest + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", reason="Skip model tests in CI to avoid MPS memory issues" +) +class TestHybridSearch: + """Test suite for hybrid search functionality.""" + + @pytest.fixture + def sample_index(self): + """Create a sample index for testing.""" + from leann.api import LeannBuilder, LeannSearcher + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "test_hybrid.hnsw") + + # Create documents with diverse content for testing + # Some documents are keyword-rich, others are semantically similar + texts = [ + "The quick brown fox jumps over the lazy dog", + "A fast auburn canine leaps above a sleepy hound", # Semantically similar to first + "Python programming language is great for data science", + "Machine learning and artificial intelligence are transforming technology", + "The weather today is sunny and warm", + "Climate conditions are pleasant with clear skies", # Semantically similar to weather + "Database management systems store and retrieve data efficiently", + "SQL queries help extract information from databases", # Related to databases + "Cooking recipes require precise measurements and timing", + "Baking bread needs flour water yeast and patience", # Related to cooking + ] + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="facebook/contriever", + embedding_mode="sentence-transformers", + M=16, + efConstruction=200, + ) + + for i, text in enumerate(texts): + builder.add_text(text, metadata={"id": str(i), "doc_num": i}) + + builder.build_index(index_path) + + searcher = LeannSearcher(index_path) + yield searcher, texts + + searcher.cleanup() + + def test_pure_vector_search(self, sample_index): + """Test pure vector search (vector_weight=1.0, default).""" + searcher, texts = sample_index + + # Search with vector_weight=1.0 (pure vector search) + results = searcher.search("canine animal", top_k=3, vector_weight=1.0) + + assert len(results) > 0 + assert len(results) <= 3 + # Should find semantically similar documents about animals/dogs + assert any( + "fox" in r.text.lower() or "dog" in r.text.lower() or "canine" in r.text.lower() + for r in results + ) + + def test_pure_keyword_search(self, sample_index): + """Test pure keyword search (vector_weight=0.0).""" + searcher, texts = sample_index + + # Search with vector_weight=0.0 (pure BM25 keyword search) + results = searcher.search("database SQL", top_k=3, vector_weight=0.0) + + assert len(results) > 0 + assert len(results) <= 3 + # Should find documents with exact keyword matches + # BM25 should prioritize documents containing "database" or "SQL" + top_result_text = results[0].text.lower() + assert "database" in top_result_text or "sql" in top_result_text + + def test_hybrid_search_balanced(self, sample_index): + """Test balanced hybrid search (vector_weight=0.5).""" + searcher, texts = sample_index + + # Search with vector_weight=0.5 (balanced hybrid) + results = searcher.search("programming Python code", top_k=5, vector_weight=0.5) + + assert len(results) > 0 + assert len(results) <= 5 + # Should combine both semantic and keyword matching + # At least one result should contain "Python" or "programming" + assert any("python" in r.text.lower() or "programming" in r.text.lower() for r in results) + + def test_hybrid_search_vector_heavy(self, sample_index): + """Test vector-heavy hybrid search (vector_weight=0.8).""" + searcher, texts = sample_index + + # Search with vector_weight=0.8 (mostly vector, some keyword) + results = searcher.search("sunny weather conditions", top_k=3, vector_weight=0.8) + + assert len(results) > 0 + # Should prioritize semantic similarity but consider keywords + # Should find weather-related documents + assert any( + "weather" in r.text.lower() or "sunny" in r.text.lower() or "climate" in r.text.lower() + for r in results + ) + + def test_hybrid_search_keyword_heavy(self, sample_index): + """Test keyword-heavy hybrid search (vector_weight=0.2).""" + searcher, texts = sample_index + + # Search with vector_weight=0.2 (mostly keyword, some vector) + results = searcher.search("bread flour baking", top_k=3, vector_weight=0.2) + + assert len(results) > 0 + # Should prioritize keyword matches + # Should find documents with exact keyword matches + top_results_text = " ".join([r.text.lower() for r in results[:2]]) + assert ( + "bread" in top_results_text + or "flour" in top_results_text + or "baking" in top_results_text + ) + + def test_hybrid_search_score_combination(self, sample_index): + """Test that hybrid search properly combines scores.""" + searcher, texts = sample_index + + # Get results with different vector_weight values + pure_vector = searcher.search("machine learning AI", top_k=5, vector_weight=1.0) + pure_keyword = searcher.search("machine learning AI", top_k=5, vector_weight=0.0) + hybrid = searcher.search("machine learning AI", top_k=5, vector_weight=0.5) + + # All should return results + assert len(pure_vector) > 0 + assert len(pure_keyword) > 0 + assert len(hybrid) > 0 + + # Hybrid results should potentially differ from pure approaches + # (though with small dataset, there might be overlap) + assert all(r.score > 0 for r in hybrid) + + def test_hybrid_search_with_metadata_filters(self, sample_index): + """Test hybrid search combined with metadata filtering.""" + searcher, texts = sample_index + + # Search with hybrid and metadata filter + results = searcher.search( + "data information", top_k=5, vector_weight=0.6, metadata_filters={"doc_num": {"<": 8}} + ) + + assert len(results) > 0 + # All results should satisfy the metadata filter + for r in results: + assert r.metadata.get("doc_num", 999) < 8 diff --git a/tests/test_incremental_build.py b/tests/test_incremental_build.py new file mode 100644 index 0000000..63ec0ea --- /dev/null +++ b/tests/test_incremental_build.py @@ -0,0 +1,373 @@ +""" +Tests for incremental build (Feature #89). + +When an index already exists and build is run again without --force, +only new files are indexed and appended to the existing index (HNSW, non-compact only). +Change detection uses content-hash (merkle tree) via FileSynchronizer. +""" + +import os +from pathlib import Path + +import pytest +from leann.cli import _normalize_path +from leann.sync import FileSynchronizer + + +def test_normalize_path(): + assert _normalize_path("") == "" + assert _normalize_path("/a/b") == "/a/b" or "a" in _normalize_path("/a/b") + rel = "foo/bar" + out = _normalize_path(rel) + assert Path(out).is_absolute() or out == rel + + +def test_file_synchronizer_detect_changes(tmp_path): + """FileSynchronizer detect_changes returns all files as added when no snapshot exists.""" + docs = tmp_path / "docs" + docs.mkdir() + (docs / "a.txt").write_text("hello", encoding="utf-8") + snapshot = str(tmp_path / "test.pickle") + fs = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + added, removed, modified = fs.detect_changes() + assert len(added) == 1 + assert len(removed) == 0 + assert len(modified) == 0 + + +def test_file_synchronizer_no_changes_after_commit(tmp_path): + """After commit, detect_changes should report no changes if files haven't changed.""" + docs = tmp_path / "docs" + docs.mkdir() + (docs / "a.txt").write_text("hello", encoding="utf-8") + snapshot = str(tmp_path / "test.pickle") + fs = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + fs.detect_changes() + fs.commit() + + fs2 = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + added, removed, modified = fs2.detect_changes() + assert not added and not removed and not modified + + +def test_file_synchronizer_detects_new_file(tmp_path): + """Adding a file after commit should be detected as added.""" + docs = tmp_path / "docs" + docs.mkdir() + (docs / "a.txt").write_text("hello", encoding="utf-8") + snapshot = str(tmp_path / "test.pickle") + fs = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + fs.detect_changes() + fs.commit() + + (docs / "b.txt").write_text("world", encoding="utf-8") + fs2 = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + added, removed, modified = fs2.detect_changes() + assert len(added) == 1 + assert len(removed) == 0 + assert len(modified) == 0 + + +def test_file_synchronizer_detects_modification(tmp_path): + """Changing file content should be detected as modified.""" + docs = tmp_path / "docs" + docs.mkdir() + (docs / "a.txt").write_text("hello", encoding="utf-8") + snapshot = str(tmp_path / "test.pickle") + fs = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + fs.detect_changes() + fs.commit() + + (docs / "a.txt").write_text("changed", encoding="utf-8") + fs2 = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + added, removed, modified = fs2.detect_changes() + assert len(modified) == 1 + assert len(added) == 0 + + +def test_file_synchronizer_touch_no_false_positive(tmp_path): + """Touching a file (mtime change, same content) should NOT report as modified.""" + docs = tmp_path / "docs" + docs.mkdir() + f = docs / "a.txt" + f.write_text("hello", encoding="utf-8") + snapshot = str(tmp_path / "test.pickle") + fs = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + fs.detect_changes() + fs.commit() + + os.utime(f, None) + fs2 = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + added, removed, modified = fs2.detect_changes() + assert not added and not removed and not modified + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip in CI to avoid embedding/model load", +) +def test_incremental_build_adds_only_new_files(tmp_path): + """Build once with one file, add a second file, run build again without --force; index grows.""" + import asyncio + + from leann.api import LeannSearcher + from leann.cli import LeannCLI + + docs_dir = tmp_path / "docs" + docs_dir.mkdir() + (docs_dir / "a.txt").write_text("First document content for indexing.", encoding="utf-8") + + cli = LeannCLI() + cli.indexes_dir = tmp_path / ".leann" / "indexes" + cli.indexes_dir.mkdir(parents=True, exist_ok=True) + index_name = "incr_test" + index_dir = cli.indexes_dir / index_name + index_path = cli.get_index_path(index_name) + + parser = cli.create_parser() + args = parser.parse_args( + [ + "build", + index_name, + "--docs", + str(docs_dir), + "--backend-name", + "hnsw", + "--no-compact", + "--embedding-model", + "all-MiniLM-L6-v2", + "--embedding-mode", + "sentence-transformers", + "--force", + ] + ) + asyncio.run(cli.build_index(args)) + assert index_dir.exists() + assert (index_dir / "documents.leann.meta.json").exists() + + # Add second file + (docs_dir / "b.txt").write_text("Second document content.", encoding="utf-8") + + # Build again without --force (incremental) + args2 = parser.parse_args( + [ + "build", + index_name, + "--docs", + str(docs_dir), + "--backend-name", + "hnsw", + "--no-compact", + "--embedding-model", + "all-MiniLM-L6-v2", + "--embedding-mode", + "sentence-transformers", + ] + ) + asyncio.run(cli.build_index(args2)) + + # Index should still be searchable and contain both files + searcher = LeannSearcher(index_path) + results = searcher.search("Second document", top_k=3) + searcher.cleanup() + assert len(results) >= 1 + assert "Second" in results[0].text or "document" in results[0].text + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip in CI to avoid embedding/model load", +) +def test_ivf_incremental_add_then_remove_searchable(tmp_path): + """IVF: add content, search finds it; delete content, incremental build, search no longer finds it.""" + import asyncio + + from leann.api import LeannSearcher + from leann.cli import LeannCLI + + docs_dir = tmp_path / "docs" + docs_dir.mkdir() + unique_phrase = "XYZZY_PLUGH_INCR_TEST_12345" + (docs_dir / "f.txt").write_text( + f"Initial content.\n\n{unique_phrase}\n\nMore text here.", + encoding="utf-8", + ) + # Add extra files so IVF has enough training points (nlist=100) + for i in range(110): + (docs_dir / f"filler_{i:03d}.txt").write_text( + f"Filler document {i} with some content to create enough chunks.", + encoding="utf-8", + ) + + cli = LeannCLI() + cli.indexes_dir = tmp_path / ".leann" / "indexes" + cli.indexes_dir.mkdir(parents=True, exist_ok=True) + index_name = "ivf_incr_test" + index_path = cli.get_index_path(index_name) + + parser = cli.create_parser() + build_args = [ + "build", + index_name, + "--docs", + str(docs_dir), + "--backend-name", + "ivf", + "--embedding-model", + "all-MiniLM-L6-v2", + "--embedding-mode", + "sentence-transformers", + ] + + asyncio.run(cli.build_index(parser.parse_args([*build_args, "--force"]))) + + searcher = LeannSearcher(index_path) + results = searcher.search(unique_phrase, top_k=5) + searcher.cleanup() + assert len(results) >= 1, "Added content should be searchable" + assert unique_phrase in results[0].text + + # Remove the unique phrase from the file + (docs_dir / "f.txt").write_text("Initial content.\n\nMore text here.", encoding="utf-8") + + # Incremental build (IVF remove+add) + asyncio.run(cli.build_index(parser.parse_args(build_args))) + + # Deleted content should no longer be searchable + searcher = LeannSearcher(index_path) + results = searcher.search(unique_phrase, top_k=5) + searcher.cleanup() + assert all(unique_phrase not in r.text for r in results), ( + "Deleted content should not appear in search results" + ) + + +@pytest.mark.skipif( + os.environ.get("CI") == "true", + reason="Skip in CI to avoid embedding/model load", +) +def test_ivf_multiple_incremental_no_duplicates(tmp_path): + """IVF: modifying the same file across multiple incremental builds must not create duplicate chunks.""" + import asyncio + import json + import pickle + + from leann.api import LeannSearcher + from leann.cli import LeannCLI + + docs_dir = tmp_path / "docs" + docs_dir.mkdir() + + target_phrase_v1 = "UNIQUE_TARGET_V1_ALPHA_BRAVO" + target_phrase_v2 = "UNIQUE_TARGET_V2_CHARLIE_DELTA" + target_phrase_v3 = "UNIQUE_TARGET_V3_ECHO_FOXTROT" + + (docs_dir / "target.txt").write_text( + f"Version one content.\n\n{target_phrase_v1}\n\nEnd of version one.", + encoding="utf-8", + ) + # Filler files for IVF training (nlist=100) + for i in range(110): + (docs_dir / f"filler_{i:03d}.txt").write_text( + f"Filler document {i} with unique content for padding the IVF index.", + encoding="utf-8", + ) + + cli = LeannCLI() + cli.indexes_dir = tmp_path / ".leann" / "indexes" + cli.indexes_dir.mkdir(parents=True, exist_ok=True) + index_name = "ivf_dup_test" + index_path = cli.get_index_path(index_name) + index_dir = cli.indexes_dir / index_name + + parser = cli.create_parser() + build_args = [ + "build", + index_name, + "--docs", + str(docs_dir), + "--backend-name", + "ivf", + "--embedding-model", + "all-MiniLM-L6-v2", + "--embedding-mode", + "sentence-transformers", + ] + + # --- Initial build (--force) --- + asyncio.run(cli.build_index(parser.parse_args([*build_args, "--force"]))) + + searcher = LeannSearcher(index_path) + results = searcher.search(target_phrase_v1, top_k=10) + searcher.cleanup() + v1_hits = [r for r in results if target_phrase_v1 in r.text] + assert len(v1_hits) >= 1, "V1 content should be searchable after initial build" + + # --- Modify target file to V2, incremental build --- + (docs_dir / "target.txt").write_text( + f"Version two content.\n\n{target_phrase_v2}\n\nEnd of version two.", + encoding="utf-8", + ) + asyncio.run(cli.build_index(parser.parse_args(build_args))) + + searcher = LeannSearcher(index_path) + results_v2 = searcher.search(target_phrase_v1, top_k=10) + searcher.cleanup() + assert all(target_phrase_v1 not in r.text for r in results_v2), ( + "V1 content should be gone after first incremental update" + ) + + searcher = LeannSearcher(index_path) + results_v2b = searcher.search(target_phrase_v2, top_k=10) + searcher.cleanup() + v2_hits = [r for r in results_v2b if target_phrase_v2 in r.text] + assert len(v2_hits) >= 1, "V2 content should be searchable" + + # --- Modify target file to V3, second incremental build --- + (docs_dir / "target.txt").write_text( + f"Version three content.\n\n{target_phrase_v3}\n\nEnd of version three.", + encoding="utf-8", + ) + asyncio.run(cli.build_index(parser.parse_args(build_args))) + + # V1 and V2 should be gone, only V3 present + searcher = LeannSearcher(index_path) + results_v3_check_v1 = searcher.search(target_phrase_v1, top_k=10) + searcher.cleanup() + assert all(target_phrase_v1 not in r.text for r in results_v3_check_v1), ( + "V1 content should NOT appear after two incremental updates" + ) + + searcher = LeannSearcher(index_path) + results_v3_check_v2 = searcher.search(target_phrase_v2, top_k=10) + searcher.cleanup() + assert all(target_phrase_v2 not in r.text for r in results_v3_check_v2), ( + "V2 content should NOT appear after second incremental update" + ) + + searcher = LeannSearcher(index_path) + results_v3 = searcher.search(target_phrase_v3, top_k=10) + searcher.cleanup() + v3_hits = [r for r in results_v3 if target_phrase_v3 in r.text] + assert len(v3_hits) >= 1, "V3 content should be searchable" + + # --- Verify passages.jsonl has no stale entries --- + passages_file = index_dir / "documents.leann.passages.jsonl" + offset_file = index_dir / "documents.leann.passages.idx" + with open(offset_file, "rb") as f: + offset_map = pickle.load(f) + live_ids = set(offset_map.keys()) + + jsonl_ids = [] + with open(passages_file, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + data = json.loads(line) + jsonl_ids.append(data["id"]) + + stale_ids = [pid for pid in jsonl_ids if pid not in live_ids] + assert len(stale_ids) == 0, ( + f"passages.jsonl has {len(stale_ids)} stale entries not in offset_map: {stale_ids[:5]}" + ) diff --git a/tests/test_lmstudio_bridge.py b/tests/test_lmstudio_bridge.py new file mode 100644 index 0000000..b563682 --- /dev/null +++ b/tests/test_lmstudio_bridge.py @@ -0,0 +1,315 @@ +"""Unit tests for LM Studio TypeScript SDK bridge functionality. + +This test suite defines the contract for the LM Studio SDK bridge that queries +model context length via Node.js subprocess. These tests verify: + +1. Successful SDK query returns context length +2. Graceful fallback when Node.js not installed (FileNotFoundError) +3. Graceful fallback when SDK not installed (npm error) +4. Timeout handling (subprocess.TimeoutExpired) +5. Invalid JSON response handling + +All tests are written in Red Phase - they should FAIL initially because the +`_query_lmstudio_context_limit` function does not exist yet. + +The function contract: +- Inputs: model_name (str), base_url (str, WebSocket format "ws://localhost:1234") +- Outputs: context_length (int) or None on error +- Requirements: + 1. Call Node.js with inline JavaScript using @lmstudio/sdk + 2. 10-second timeout (accounts for Node.js startup) + 3. Graceful fallback on any error (returns None, doesn't raise) + 4. Parse JSON response with contextLength field + 5. Log errors at debug level (not warning/error) +""" + +import subprocess +from unittest.mock import Mock + +import pytest + +# Try to import the function - if it doesn't exist, tests will fail as expected +try: + from leann.embedding_compute import _query_lmstudio_context_limit +except ImportError: + # Function doesn't exist yet (Red Phase) - create a placeholder that will fail + def _query_lmstudio_context_limit(*args, **kwargs): + raise NotImplementedError( + "_query_lmstudio_context_limit not implemented yet - this is the Red Phase" + ) + + +class TestLMStudioBridge: + """Tests for LM Studio TypeScript SDK bridge integration.""" + + def test_query_lmstudio_success(self, monkeypatch): + """Verify successful SDK query returns context length. + + When the Node.js subprocess successfully queries the LM Studio SDK, + it should return a JSON response with contextLength field. The function + should parse this and return the integer context length. + """ + + def mock_run(*args, **kwargs): + # Verify timeout is set to 10 seconds + assert kwargs.get("timeout") == 10, "Should use 10-second timeout for Node.js startup" + + # Verify capture_output and text=True are set + assert kwargs.get("capture_output") is True, "Should capture stdout/stderr" + assert kwargs.get("text") is True, "Should decode output as text" + + # Return successful JSON response + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = '{"contextLength": 8192, "identifier": "custom-model"}' + mock_result.stderr = "" + return mock_result + + monkeypatch.setattr("subprocess.run", mock_run) + + # Test with typical LM Studio model + limit = _query_lmstudio_context_limit( + model_name="custom-model", base_url="ws://localhost:1234" + ) + + assert limit == 8192, "Should return context length from SDK response" + + def test_query_lmstudio_nodejs_not_found(self, monkeypatch): + """Verify graceful fallback when Node.js not installed. + + When Node.js is not installed, subprocess.run will raise FileNotFoundError. + The function should catch this and return None (graceful fallback to registry). + """ + + def mock_run(*args, **kwargs): + raise FileNotFoundError("node: command not found") + + monkeypatch.setattr("subprocess.run", mock_run) + + limit = _query_lmstudio_context_limit( + model_name="custom-model", base_url="ws://localhost:1234" + ) + + assert limit is None, "Should return None when Node.js not installed" + + def test_query_lmstudio_sdk_not_installed(self, monkeypatch): + """Verify graceful fallback when @lmstudio/sdk not installed. + + When the SDK npm package is not installed, Node.js will return non-zero + exit code with error message in stderr. The function should detect this + and return None. + """ + + def mock_run(*args, **kwargs): + mock_result = Mock() + mock_result.returncode = 1 + mock_result.stdout = "" + mock_result.stderr = ( + "Error: Cannot find module '@lmstudio/sdk'\nRequire stack:\n- /path/to/script.js" + ) + return mock_result + + monkeypatch.setattr("subprocess.run", mock_run) + + limit = _query_lmstudio_context_limit( + model_name="custom-model", base_url="ws://localhost:1234" + ) + + assert limit is None, "Should return None when SDK not installed" + + def test_query_lmstudio_timeout(self, monkeypatch): + """Verify graceful fallback when subprocess times out. + + When the Node.js process takes longer than 10 seconds (e.g., LM Studio + not responding), subprocess.TimeoutExpired should be raised. The function + should catch this and return None. + """ + + def mock_run(*args, **kwargs): + raise subprocess.TimeoutExpired(cmd=["node", "lmstudio_bridge.js"], timeout=10) + + monkeypatch.setattr("subprocess.run", mock_run) + + limit = _query_lmstudio_context_limit( + model_name="custom-model", base_url="ws://localhost:1234" + ) + + assert limit is None, "Should return None on timeout" + + def test_query_lmstudio_invalid_json(self, monkeypatch): + """Verify graceful fallback when response is invalid JSON. + + When the subprocess returns malformed JSON (e.g., due to SDK error), + json.loads will raise ValueError/JSONDecodeError. The function should + catch this and return None. + """ + + def mock_run(*args, **kwargs): + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = "This is not valid JSON" + mock_result.stderr = "" + return mock_result + + monkeypatch.setattr("subprocess.run", mock_run) + + limit = _query_lmstudio_context_limit( + model_name="custom-model", base_url="ws://localhost:1234" + ) + + assert limit is None, "Should return None when JSON parsing fails" + + def test_query_lmstudio_missing_context_length_field(self, monkeypatch): + """Verify graceful fallback when JSON lacks contextLength field. + + When the SDK returns valid JSON but without the expected contextLength + field (e.g., error response), the function should return None. + """ + + def mock_run(*args, **kwargs): + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = '{"identifier": "test-model", "error": "Model not found"}' + mock_result.stderr = "" + return mock_result + + monkeypatch.setattr("subprocess.run", mock_run) + + limit = _query_lmstudio_context_limit( + model_name="nonexistent-model", base_url="ws://localhost:1234" + ) + + assert limit is None, "Should return None when contextLength field missing" + + def test_query_lmstudio_null_context_length(self, monkeypatch): + """Verify graceful fallback when contextLength is null. + + When the SDK returns contextLength: null (model couldn't be loaded), + the function should return None for registry fallback. + """ + + def mock_run(*args, **kwargs): + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = '{"contextLength": null, "identifier": "test-model"}' + mock_result.stderr = "" + return mock_result + + monkeypatch.setattr("subprocess.run", mock_run) + + limit = _query_lmstudio_context_limit( + model_name="test-model", base_url="ws://localhost:1234" + ) + + assert limit is None, "Should return None when contextLength is null" + + def test_query_lmstudio_zero_context_length(self, monkeypatch): + """Verify graceful fallback when contextLength is zero. + + When the SDK returns contextLength: 0 (invalid value), the function + should return None to trigger registry fallback. + """ + + def mock_run(*args, **kwargs): + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = '{"contextLength": 0, "identifier": "test-model"}' + mock_result.stderr = "" + return mock_result + + monkeypatch.setattr("subprocess.run", mock_run) + + limit = _query_lmstudio_context_limit( + model_name="test-model", base_url="ws://localhost:1234" + ) + + assert limit is None, "Should return None when contextLength is zero" + + def test_query_lmstudio_with_custom_port(self, monkeypatch): + """Verify SDK query works with non-default WebSocket port. + + LM Studio can run on custom ports. The function should pass the + provided base_url to the Node.js subprocess. + """ + + def mock_run(*args, **kwargs): + # Verify the base_url argument is passed correctly + command = args[0] if args else kwargs.get("args", []) + assert "ws://localhost:8080" in " ".join(command), ( + "Should pass custom port to subprocess" + ) + + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = '{"contextLength": 4096, "identifier": "custom-model"}' + mock_result.stderr = "" + return mock_result + + monkeypatch.setattr("subprocess.run", mock_run) + + limit = _query_lmstudio_context_limit( + model_name="custom-model", base_url="ws://localhost:8080" + ) + + assert limit == 4096, "Should work with custom WebSocket port" + + @pytest.mark.parametrize( + "context_length,expected", + [ + (512, 512), # Small context + (2048, 2048), # Common context + (8192, 8192), # Large context + (32768, 32768), # Very large context + ], + ) + def test_query_lmstudio_various_context_lengths(self, monkeypatch, context_length, expected): + """Verify SDK query handles various context length values. + + Different models have different context lengths. The function should + correctly parse and return any positive integer value. + """ + + def mock_run(*args, **kwargs): + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = f'{{"contextLength": {context_length}, "identifier": "test"}}' + mock_result.stderr = "" + return mock_result + + monkeypatch.setattr("subprocess.run", mock_run) + + limit = _query_lmstudio_context_limit( + model_name="test-model", base_url="ws://localhost:1234" + ) + + assert limit == expected, f"Should return {expected} for context length {context_length}" + + def test_query_lmstudio_logs_at_debug_level(self, monkeypatch, caplog): + """Verify errors are logged at DEBUG level, not WARNING/ERROR. + + Following the graceful fallback pattern from Ollama implementation, + errors should be logged at debug level to avoid alarming users when + fallback to registry works fine. + """ + import logging + + caplog.set_level(logging.DEBUG, logger="leann.embedding_compute") + + def mock_run(*args, **kwargs): + raise FileNotFoundError("node: command not found") + + monkeypatch.setattr("subprocess.run", mock_run) + + _query_lmstudio_context_limit(model_name="test-model", base_url="ws://localhost:1234") + + # Check that debug logging occurred (not warning/error) + debug_logs = [record for record in caplog.records if record.levelname == "DEBUG"] + assert len(debug_logs) > 0, "Should log error at DEBUG level" + + # Verify no WARNING or ERROR logs + warning_or_error_logs = [ + record for record in caplog.records if record.levelname in ["WARNING", "ERROR"] + ] + assert len(warning_or_error_logs) == 0, ( + "Should not log at WARNING/ERROR level for expected failures" + ) diff --git a/tests/test_mcp_integration.py b/tests/test_mcp_integration.py new file mode 100644 index 0000000..99a5cdb --- /dev/null +++ b/tests/test_mcp_integration.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +Test script for MCP integration implementations. + +This script tests the basic functionality of the MCP readers and RAG applications +without requiring actual MCP servers to be running. +""" + +import sys +from pathlib import Path + +# Add the parent directory to the path so we can import from apps +sys.path.append(str(Path(__file__).parent.parent)) + +from apps.slack_data.slack_mcp_reader import SlackMCPReader +from apps.slack_rag import SlackMCPRAG +from apps.twitter_data.twitter_mcp_reader import TwitterMCPReader +from apps.twitter_rag import TwitterMCPRAG + + +def test_slack_reader_initialization(): + """Test that SlackMCPReader can be initialized with various parameters.""" + print("Testing SlackMCPReader initialization...") + + # Test basic initialization + reader = SlackMCPReader("slack-mcp-server") + assert reader.mcp_server_command == "slack-mcp-server" + assert reader.concatenate_conversations + assert reader.max_messages_per_conversation == 100 + + # Test with custom parameters + reader = SlackMCPReader( + "custom-slack-server", + workspace_name="test-workspace", + concatenate_conversations=False, + max_messages_per_conversation=50, + ) + assert reader.workspace_name == "test-workspace" + assert not reader.concatenate_conversations + assert reader.max_messages_per_conversation == 50 + + print("βœ… SlackMCPReader initialization tests passed") + + +def test_twitter_reader_initialization(): + """Test that TwitterMCPReader can be initialized with various parameters.""" + print("Testing TwitterMCPReader initialization...") + + # Test basic initialization + reader = TwitterMCPReader("twitter-mcp-server") + assert reader.mcp_server_command == "twitter-mcp-server" + assert reader.include_tweet_content + assert reader.include_metadata + assert reader.max_bookmarks == 1000 + + # Test with custom parameters + reader = TwitterMCPReader( + "custom-twitter-server", + username="testuser", + include_tweet_content=False, + include_metadata=False, + max_bookmarks=500, + ) + assert reader.username == "testuser" + assert not reader.include_tweet_content + assert not reader.include_metadata + assert reader.max_bookmarks == 500 + + print("βœ… TwitterMCPReader initialization tests passed") + + +def test_slack_message_formatting(): + """Test Slack message formatting functionality.""" + print("Testing Slack message formatting...") + + reader = SlackMCPReader("slack-mcp-server") + + # Test basic message formatting + message = { + "text": "Hello, world!", + "user": "john_doe", + "channel": "general", + "ts": "1234567890.123456", + } + + formatted = reader._format_message(message) + assert "Channel: #general" in formatted + assert "User: john_doe" in formatted + assert "Message: Hello, world!" in formatted + assert "Time:" in formatted + + # Test with missing fields + message = {"text": "Simple message"} + formatted = reader._format_message(message) + assert "Message: Simple message" in formatted + + print("βœ… Slack message formatting tests passed") + + +def test_twitter_bookmark_formatting(): + """Test Twitter bookmark formatting functionality.""" + print("Testing Twitter bookmark formatting...") + + reader = TwitterMCPReader("twitter-mcp-server") + + # Test basic bookmark formatting + bookmark = { + "text": "This is a great article about AI!", + "author": "ai_researcher", + "created_at": "2024-01-01T12:00:00Z", + "url": "https://twitter.com/ai_researcher/status/123456789", + "likes": 42, + "retweets": 15, + } + + formatted = reader._format_bookmark(bookmark) + assert "=== Twitter Bookmark ===" in formatted + assert "Author: @ai_researcher" in formatted + assert "Content:" in formatted + assert "This is a great article about AI!" in formatted + assert "URL: https://twitter.com" in formatted + assert "Likes: 42" in formatted + assert "Retweets: 15" in formatted + + # Test with minimal data + bookmark = {"text": "Simple tweet"} + formatted = reader._format_bookmark(bookmark) + assert "=== Twitter Bookmark ===" in formatted + assert "Simple tweet" in formatted + + print("βœ… Twitter bookmark formatting tests passed") + + +def test_slack_rag_initialization(): + """Test that SlackMCPRAG can be initialized.""" + print("Testing SlackMCPRAG initialization...") + + app = SlackMCPRAG() + assert app.default_index_name == "slack_messages" + assert hasattr(app, "parser") + + print("βœ… SlackMCPRAG initialization tests passed") + + +def test_twitter_rag_initialization(): + """Test that TwitterMCPRAG can be initialized.""" + print("Testing TwitterMCPRAG initialization...") + + app = TwitterMCPRAG() + assert app.default_index_name == "twitter_bookmarks" + assert hasattr(app, "parser") + + print("βœ… TwitterMCPRAG initialization tests passed") + + +def test_concatenated_content_creation(): + """Test creation of concatenated content from multiple messages.""" + print("Testing concatenated content creation...") + + reader = SlackMCPReader("slack-mcp-server", workspace_name="test-workspace") + + messages = [ + {"text": "First message", "user": "alice", "ts": "1000"}, + {"text": "Second message", "user": "bob", "ts": "2000"}, + {"text": "Third message", "user": "charlie", "ts": "3000"}, + ] + + content = reader._create_concatenated_content(messages, "general") + + assert "Slack Channel: #general" in content + assert "Message Count: 3" in content + assert "Workspace: test-workspace" in content + assert "First message" in content + assert "Second message" in content + assert "Third message" in content + + print("βœ… Concatenated content creation tests passed") + + +def main(): + """Run all tests.""" + print("πŸ§ͺ Running MCP Integration Tests") + print("=" * 50) + + try: + test_slack_reader_initialization() + test_twitter_reader_initialization() + test_slack_message_formatting() + test_twitter_bookmark_formatting() + test_slack_rag_initialization() + test_twitter_rag_initialization() + test_concatenated_content_creation() + + print("\n" + "=" * 50) + print("πŸŽ‰ All tests passed! MCP integration is working correctly.") + print("\nNext steps:") + print("1. Install actual MCP servers for Slack and Twitter") + print("2. Configure API credentials") + print("3. Test with --test-connection flag") + print("4. Start indexing your live data!") + + except Exception as e: + print(f"\n❌ Test failed: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/test_mcp_standalone.py b/tests/test_mcp_standalone.py new file mode 100644 index 0000000..c6c6ccd --- /dev/null +++ b/tests/test_mcp_standalone.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +""" +Standalone test script for MCP integration implementations. + +This script tests the basic functionality of the MCP readers +without requiring LEANN core dependencies. +""" + +import json +import sys +from pathlib import Path + +# Add the parent directory to the path so we can import from apps +sys.path.append(str(Path(__file__).parent.parent)) + + +def test_slack_reader_basic(): + """Test basic SlackMCPReader functionality without async operations.""" + print("Testing SlackMCPReader basic functionality...") + + # Import and test initialization + from apps.slack_data.slack_mcp_reader import SlackMCPReader + + reader = SlackMCPReader("slack-mcp-server") + assert reader.mcp_server_command == "slack-mcp-server" + assert reader.concatenate_conversations + + # Test message formatting + message = { + "text": "Hello team! How's the project going?", + "user": "john_doe", + "channel": "general", + "ts": "1234567890.123456", + } + + formatted = reader._format_message(message) + assert "Channel: #general" in formatted + assert "User: john_doe" in formatted + assert "Message: Hello team!" in formatted + + # Test concatenated content creation + messages = [ + {"text": "First message", "user": "alice", "ts": "1000"}, + {"text": "Second message", "user": "bob", "ts": "2000"}, + ] + + content = reader._create_concatenated_content(messages, "dev-team") + assert "Slack Channel: #dev-team" in content + assert "Message Count: 2" in content + assert "First message" in content + assert "Second message" in content + + print("βœ… SlackMCPReader basic tests passed") + + +def test_twitter_reader_basic(): + """Test basic TwitterMCPReader functionality.""" + print("Testing TwitterMCPReader basic functionality...") + + from apps.twitter_data.twitter_mcp_reader import TwitterMCPReader + + reader = TwitterMCPReader("twitter-mcp-server") + assert reader.mcp_server_command == "twitter-mcp-server" + assert reader.include_tweet_content + assert reader.max_bookmarks == 1000 + + # Test bookmark formatting + bookmark = { + "text": "Amazing article about the future of AI! Must read for everyone interested in tech.", + "author": "tech_guru", + "created_at": "2024-01-15T14:30:00Z", + "url": "https://twitter.com/tech_guru/status/123456789", + "likes": 156, + "retweets": 42, + "replies": 23, + "hashtags": ["AI", "tech", "future"], + "mentions": ["@openai", "@anthropic"], + } + + formatted = reader._format_bookmark(bookmark) + assert "=== Twitter Bookmark ===" in formatted + assert "Author: @tech_guru" in formatted + assert "Amazing article about the future of AI!" in formatted + assert "Likes: 156" in formatted + assert "Retweets: 42" in formatted + assert "Hashtags: AI, tech, future" in formatted + assert "Mentions: @openai, @anthropic" in formatted + + # Test with minimal data + simple_bookmark = {"text": "Short tweet", "author": "user123"} + formatted_simple = reader._format_bookmark(simple_bookmark) + assert "=== Twitter Bookmark ===" in formatted_simple + assert "Short tweet" in formatted_simple + assert "Author: @user123" in formatted_simple + + print("βœ… TwitterMCPReader basic tests passed") + + +def test_mcp_request_format(): + """Test MCP request formatting.""" + print("Testing MCP request formatting...") + + # Test initialization request format + init_request = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "leann-slack-reader", "version": "1.0.0"}, + }, + } + + # Verify it's valid JSON + json_str = json.dumps(init_request) + parsed = json.loads(json_str) + assert parsed["jsonrpc"] == "2.0" + assert parsed["method"] == "initialize" + assert parsed["params"]["protocolVersion"] == "2024-11-05" + + # Test tools/list request + list_request = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}} + + json_str = json.dumps(list_request) + parsed = json.loads(json_str) + assert parsed["method"] == "tools/list" + + print("βœ… MCP request formatting tests passed") + + +def test_data_processing(): + """Test data processing capabilities.""" + print("Testing data processing capabilities...") + + from apps.slack_data.slack_mcp_reader import SlackMCPReader + from apps.twitter_data.twitter_mcp_reader import TwitterMCPReader + + # Test Slack message processing with various formats + slack_reader = SlackMCPReader("test-server") + + messages_with_timestamps = [ + {"text": "Meeting in 5 minutes", "user": "alice", "ts": "1000.123"}, + {"text": "On my way!", "user": "bob", "ts": "1001.456"}, + {"text": "Starting now", "user": "charlie", "ts": "1002.789"}, + ] + + content = slack_reader._create_concatenated_content(messages_with_timestamps, "meetings") + assert "Meeting in 5 minutes" in content + assert "On my way!" in content + assert "Starting now" in content + + # Test Twitter bookmark processing with engagement data + twitter_reader = TwitterMCPReader("test-server", include_metadata=True) + + high_engagement_bookmark = { + "text": "Thread about startup lessons learned 🧡", + "author": "startup_founder", + "likes": 1250, + "retweets": 340, + "replies": 89, + } + + formatted = twitter_reader._format_bookmark(high_engagement_bookmark) + assert "Thread about startup lessons learned" in formatted + assert "Likes: 1250" in formatted + assert "Retweets: 340" in formatted + assert "Replies: 89" in formatted + + # Test with metadata disabled + twitter_reader_no_meta = TwitterMCPReader("test-server", include_metadata=False) + formatted_no_meta = twitter_reader_no_meta._format_bookmark(high_engagement_bookmark) + assert "Thread about startup lessons learned" in formatted_no_meta + assert "Likes:" not in formatted_no_meta + assert "Retweets:" not in formatted_no_meta + + print("βœ… Data processing tests passed") + + +def main(): + """Run all standalone tests.""" + print("πŸ§ͺ Running MCP Integration Standalone Tests") + print("=" * 60) + print("Testing core functionality without LEANN dependencies...") + print() + + try: + test_slack_reader_basic() + test_twitter_reader_basic() + test_mcp_request_format() + test_data_processing() + + print("\n" + "=" * 60) + print("πŸŽ‰ All standalone tests passed!") + print("\n✨ MCP Integration Summary:") + print("- SlackMCPReader: Ready for Slack message processing") + print("- TwitterMCPReader: Ready for Twitter bookmark processing") + print("- MCP Protocol: Properly formatted JSON-RPC requests") + print("- Data Processing: Handles various message/bookmark formats") + + print("\nπŸš€ Next Steps:") + print("1. Install MCP servers: npm install -g slack-mcp-server twitter-mcp-server") + print("2. Configure API credentials for Slack and Twitter") + print("3. Test connections: python -m apps.slack_rag --test-connection") + print("4. Start indexing live data from your platforms!") + + print("\nπŸ“– Documentation:") + print("- Check README.md for detailed setup instructions") + print("- Run examples/mcp_integration_demo.py for usage examples") + print("- Explore apps/slack_rag.py and apps/twitter_rag.py for implementation details") + + except Exception as e: + print(f"\n❌ Test failed: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/test_metadata_filtering.py b/tests/test_metadata_filtering.py new file mode 100644 index 0000000..4996964 --- /dev/null +++ b/tests/test_metadata_filtering.py @@ -0,0 +1,449 @@ +""" +Comprehensive tests for metadata filtering functionality. + +This module tests the MetadataFilterEngine class and its integration +with the LEANN search system. +""" + +import os + +# Import the modules we're testing +import sys +from unittest.mock import Mock, patch + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../packages/leann-core/src")) + +from leann.api import PassageManager, SearchResult +from leann.metadata_filter import MetadataFilterEngine + + +class TestMetadataFilterEngine: + """Test suite for the MetadataFilterEngine class.""" + + def setup_method(self): + """Setup test fixtures.""" + self.engine = MetadataFilterEngine() + + # Sample search results for testing + self.sample_results = [ + { + "id": "doc1", + "score": 0.95, + "text": "This is chapter 1 content", + "metadata": { + "chapter": 1, + "character": "Alice", + "tags": ["adventure", "fantasy"], + "word_count": 150, + "is_published": True, + "genre": "fiction", + }, + }, + { + "id": "doc2", + "score": 0.87, + "text": "This is chapter 3 content", + "metadata": { + "chapter": 3, + "character": "Bob", + "tags": ["mystery", "thriller"], + "word_count": 250, + "is_published": True, + "genre": "fiction", + }, + }, + { + "id": "doc3", + "score": 0.82, + "text": "This is chapter 5 content", + "metadata": { + "chapter": 5, + "character": "Alice", + "tags": ["romance", "drama"], + "word_count": 300, + "is_published": False, + "genre": "non-fiction", + }, + }, + { + "id": "doc4", + "score": 0.78, + "text": "This is chapter 10 content", + "metadata": { + "chapter": 10, + "character": "Charlie", + "tags": ["action", "adventure"], + "word_count": 400, + "is_published": True, + "genre": "fiction", + }, + }, + ] + + def test_engine_initialization(self): + """Test that the filter engine initializes correctly.""" + assert self.engine is not None + assert len(self.engine.operators) > 0 + assert "==" in self.engine.operators + assert "contains" in self.engine.operators + assert "in" in self.engine.operators + + def test_direct_instantiation(self): + """Test direct instantiation of the engine.""" + engine = MetadataFilterEngine() + assert isinstance(engine, MetadataFilterEngine) + + def test_no_filters_returns_all_results(self): + """Test that passing None or empty filters returns all results.""" + # Test with None + result = self.engine.apply_filters(self.sample_results, None) + assert len(result) == len(self.sample_results) + + # Test with empty dict + result = self.engine.apply_filters(self.sample_results, {}) + assert len(result) == len(self.sample_results) + + # Test comparison operators + def test_equals_filter(self): + """Test equals (==) filter.""" + filters = {"chapter": {"==": 1}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 1 + assert result[0]["id"] == "doc1" + + def test_not_equals_filter(self): + """Test not equals (!=) filter.""" + filters = {"genre": {"!=": "fiction"}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 1 + assert result[0]["metadata"]["genre"] == "non-fiction" + + def test_less_than_filter(self): + """Test less than (<) filter.""" + filters = {"chapter": {"<": 5}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 2 + chapters = [r["metadata"]["chapter"] for r in result] + assert all(ch < 5 for ch in chapters) + + def test_less_than_or_equal_filter(self): + """Test less than or equal (<=) filter.""" + filters = {"chapter": {"<=": 5}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 3 + chapters = [r["metadata"]["chapter"] for r in result] + assert all(ch <= 5 for ch in chapters) + + def test_greater_than_filter(self): + """Test greater than (>) filter.""" + filters = {"word_count": {">": 200}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 3 # Documents with word_count 250, 300, 400 + word_counts = [r["metadata"]["word_count"] for r in result] + assert all(wc > 200 for wc in word_counts) + + def test_greater_than_or_equal_filter(self): + """Test greater than or equal (>=) filter.""" + filters = {"word_count": {">=": 250}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 3 + word_counts = [r["metadata"]["word_count"] for r in result] + assert all(wc >= 250 for wc in word_counts) + + # Test membership operators + def test_in_filter(self): + """Test in filter.""" + filters = {"character": {"in": ["Alice", "Bob"]}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 3 + characters = [r["metadata"]["character"] for r in result] + assert all(ch in ["Alice", "Bob"] for ch in characters) + + def test_not_in_filter(self): + """Test not_in filter.""" + filters = {"character": {"not_in": ["Alice", "Bob"]}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 1 + assert result[0]["metadata"]["character"] == "Charlie" + + # Test string operators + def test_contains_filter(self): + """Test contains filter.""" + filters = {"genre": {"contains": "fiction"}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 4 # Both "fiction" and "non-fiction" + + def test_starts_with_filter(self): + """Test starts_with filter.""" + filters = {"genre": {"starts_with": "non"}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 1 + assert result[0]["metadata"]["genre"] == "non-fiction" + + def test_ends_with_filter(self): + """Test ends_with filter.""" + filters = {"text": {"ends_with": "content"}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 4 # All sample texts end with "content" + + # Test boolean operators + def test_is_true_filter(self): + """Test is_true filter.""" + filters = {"is_published": {"is_true": True}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 3 + assert all(r["metadata"]["is_published"] for r in result) + + def test_is_false_filter(self): + """Test is_false filter.""" + filters = {"is_published": {"is_false": False}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 1 + assert not result[0]["metadata"]["is_published"] + + # Test compound filters (AND logic) + def test_compound_filters(self): + """Test multiple filters applied together (AND logic).""" + filters = {"genre": {"==": "fiction"}, "chapter": {"<=": 5}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 2 + for r in result: + assert r["metadata"]["genre"] == "fiction" + assert r["metadata"]["chapter"] <= 5 + + def test_multiple_operators_same_field(self): + """Test multiple operators on the same field.""" + filters = {"word_count": {">=": 200, "<=": 350}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 2 + for r in result: + wc = r["metadata"]["word_count"] + assert 200 <= wc <= 350 + + # Test edge cases + def test_missing_field_fails_filter(self): + """Test that missing metadata fields fail filters.""" + filters = {"nonexistent_field": {"==": "value"}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 0 + + def test_invalid_operator(self): + """Test that invalid operators are handled gracefully.""" + filters = {"chapter": {"invalid_op": 1}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 0 # Should filter out all results + + def test_type_coercion_numeric(self): + """Test numeric type coercion in comparisons.""" + # Add a result with string chapter number + test_results = [ + *self.sample_results, + { + "id": "doc5", + "score": 0.75, + "text": "String chapter test", + "metadata": {"chapter": "2", "genre": "test"}, + }, + ] + + filters = {"chapter": {"<": 3}} + result = self.engine.apply_filters(test_results, filters) + # Should include doc1 (chapter=1) and doc5 (chapter="2") + assert len(result) == 2 + ids = [r["id"] for r in result] + assert "doc1" in ids + assert "doc5" in ids + + def test_list_membership_with_nested_tags(self): + """Test membership operations with list metadata.""" + # Note: This tests the metadata structure, not list field filtering + # For list field filtering, we'd need to modify the test data + filters = {"character": {"in": ["Alice"]}} + result = self.engine.apply_filters(self.sample_results, filters) + assert len(result) == 2 + assert all(r["metadata"]["character"] == "Alice" for r in result) + + def test_empty_results_list(self): + """Test filtering on empty results list.""" + filters = {"chapter": {"==": 1}} + result = self.engine.apply_filters([], filters) + assert len(result) == 0 + + +class TestPassageManagerFiltering: + """Test suite for PassageManager filtering integration.""" + + def setup_method(self): + """Setup test fixtures.""" + # Mock the passage manager without actual file I/O + self.passage_manager = Mock(spec=PassageManager) + self.passage_manager.filter_engine = MetadataFilterEngine() + + # Sample SearchResult objects + self.search_results = [ + SearchResult( + id="doc1", + score=0.95, + text="Chapter 1 content", + metadata={"chapter": 1, "character": "Alice"}, + ), + SearchResult( + id="doc2", + score=0.87, + text="Chapter 5 content", + metadata={"chapter": 5, "character": "Bob"}, + ), + SearchResult( + id="doc3", + score=0.82, + text="Chapter 10 content", + metadata={"chapter": 10, "character": "Alice"}, + ), + ] + + def test_search_result_filtering(self): + """Test filtering SearchResult objects.""" + # Create a real PassageManager instance just for the filtering method + # We'll mock the file operations + with patch("builtins.open"), patch("json.loads"), patch("pickle.load"): + pm = PassageManager([{"type": "jsonl", "path": "test.jsonl"}]) + + filters = {"chapter": {"<=": 5}} + result = pm.filter_search_results(self.search_results, filters) + + assert len(result) == 2 + chapters = [r.metadata["chapter"] for r in result] + assert all(ch <= 5 for ch in chapters) + + def test_filter_search_results_no_filters(self): + """Test that None filters return all results.""" + with patch("builtins.open"), patch("json.loads"), patch("pickle.load"): + pm = PassageManager([{"type": "jsonl", "path": "test.jsonl"}]) + + result = pm.filter_search_results(self.search_results, None) + assert len(result) == len(self.search_results) + + def test_filter_maintains_search_result_type(self): + """Test that filtering returns SearchResult objects.""" + with patch("builtins.open"), patch("json.loads"), patch("pickle.load"): + pm = PassageManager([{"type": "jsonl", "path": "test.jsonl"}]) + + filters = {"character": {"==": "Alice"}} + result = pm.filter_search_results(self.search_results, filters) + + assert len(result) == 2 + for r in result: + assert isinstance(r, SearchResult) + assert r.metadata["character"] == "Alice" + + +# Integration tests would go here, but they require actual LEANN backend setup +# These would test the full pipeline from LeannSearcher.search() with metadata_filters + + +class TestCLIMetadataFilterParsing: + """Tests for CLI --metadata-filters argument parsing.""" + + def _parse_metadata_filters(self, raw_filters): + """Mirror the parsing logic from cli.py search_documents.""" + import json + + if not raw_filters: + return None + try: + parsed = json.loads(raw_filters) + if not isinstance(parsed, dict): + return "error: not a dict" + return parsed + except json.JSONDecodeError as e: + return f"error: {e}" + + def test_valid_equality_filter(self): + raw = '{"genre": {"==": "fiction"}}' + result = self._parse_metadata_filters(raw) + assert result == {"genre": {"==": "fiction"}} + + def test_valid_numeric_range_filter(self): + raw = '{"chapter": {"<=": 5}}' + result = self._parse_metadata_filters(raw) + assert result == {"chapter": {"<=": 5}} + + def test_valid_compound_filter(self): + raw = '{"chapter": {"<=": 5}, "genre": {"==": "fiction"}}' + result = self._parse_metadata_filters(raw) + assert result == {"chapter": {"<=": 5}, "genre": {"==": "fiction"}} + + def test_none_returns_none(self): + result = self._parse_metadata_filters(None) + assert result is None + + def test_empty_string_returns_none(self): + result = self._parse_metadata_filters("") + assert result is None + + def test_invalid_json_returns_error(self): + result = self._parse_metadata_filters("{not valid json}") + assert isinstance(result, str) and result.startswith("error:") + + def test_non_dict_returns_error(self): + result = self._parse_metadata_filters("[1, 2, 3]") + assert result == "error: not a dict" + + def test_filters_applied_to_results(self): + """Test that CLI-parsed filters actually work with MetadataFilterEngine.""" + import json + + engine = MetadataFilterEngine() + sample_results = [ + { + "id": "doc1", + "score": 0.9, + "text": "Chapter 1 content", + "metadata": {"chapter": 1, "genre": "fiction"}, + }, + { + "id": "doc2", + "score": 0.8, + "text": "Chapter 6 content", + "metadata": {"chapter": 6, "genre": "fiction"}, + }, + { + "id": "doc3", + "score": 0.7, + "text": "Chapter 3 non-fiction", + "metadata": {"chapter": 3, "genre": "non-fiction"}, + }, + ] + + # Parse filters as CLI would + raw = '{"chapter": {"<=": 5}, "genre": {"==": "fiction"}}' + filters = json.loads(raw) + + results = engine.apply_filters(sample_results, filters) + assert len(results) == 1 + assert results[0]["id"] == "doc1" + + +if __name__ == "__main__": + # Run basic smoke tests + engine = MetadataFilterEngine() + + sample_data = [ + { + "id": "test1", + "score": 0.9, + "text": "Test content", + "metadata": {"chapter": 1, "published": True}, + } + ] + + # Test basic filtering + result = engine.apply_filters(sample_data, {"chapter": {"==": 1}}) + assert len(result) == 1 + print("βœ… Basic filtering test passed") + + result = engine.apply_filters(sample_data, {"chapter": {"==": 2}}) + assert len(result) == 0 + print("βœ… No match filtering test passed") + + print("πŸŽ‰ All smoke tests passed!") diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py new file mode 100644 index 0000000..7a9eb76 --- /dev/null +++ b/tests/test_minimax_provider.py @@ -0,0 +1,231 @@ +""" +Tests for MiniMax provider integration. + +These tests validate MiniMax provider settings, chat class, and factory integration. +We import from leann.settings and leann.chat directly to avoid triggering +the full leann.__init__ import chain which requires C++ backend builds. +""" + +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest + +# Add the leann-core source to sys.path so we can import submodules +# without triggering __init__.py's backend imports. +_LEANN_SRC = os.path.join(os.path.dirname(__file__), "..", "packages", "leann-core", "src") +if _LEANN_SRC not in sys.path: + sys.path.insert(0, os.path.abspath(_LEANN_SRC)) + +# Prevent leann.__init__ from running its heavy imports by pre-registering +# a lightweight stub in sys.modules (if not already present). +if "leann" not in sys.modules: + import types + + _stub = types.ModuleType("leann") + _stub.__path__ = [os.path.join(os.path.abspath(_LEANN_SRC), "leann")] + sys.modules["leann"] = _stub + +# Now we can safely import the modules we actually test. +from leann.settings import ( # noqa: E402 + resolve_minimax_api_key, + resolve_minimax_base_url, +) + + +class TestMiniMaxSettings: + """Test MiniMax settings resolver functions.""" + + def test_resolve_minimax_api_key_explicit(self): + assert resolve_minimax_api_key("test-key") == "test-key" + + def test_resolve_minimax_api_key_from_env(self): + with patch.dict(os.environ, {"MINIMAX_API_KEY": "env-key"}): + assert resolve_minimax_api_key() == "env-key" + + def test_resolve_minimax_api_key_none(self): + with patch.dict(os.environ, {}, clear=True): + assert resolve_minimax_api_key() is None + + def test_resolve_minimax_base_url_default(self): + with patch.dict(os.environ, {}, clear=True): + assert resolve_minimax_base_url() == "https://api.minimax.io/v1" + + def test_resolve_minimax_base_url_explicit(self): + assert resolve_minimax_base_url("https://custom.url/v1") == "https://custom.url/v1" + + def test_resolve_minimax_base_url_from_env(self): + with patch.dict(os.environ, {"MINIMAX_BASE_URL": "https://env.url/v1"}): + assert resolve_minimax_base_url() == "https://env.url/v1" + + def test_resolve_minimax_base_url_leann_env(self): + with patch.dict(os.environ, {"LEANN_MINIMAX_BASE_URL": "https://leann.url/v1"}): + assert resolve_minimax_base_url() == "https://leann.url/v1" + + def test_resolve_minimax_base_url_strips_trailing_slash(self): + assert resolve_minimax_base_url("https://api.minimax.io/v1/") == "https://api.minimax.io/v1" + + +class TestMiniMaxChat: + """Test MiniMaxChat class.""" + + def test_init_requires_api_key(self): + from leann.chat import MiniMaxChat + + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="MiniMax API key is required"): + MiniMaxChat(api_key=None) + + @patch("openai.OpenAI") + def test_init_with_api_key(self, mock_openai_cls): + from leann.chat import MiniMaxChat + + chat = MiniMaxChat(api_key="test-key") + assert chat.model == "MiniMax-M2.5" + assert chat.api_key == "test-key" + assert chat.base_url == "https://api.minimax.io/v1" + mock_openai_cls.assert_called_once_with( + api_key="test-key", base_url="https://api.minimax.io/v1" + ) + + @patch("openai.OpenAI") + def test_init_custom_model(self, mock_openai_cls): + from leann.chat import MiniMaxChat + + chat = MiniMaxChat(model="MiniMax-M2.5-highspeed", api_key="test-key") + assert chat.model == "MiniMax-M2.5-highspeed" + + @patch("openai.OpenAI") + def test_init_custom_base_url(self, mock_openai_cls): + from leann.chat import MiniMaxChat + + chat = MiniMaxChat(api_key="test-key", base_url="https://api.minimaxi.com/v1") + assert chat.base_url == "https://api.minimaxi.com/v1" + + @patch("openai.OpenAI") + def test_ask_returns_response(self, mock_openai_cls): + from leann.chat import MiniMaxChat + + # Mock the response chain + mock_client = MagicMock() + mock_openai_cls.return_value = mock_client + + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "Hello from MiniMax!" + mock_response.choices[0].finish_reason = "stop" + mock_response.usage.total_tokens = 100 + mock_response.usage.prompt_tokens = 50 + mock_response.usage.completion_tokens = 50 + mock_client.chat.completions.create.return_value = mock_response + + chat = MiniMaxChat(api_key="test-key") + result = chat.ask("Hello") + + assert result == "Hello from MiniMax!" + mock_client.chat.completions.create.assert_called_once() + + @patch("openai.OpenAI") + def test_ask_with_kwargs(self, mock_openai_cls): + from leann.chat import MiniMaxChat + + mock_client = MagicMock() + mock_openai_cls.return_value = mock_client + + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "Response" + mock_response.choices[0].finish_reason = "stop" + mock_response.usage.total_tokens = 50 + mock_response.usage.prompt_tokens = 25 + mock_response.usage.completion_tokens = 25 + mock_client.chat.completions.create.return_value = mock_response + + chat = MiniMaxChat(api_key="test-key") + chat.ask("Hello", temperature=0.5, max_tokens=500, top_p=0.9) + + call_kwargs = mock_client.chat.completions.create.call_args[1] + assert call_kwargs["temperature"] == 0.5 + assert call_kwargs["max_tokens"] == 500 + assert call_kwargs["top_p"] == 0.9 + + @patch("openai.OpenAI") + def test_ask_handles_error(self, mock_openai_cls): + from leann.chat import MiniMaxChat + + mock_client = MagicMock() + mock_openai_cls.return_value = mock_client + mock_client.chat.completions.create.side_effect = Exception("API error") + + chat = MiniMaxChat(api_key="test-key") + result = chat.ask("Hello") + + assert "Error" in result + assert "MiniMax" in result + + +class TestGetLLMFactory: + """Test get_llm factory function with minimax type.""" + + @patch("openai.OpenAI") + def test_get_llm_minimax(self, mock_openai_cls): + from leann.chat import MiniMaxChat, get_llm + + llm = get_llm({"type": "minimax", "api_key": "test-key"}) + assert isinstance(llm, MiniMaxChat) + assert llm.model == "MiniMax-M2.5" + + @patch("openai.OpenAI") + def test_get_llm_minimax_custom_model(self, mock_openai_cls): + from leann.chat import MiniMaxChat, get_llm + + llm = get_llm({"type": "minimax", "model": "MiniMax-M2.5-highspeed", "api_key": "test-key"}) + assert isinstance(llm, MiniMaxChat) + assert llm.model == "MiniMax-M2.5-highspeed" + + @patch("openai.OpenAI") + def test_get_llm_minimax_custom_base_url(self, mock_openai_cls): + from leann.chat import MiniMaxChat, get_llm + + llm = get_llm( + { + "type": "minimax", + "api_key": "test-key", + "base_url": "https://api.minimaxi.com/v1", + } + ) + assert isinstance(llm, MiniMaxChat) + assert llm.base_url == "https://api.minimaxi.com/v1" + + +@pytest.mark.skipif( + not os.getenv("MINIMAX_API_KEY"), + reason="MINIMAX_API_KEY not set; skipping live API test", +) +class TestMiniMaxLiveAPI: + """Live API tests for MiniMax provider (requires MINIMAX_API_KEY).""" + + def test_minimax_m25_live(self): + from leann.chat import MiniMaxChat + + chat = MiniMaxChat(model="MiniMax-M2.5") + response = chat.ask("Say hello in one word.", max_tokens=10) + assert isinstance(response, str) + assert len(response) > 0 + + def test_minimax_m25_highspeed_live(self): + from leann.chat import MiniMaxChat + + chat = MiniMaxChat(model="MiniMax-M2.5-highspeed") + response = chat.ask("Say hello in one word.", max_tokens=10) + assert isinstance(response, str) + assert len(response) > 0 + + def test_minimax_via_get_llm_live(self): + from leann.chat import get_llm + + llm = get_llm({"type": "minimax"}) + response = llm.ask("What is 1+1? Reply with just the number.", max_tokens=10) + assert isinstance(response, str) + assert len(response) > 0 diff --git a/tests/test_novita_provider.py b/tests/test_novita_provider.py new file mode 100644 index 0000000..e7374de --- /dev/null +++ b/tests/test_novita_provider.py @@ -0,0 +1,246 @@ +""" +Tests for Novita AI provider integration. + +These tests validate Novita provider settings, chat class, and factory integration. +We import from leann.settings and leann.chat directly to avoid triggering +the full leann.__init__ import chain which requires C++ backend builds. +""" + +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest + +# Add the leann-core source to sys.path so we can import submodules +# without triggering __init__.py's backend imports. +_LEANN_SRC = os.path.join(os.path.dirname(__file__), "..", "packages", "leann-core", "src") +if _LEANN_SRC not in sys.path: + sys.path.insert(0, os.path.abspath(_LEANN_SRC)) + +# Prevent leann.__init__ from running its heavy imports by pre-registering +# a lightweight stub in sys.modules (if not already present). +if "leann" not in sys.modules: + import types + + _stub = types.ModuleType("leann") + _stub.__path__ = [os.path.join(os.path.abspath(_LEANN_SRC), "leann")] + sys.modules["leann"] = _stub + +# Now we can safely import the modules we actually test. +from leann.settings import ( # noqa: E402 + resolve_novita_api_key, + resolve_novita_base_url, +) + + +class TestNovitaSettings: + """Test Novita settings resolver functions.""" + + def test_resolve_novita_api_key_explicit(self): + assert resolve_novita_api_key("test-key") == "test-key" + + def test_resolve_novita_api_key_from_novita_env(self): + with patch.dict(os.environ, {"NOVITA_API_KEY": "novita-key"}, clear=True): + assert resolve_novita_api_key() == "novita-key" + + def test_resolve_novita_api_key_fallback_to_openai(self): + with patch.dict(os.environ, {"OPENAI_API_KEY": "openai-key"}, clear=True): + assert resolve_novita_api_key() == "openai-key" + + def test_resolve_novita_api_key_novita_takes_precedence(self): + with patch.dict( + os.environ, + {"NOVITA_API_KEY": "novita-key", "OPENAI_API_KEY": "openai-key"}, + ): + assert resolve_novita_api_key() == "novita-key" + + def test_resolve_novita_api_key_none(self): + with patch.dict(os.environ, {}, clear=True): + assert resolve_novita_api_key() is None + + def test_resolve_novita_base_url_default(self): + with patch.dict(os.environ, {}, clear=True): + assert resolve_novita_base_url() == "https://api.novita.ai/openai" + + def test_resolve_novita_base_url_explicit(self): + assert resolve_novita_base_url("https://custom.url/v1") == "https://custom.url/v1" + + def test_resolve_novita_base_url_from_novita_env(self): + with patch.dict(os.environ, {"NOVITA_BASE_URL": "https://env.url/v1"}): + assert resolve_novita_base_url() == "https://env.url/v1" + + def test_resolve_novita_base_url_from_leann_env(self): + with patch.dict(os.environ, {"LEANN_NOVITA_BASE_URL": "https://leann.url/v1"}): + assert resolve_novita_base_url() == "https://leann.url/v1" + + def test_resolve_novita_base_url_fallback_to_openai_env(self): + with patch.dict(os.environ, {"OPENAI_BASE_URL": "https://openai.url/v1"}, clear=True): + assert resolve_novita_base_url() == "https://openai.url/v1" + + def test_resolve_novita_base_url_strips_trailing_slash(self): + assert ( + resolve_novita_base_url("https://api.novita.ai/openai/") + == "https://api.novita.ai/openai" + ) + + +class TestNovitaChat: + """Test NovitaChat class.""" + + def test_init_requires_api_key(self): + from leann.chat import NovitaChat + + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="Novita API key is required"): + NovitaChat(api_key=None) + + @patch("openai.OpenAI") + def test_init_with_api_key(self, mock_openai_cls): + from leann.chat import NovitaChat + + chat = NovitaChat(api_key="test-key") + assert chat.model == "moonshotai/kimi-k2.5" + assert chat.api_key == "test-key" + assert chat.base_url == "https://api.novita.ai/openai" + mock_openai_cls.assert_called_once_with( + api_key="test-key", base_url="https://api.novita.ai/openai" + ) + + @patch("openai.OpenAI") + def test_init_custom_model(self, mock_openai_cls): + from leann.chat import NovitaChat + + chat = NovitaChat(model="zai-org/glm-5", api_key="test-key") + assert chat.model == "zai-org/glm-5" + + @patch("openai.OpenAI") + def test_init_custom_base_url(self, mock_openai_cls): + from leann.chat import NovitaChat + + chat = NovitaChat(api_key="test-key", base_url="https://custom.novita.url/v1") + assert chat.base_url == "https://custom.novita.url/v1" + + @patch("openai.OpenAI") + def test_ask_returns_response(self, mock_openai_cls): + from leann.chat import NovitaChat + + mock_client = MagicMock() + mock_openai_cls.return_value = mock_client + + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "Hello from Novita!" + mock_response.choices[0].finish_reason = "stop" + mock_response.usage.total_tokens = 100 + mock_response.usage.prompt_tokens = 50 + mock_response.usage.completion_tokens = 50 + mock_client.chat.completions.create.return_value = mock_response + + chat = NovitaChat(api_key="test-key") + result = chat.ask("Hello") + + assert result == "Hello from Novita!" + mock_client.chat.completions.create.assert_called_once() + + @patch("openai.OpenAI") + def test_ask_with_kwargs(self, mock_openai_cls): + from leann.chat import NovitaChat + + mock_client = MagicMock() + mock_openai_cls.return_value = mock_client + + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "Response" + mock_response.choices[0].finish_reason = "stop" + mock_response.usage.total_tokens = 50 + mock_response.usage.prompt_tokens = 25 + mock_response.usage.completion_tokens = 25 + mock_client.chat.completions.create.return_value = mock_response + + chat = NovitaChat(api_key="test-key") + chat.ask("Hello", temperature=0.5, max_tokens=500, top_p=0.9) + + call_kwargs = mock_client.chat.completions.create.call_args[1] + assert call_kwargs["temperature"] == 0.5 + assert call_kwargs["max_tokens"] == 500 + assert call_kwargs["top_p"] == 0.9 + + @patch("openai.OpenAI") + def test_ask_handles_error(self, mock_openai_cls): + from leann.chat import NovitaChat + + mock_client = MagicMock() + mock_openai_cls.return_value = mock_client + mock_client.chat.completions.create.side_effect = Exception("API error") + + chat = NovitaChat(api_key="test-key") + result = chat.ask("Hello") + + assert "Error" in result + assert "Novita" in result + + +class TestGetLLMFactory: + """Test get_llm factory function with novita type.""" + + @patch("openai.OpenAI") + def test_get_llm_novita(self, mock_openai_cls): + from leann.chat import NovitaChat, get_llm + + llm = get_llm({"type": "novita", "api_key": "test-key"}) + assert isinstance(llm, NovitaChat) + assert llm.model == "moonshotai/kimi-k2.5" + + @patch("openai.OpenAI") + def test_get_llm_novita_custom_model(self, mock_openai_cls): + from leann.chat import NovitaChat, get_llm + + llm = get_llm( + { + "type": "novita", + "model": "zai-org/glm-5", + "api_key": "test-key", + } + ) + assert isinstance(llm, NovitaChat) + assert llm.model == "zai-org/glm-5" + + @patch("openai.OpenAI") + def test_get_llm_novita_custom_base_url(self, mock_openai_cls): + from leann.chat import NovitaChat, get_llm + + llm = get_llm( + { + "type": "novita", + "api_key": "test-key", + "base_url": "https://custom.novita.url/v1", + } + ) + assert isinstance(llm, NovitaChat) + assert llm.base_url == "https://custom.novita.url/v1" + + +@pytest.mark.skipif( + not os.getenv("NOVITA_API_KEY"), + reason="NOVITA_API_KEY not set; skipping live API test", +) +class TestNovitaLiveAPI: + """Live API tests for Novita provider (requires NOVITA_API_KEY).""" + + def test_novita_kimi_k25_live(self): + from leann.chat import NovitaChat + + chat = NovitaChat(model="moonshotai/kimi-k2.5") + response = chat.ask("Say hello in one word.", max_tokens=10) + assert isinstance(response, str) + assert len(response) > 0 + + def test_novita_via_get_llm_live(self): + from leann.chat import get_llm + + llm = get_llm({"type": "novita"}) + response = llm.ask("What is 1+1? Reply with just the number.", max_tokens=10) + assert isinstance(response, str) + assert len(response) > 0 diff --git a/tests/test_passage_id_scheme.py b/tests/test_passage_id_scheme.py new file mode 100644 index 0000000..e83c7ac --- /dev/null +++ b/tests/test_passage_id_scheme.py @@ -0,0 +1,117 @@ +import hashlib +import json +import pickle +from types import SimpleNamespace + +from leann.api import Fts5BM25Index, LeannBuilder +from leann.cli import LeannCLI + + +def _content_id(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16] + + +def test_builder_content_hash_passage_ids_are_content_stable(): + builder = LeannBuilder(backend_name="hnsw", passage_id_scheme="content-hash") + + builder.add_text("same text", metadata={"source": "a.txt"}) + builder.add_text("same text", metadata={"source": "b.txt"}) + builder.add_text("different text", metadata={"source": "c.txt"}) + + same_id = _content_id("same text") + assert builder.chunks[0]["id"] == same_id + assert builder.chunks[1]["id"] == same_id + assert builder.chunks[2]["id"] == _content_id("different text") + + +def test_existing_index_id_scheme_treats_legacy_meta_as_sequential(tmp_path): + cli = LeannCLI() + index_path = tmp_path / "legacy.leann" + + assert cli._existing_index_id_scheme(str(index_path)) is None + + meta_path = tmp_path / "legacy.leann.meta.json" + meta_path.write_text(json.dumps({"version": "1.0", "backend_name": "hnsw"}), encoding="utf-8") + + assert cli._existing_index_id_scheme(str(index_path)) == "sequential" + + meta_path.write_text( + json.dumps({"version": "1.1", "backend_name": "hnsw", "passage_id_scheme": "content-hash"}), + encoding="utf-8", + ) + + assert cli._existing_index_id_scheme(str(index_path)) == "content-hash" + + +def test_migrate_ids_rewrites_passages_offsets_idmap_meta_and_bm25(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + index_dir = tmp_path / ".leann" / "indexes" / "sample" + index_dir.mkdir(parents=True) + + passages = [ + {"id": "0", "text": "alpha beta", "metadata": {"source": "a.txt"}}, + {"id": "1", "text": "gamma delta", "metadata": {"source": "b.txt"}}, + ] + passages_file = index_dir / "documents.leann.passages.jsonl" + offsets = {} + with open(passages_file, "w", encoding="utf-8") as f: + for passage in passages: + offsets[passage["id"]] = f.tell() + json.dump(passage, f) + f.write("\n") + + offset_file = index_dir / "documents.leann.passages.idx" + with open(offset_file, "wb") as f: + pickle.dump(offsets, f) + + idmap_file = index_dir / "documents.ids.txt" + idmap_file.write_text("0\n1\n", encoding="utf-8") + + bm25_db = index_dir / "documents.leann.bm25.sqlite" + bm25 = Fts5BM25Index(str(bm25_db)) + bm25.fit(passages) + bm25.close() + + meta_path = index_dir / "documents.leann.meta.json" + meta_path.write_text( + json.dumps( + { + "version": "1.0", + "backend_name": "hnsw", + "embedding_model": "dummy", + "dimensions": 3, + "backend_kwargs": {}, + "embedding_mode": "sentence-transformers", + "bm25_backend": "fts5", + "bm25_db": "documents.leann.bm25.sqlite", + "passage_id_scheme": "sequential", + } + ), + encoding="utf-8", + ) + + cli = LeannCLI() + cli.migrate_ids(SimpleNamespace(index_name="sample", dry_run=False, yes=True)) + + expected_ids = [_content_id("alpha beta"), _content_id("gamma delta")] + with open(passages_file, encoding="utf-8") as f: + migrated_passages = [json.loads(line) for line in f if line.strip()] + assert [p["id"] for p in migrated_passages] == expected_ids + + with open(offset_file, "rb") as f: + migrated_offsets = pickle.load(f) + assert set(migrated_offsets) == set(expected_ids) + assert idmap_file.read_text(encoding="utf-8").splitlines() == expected_ids + + meta = json.loads(meta_path.read_text(encoding="utf-8")) + assert meta["version"] == "1.1" + assert meta["passage_id_scheme"] == "content-hash" + assert meta["bm25_backend"] == "fts5" + assert meta["bm25_db"] == "documents.leann.bm25.sqlite" + + migrated_bm25 = Fts5BM25Index(str(bm25_db)) + try: + bm25_results = migrated_bm25.search("alpha", top_k=1) + finally: + migrated_bm25.close() + assert [result.id for result in bm25_results] == [expected_ids[0]] diff --git a/tests/test_prompt_template_e2e.py b/tests/test_prompt_template_e2e.py new file mode 100644 index 0000000..6cb40b1 --- /dev/null +++ b/tests/test_prompt_template_e2e.py @@ -0,0 +1,403 @@ +"""End-to-end integration tests for prompt template and token limit features. + +These tests verify real-world functionality with live services: +- OpenAI-compatible APIs (OpenAI, LM Studio) with prompt template support +- Ollama with dynamic token limit detection +- Hybrid token limit discovery mechanism + +Run with: pytest tests/test_prompt_template_e2e.py -v -s +Skip if services unavailable: pytest tests/test_prompt_template_e2e.py -m "not integration" + +Prerequisites: +1. LM Studio running with embedding model: http://localhost:1234 +2. [Optional] Ollama running: ollama serve +3. [Optional] Ollama model: ollama pull nomic-embed-text +4. [Optional] Node.js + @lmstudio/sdk for context length detection +""" + +import logging +import socket + +import numpy as np +import pytest +import requests +from leann.embedding_compute import ( + compute_embeddings_ollama, + compute_embeddings_openai, + get_model_token_limit, +) + +# Test markers for conditional execution +pytestmark = pytest.mark.integration + +logger = logging.getLogger(__name__) + + +def check_service_available(host: str, port: int, timeout: float = 2.0) -> bool: + """Check if a service is available on the given host:port.""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + result = sock.connect_ex((host, port)) + sock.close() + return result == 0 + except Exception: + return False + + +def check_ollama_available() -> bool: + """Check if Ollama service is available.""" + if not check_service_available("localhost", 11434): + return False + try: + response = requests.get("http://localhost:11434/api/tags", timeout=2.0) + return response.status_code == 200 + except Exception: + return False + + +def check_lmstudio_available() -> bool: + """Check if LM Studio service is available.""" + if not check_service_available("localhost", 1234): + return False + try: + response = requests.get("http://localhost:1234/v1/models", timeout=2.0) + return response.status_code == 200 + except Exception: + return False + + +def get_lmstudio_first_model() -> str | None: + """Get the first available model from LM Studio.""" + try: + response = requests.get("http://localhost:1234/v1/models", timeout=5.0) + data = response.json() + models = data.get("data", []) + if models: + return models[0]["id"] + except Exception: + pass + return None + + +class TestPromptTemplateOpenAI: + """End-to-end tests for prompt template with OpenAI-compatible APIs (LM Studio).""" + + @pytest.mark.skipif( + not check_lmstudio_available(), reason="LM Studio service not available on localhost:1234" + ) + def test_lmstudio_embedding_with_prompt_template(self): + """Test prompt templates with LM Studio using OpenAI-compatible API.""" + model_name = get_lmstudio_first_model() + if not model_name: + pytest.skip("No models loaded in LM Studio") + assert model_name is not None # Type narrowing for type checker + + texts = ["artificial intelligence", "machine learning"] + prompt_template = "search_query: " + + # Get embeddings with prompt template via provider_options + provider_options = {"prompt_template": prompt_template} + embeddings = compute_embeddings_openai( + texts=texts, + model_name=model_name, + base_url="http://localhost:1234/v1", + api_key="lm-studio", # LM Studio doesn't require real key + provider_options=provider_options, + ) + + assert embeddings is not None + assert len(embeddings) == 2 + assert all(isinstance(emb, np.ndarray) for emb in embeddings) + assert all(len(emb) > 0 for emb in embeddings) + + logger.info( + f"βœ“ LM Studio embeddings with prompt template: {len(embeddings)} vectors, {len(embeddings[0])} dimensions" + ) + + @pytest.mark.skipif(not check_lmstudio_available(), reason="LM Studio service not available") + def test_lmstudio_prompt_template_affects_embeddings(self): + """Verify that prompt templates actually change embedding values.""" + model_name = get_lmstudio_first_model() + if not model_name: + pytest.skip("No models loaded in LM Studio") + assert model_name is not None # Type narrowing for type checker + + text = "machine learning" + base_url = "http://localhost:1234/v1" + api_key = "lm-studio" + + # Get embeddings without template + embeddings_no_template = compute_embeddings_openai( + texts=[text], + model_name=model_name, + base_url=base_url, + api_key=api_key, + provider_options={}, + ) + + # Get embeddings with template + embeddings_with_template = compute_embeddings_openai( + texts=[text], + model_name=model_name, + base_url=base_url, + api_key=api_key, + provider_options={"prompt_template": "search_query: "}, + ) + + # Embeddings should be different when template is applied + assert not np.allclose(embeddings_no_template[0], embeddings_with_template[0]) + + logger.info("βœ“ Prompt template changes embedding values as expected") + + +class TestPromptTemplateOllama: + """End-to-end tests for prompt template with Ollama.""" + + @pytest.mark.skipif( + not check_ollama_available(), reason="Ollama service not available on localhost:11434" + ) + def test_ollama_embedding_with_prompt_template(self): + """Test prompt templates with Ollama using any available embedding model.""" + # Get any available embedding model + try: + response = requests.get("http://localhost:11434/api/tags", timeout=2.0) + models = response.json().get("models", []) + + embedding_models = [] + for model in models: + name = model["name"] + base_name = name.split(":")[0] + if any(emb in base_name for emb in ["embed", "bge", "minilm", "e5", "nomic"]): + embedding_models.append(name) + + if not embedding_models: + pytest.skip("No embedding models available in Ollama") + + model_name = embedding_models[0] + + texts = ["artificial intelligence", "machine learning"] + prompt_template = "search_query: " + + # Get embeddings with prompt template via provider_options + provider_options = {"prompt_template": prompt_template} + embeddings = compute_embeddings_ollama( + texts=texts, + model_name=model_name, + is_build=False, + host="http://localhost:11434", + provider_options=provider_options, + ) + + assert embeddings is not None + assert len(embeddings) == 2 + assert all(isinstance(emb, np.ndarray) for emb in embeddings) + assert all(len(emb) > 0 for emb in embeddings) + + logger.info( + f"βœ“ Ollama embeddings with prompt template: {len(embeddings)} vectors, {len(embeddings[0])} dimensions" + ) + + except Exception as e: + pytest.skip(f"Could not test Ollama prompt template: {e}") + + @pytest.mark.skipif(not check_ollama_available(), reason="Ollama service not available") + def test_ollama_prompt_template_affects_embeddings(self): + """Verify that prompt templates actually change embedding values with Ollama.""" + # Get any available embedding model + try: + response = requests.get("http://localhost:11434/api/tags", timeout=2.0) + models = response.json().get("models", []) + + embedding_models = [] + for model in models: + name = model["name"] + base_name = name.split(":")[0] + if any(emb in base_name for emb in ["embed", "bge", "minilm", "e5", "nomic"]): + embedding_models.append(name) + + if not embedding_models: + pytest.skip("No embedding models available in Ollama") + + model_name = embedding_models[0] + text = "machine learning" + host = "http://localhost:11434" + + # Get embeddings without template + embeddings_no_template = compute_embeddings_ollama( + texts=[text], model_name=model_name, is_build=False, host=host, provider_options={} + ) + + # Get embeddings with template + embeddings_with_template = compute_embeddings_ollama( + texts=[text], + model_name=model_name, + is_build=False, + host=host, + provider_options={"prompt_template": "search_query: "}, + ) + + # Embeddings should be different when template is applied + assert not np.allclose(embeddings_no_template[0], embeddings_with_template[0]) + + logger.info("βœ“ Ollama prompt template changes embedding values as expected") + + except Exception as e: + pytest.skip(f"Could not test Ollama prompt template: {e}") + + +class TestLMStudioSDK: + """End-to-end tests for LM Studio SDK integration.""" + + @pytest.mark.skipif(not check_lmstudio_available(), reason="LM Studio service not available") + def test_lmstudio_model_listing(self): + """Test that we can list models from LM Studio.""" + try: + response = requests.get("http://localhost:1234/v1/models", timeout=5.0) + assert response.status_code == 200 + + data = response.json() + assert "data" in data + + models = data["data"] + logger.info(f"βœ“ LM Studio models available: {len(models)}") + + if models: + logger.info(f" First model: {models[0].get('id', 'unknown')}") + except Exception as e: + pytest.skip(f"LM Studio API error: {e}") + + @pytest.mark.skipif(not check_lmstudio_available(), reason="LM Studio service not available") + def test_lmstudio_sdk_context_length_detection(self): + """Test context length detection via LM Studio SDK bridge (requires Node.js + SDK).""" + model_name = get_lmstudio_first_model() + if not model_name: + pytest.skip("No models loaded in LM Studio") + assert model_name is not None # Type narrowing for type checker + + try: + from leann.embedding_compute import _query_lmstudio_context_limit + + # SDK requires WebSocket URL (ws://) + context_length = _query_lmstudio_context_limit( + model_name=model_name, base_url="ws://localhost:1234" + ) + + if context_length is None: + logger.warning( + "⚠ LM Studio SDK bridge returned None (Node.js or SDK may not be available)" + ) + pytest.skip("Node.js or @lmstudio/sdk not available - SDK bridge unavailable") + else: + assert context_length > 0 + logger.info( + f"βœ“ LM Studio context length detected via SDK: {context_length} for {model_name}" + ) + + except ImportError: + pytest.skip("_query_lmstudio_context_limit not implemented yet") + except Exception as e: + logger.error(f"LM Studio SDK test error: {e}") + raise + + +class TestOllamaTokenLimit: + """End-to-end tests for Ollama token limit discovery.""" + + @pytest.mark.skipif(not check_ollama_available(), reason="Ollama service not available") + def test_ollama_token_limit_detection(self): + """Test dynamic token limit detection from Ollama /api/show endpoint.""" + # Get any available embedding model + try: + response = requests.get("http://localhost:11434/api/tags", timeout=2.0) + models = response.json().get("models", []) + + embedding_models = [] + for model in models: + name = model["name"] + base_name = name.split(":")[0] + if any(emb in base_name for emb in ["embed", "bge", "minilm", "e5", "nomic"]): + embedding_models.append(name) + + if not embedding_models: + pytest.skip("No embedding models available in Ollama") + + test_model = embedding_models[0] + + # Test token limit detection + limit = get_model_token_limit(model_name=test_model, base_url="http://localhost:11434") + + assert limit > 0 + logger.info(f"βœ“ Ollama token limit detected: {limit} for {test_model}") + + except Exception as e: + pytest.skip(f"Could not test Ollama token detection: {e}") + + +class TestHybridTokenLimit: + """End-to-end tests for hybrid token limit discovery mechanism.""" + + def test_hybrid_discovery_registry_fallback(self): + """Test fallback to static registry for known OpenAI models.""" + # Use a known OpenAI model (should be in registry) + limit = get_model_token_limit( + model_name="text-embedding-3-small", + base_url="http://fake-server:9999", # Fake URL to force registry lookup + ) + + # text-embedding-3-small should have 8192 in registry + assert limit == 8192 + logger.info(f"βœ“ Hybrid discovery (registry fallback): {limit} tokens") + + def test_hybrid_discovery_default_fallback(self): + """Test fallback to safe default for completely unknown models.""" + limit = get_model_token_limit( + model_name="completely-unknown-model-xyz-12345", + base_url="http://fake-server:9999", + default=512, + ) + + # Should get the specified default + assert limit == 512 + logger.info(f"βœ“ Hybrid discovery (default fallback): {limit} tokens") + + @pytest.mark.skipif(not check_ollama_available(), reason="Ollama service not available") + def test_hybrid_discovery_ollama_dynamic_first(self): + """Test that Ollama models use dynamic discovery first.""" + # Get any available embedding model + try: + response = requests.get("http://localhost:11434/api/tags", timeout=2.0) + models = response.json().get("models", []) + + embedding_models = [] + for model in models: + name = model["name"] + base_name = name.split(":")[0] + if any(emb in base_name for emb in ["embed", "bge", "minilm", "e5", "nomic"]): + embedding_models.append(name) + + if not embedding_models: + pytest.skip("No embedding models available in Ollama") + + test_model = embedding_models[0] + + # Should query Ollama /api/show dynamically + limit = get_model_token_limit(model_name=test_model, base_url="http://localhost:11434") + + assert limit > 0 + logger.info(f"βœ“ Hybrid discovery (Ollama dynamic): {limit} tokens for {test_model}") + + except Exception as e: + pytest.skip(f"Could not test hybrid Ollama discovery: {e}") + + +if __name__ == "__main__": + print("\n" + "=" * 70) + print("INTEGRATION TEST SUITE - Real Service Testing") + print("=" * 70) + print("\nThese tests require live services:") + print(" β€’ LM Studio: http://localhost:1234 (with embedding model loaded)") + print(" β€’ [Optional] Ollama: http://localhost:11434") + print(" β€’ [Optional] Node.js + @lmstudio/sdk for SDK bridge tests") + print("\nRun with: pytest tests/test_prompt_template_e2e.py -v -s") + print("=" * 70 + "\n") diff --git a/tests/test_prompt_template_persistence.py b/tests/test_prompt_template_persistence.py new file mode 100644 index 0000000..4c61a8c --- /dev/null +++ b/tests/test_prompt_template_persistence.py @@ -0,0 +1,878 @@ +""" +Integration tests for prompt template metadata persistence and reuse. + +These tests verify the complete lifecycle of prompt template persistence: +1. Template is saved to .meta.json during index build +2. Template is automatically loaded during search operations +3. Template can be overridden with explicit flag during search +4. Template is reused during chat/ask operations + +These are integration tests that: +- Use real file system with temporary directories +- Run actual build and search operations +- Inspect .meta.json file contents directly +- Mock embedding servers to avoid external dependencies +- Use small test codebases for fast execution + +Expected to FAIL in Red Phase because metadata persistence verification is not yet implemented. +""" + +import json +import tempfile +from pathlib import Path +from unittest.mock import Mock, patch + +import numpy as np +import pytest +from leann.api import LeannBuilder, LeannSearcher + + +class TestPromptTemplateMetadataPersistence: + """Tests for prompt template storage in .meta.json during build.""" + + @pytest.fixture + def temp_index_dir(self): + """Create temporary directory for test indexes.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: + yield Path(tmpdir) + + @pytest.fixture + def mock_embeddings(self): + """Mock compute_embeddings to return dummy embeddings.""" + with patch("leann.api.compute_embeddings") as mock_compute: + # Return dummy embeddings as numpy array + mock_compute.return_value = np.array([[0.1, 0.2, 0.3]], dtype=np.float32) + yield mock_compute + + def test_prompt_template_saved_to_metadata(self, temp_index_dir, mock_embeddings): + """ + Verify that when build is run with embedding_options containing prompt_template, + the template value is saved to .meta.json file. + + This is the core persistence requirement - templates must be saved to allow + reuse in subsequent search operations without re-specifying the flag. + + Expected failure: .meta.json exists but doesn't contain embedding_options + with prompt_template, or the value is not persisted correctly. + """ + # Setup test data + index_path = temp_index_dir / "test_index.leann" + template = "search_document: " + + # Build index with prompt template in embedding_options + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="text-embedding-3-small", + embedding_mode="openai", + embedding_options={"prompt_template": template}, + ) + + # Add a simple document + builder.add_text("This is a test document for indexing") + + # Build the index + builder.build_index(str(index_path)) + + # Verify .meta.json was created and contains the template + meta_path = temp_index_dir / "test_index.leann.meta.json" + assert meta_path.exists(), ".meta.json file should be created during build" + + # Read and parse metadata + with open(meta_path, encoding="utf-8") as f: + meta_data = json.load(f) + + # Verify embedding_options exists in metadata + assert "embedding_options" in meta_data, ( + "embedding_options should be saved to .meta.json when provided" + ) + + # Verify prompt_template is in embedding_options + embedding_options = meta_data["embedding_options"] + assert "prompt_template" in embedding_options, ( + "prompt_template should be saved within embedding_options" + ) + + # Verify the template value matches what we provided + assert embedding_options["prompt_template"] == template, ( + f"Template should be '{template}', got '{embedding_options.get('prompt_template')}'" + ) + + def test_prompt_template_absent_when_not_provided(self, temp_index_dir, mock_embeddings): + """ + Verify that when no prompt template is provided during build, + .meta.json either doesn't have embedding_options or prompt_template key. + + This ensures clean metadata without unnecessary keys when features aren't used. + + Expected behavior: Build succeeds, .meta.json doesn't contain prompt_template. + """ + index_path = temp_index_dir / "test_no_template.leann" + + # Build index WITHOUT prompt template + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="text-embedding-3-small", + embedding_mode="openai", + # No embedding_options provided + ) + + builder.add_text("Document without template") + builder.build_index(str(index_path)) + + # Verify metadata + meta_path = temp_index_dir / "test_no_template.leann.meta.json" + assert meta_path.exists() + + with open(meta_path, encoding="utf-8") as f: + meta_data = json.load(f) + + # If embedding_options exists, it should not contain prompt_template + if "embedding_options" in meta_data: + embedding_options = meta_data["embedding_options"] + assert "prompt_template" not in embedding_options, ( + "prompt_template should not be in metadata when not provided" + ) + + +class TestPromptTemplateAutoLoadOnSearch: + """Tests for automatic loading of prompt template during search operations. + + NOTE: Over-mocked test removed (test_prompt_template_auto_loaded_on_search). + This functionality is now comprehensively tested by TestQueryPromptTemplateAutoLoad + which uses simpler mocking and doesn't hang. + """ + + @pytest.fixture + def temp_index_dir(self): + """Create temporary directory for test indexes.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: + yield Path(tmpdir) + + @pytest.fixture + def mock_embeddings(self): + """Mock compute_embeddings to capture calls and return dummy embeddings.""" + with patch("leann.api.compute_embeddings") as mock_compute: + mock_compute.return_value = np.array([[0.1, 0.2, 0.3]], dtype=np.float32) + yield mock_compute + + def test_search_without_template_in_metadata(self, temp_index_dir, mock_embeddings): + """ + Verify that searching an index built WITHOUT a prompt template + works correctly (backward compatibility). + + The searcher should handle missing prompt_template gracefully. + + Expected behavior: Search succeeds, no template is used. + """ + # Build index without template + index_path = temp_index_dir / "no_template.leann" + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="text-embedding-3-small", + embedding_mode="openai", + ) + builder.add_text("Document without template") + builder.build_index(str(index_path)) + + # Reset mocks + mock_embeddings.reset_mock() + + # Create searcher and search + searcher = LeannSearcher(index_path=str(index_path)) + + # Verify no template in embedding_options + assert "prompt_template" not in searcher.embedding_options, ( + "Searcher should not have prompt_template when not in metadata" + ) + + +class TestQueryPromptTemplateAutoLoad: + """Tests for automatic loading of separate query_prompt_template during search (R2). + + These tests verify the new two-template system where: + - build_prompt_template: Applied during index building + - query_prompt_template: Applied during search operations + + Expected to FAIL in Red Phase (R2) because query template extraction + and application is not yet implemented in LeannSearcher.search(). + """ + + @pytest.fixture + def temp_index_dir(self): + """Create temporary directory for test indexes.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: + yield Path(tmpdir) + + @pytest.fixture + def mock_compute_embeddings(self): + """Mock compute_embeddings to capture calls and return dummy embeddings.""" + with patch("leann.embedding_compute.compute_embeddings") as mock_compute: + mock_compute.return_value = np.array([[0.1, 0.2, 0.3]], dtype=np.float32) + yield mock_compute + + def test_search_auto_loads_query_template(self, temp_index_dir, mock_compute_embeddings): + """ + Verify that search() automatically loads and applies query_prompt_template from .meta.json. + + Given: Index built with separate build_prompt_template and query_prompt_template + When: LeannSearcher.search("my query") is called + Then: Query embedding is computed with "query: my query" (query template applied) + + This is the core R2 requirement - query templates must be auto-loaded and applied + during search without user intervention. + + Expected failure: compute_embeddings called with raw "my query" instead of + "query: my query" because query template extraction is not implemented. + """ + # Setup: Build index with separate templates in new format + index_path = temp_index_dir / "query_template.leann" + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="text-embedding-3-small", + embedding_mode="openai", + embedding_options={ + "build_prompt_template": "doc: ", + "query_prompt_template": "query: ", + }, + ) + builder.add_text("Test document") + builder.build_index(str(index_path)) + + # Reset mock to ignore build calls + mock_compute_embeddings.reset_mock() + + # Act: Search with query + searcher = LeannSearcher(index_path=str(index_path)) + + # Mock the backend search to avoid actual search + with patch.object(searcher.backend_impl, "search") as mock_backend_search: + mock_backend_search.return_value = { + "labels": [["test_id_0"]], # IDs (nested list for batch support) + "distances": [[0.9]], # Distances (nested list for batch support) + } + + searcher.search("my query", top_k=1, recompute_embeddings=False) + + # Assert: compute_embeddings was called with query template applied + assert mock_compute_embeddings.called, "compute_embeddings should be called during search" + + # Get the actual text passed to compute_embeddings + call_args = mock_compute_embeddings.call_args + texts_arg = call_args[0][0] # First positional arg (list of texts) + + assert len(texts_arg) == 1, "Should compute embedding for one query" + assert texts_arg[0] == "query: my query", ( + f"Query template should be applied: expected 'query: my query', got '{texts_arg[0]}'" + ) + + def test_search_backward_compat_single_template(self, temp_index_dir, mock_compute_embeddings): + """ + Verify backward compatibility with old single prompt_template format. + + Given: Index with old format (single prompt_template, no query_prompt_template) + When: LeannSearcher.search("my query") is called + Then: Query embedding computed with "doc: my query" (old template applied) + + This ensures indexes built with the old single-template system continue + to work correctly with the new search implementation. + + Expected failure: Old template not recognized/applied because backward + compatibility logic is not implemented. + """ + # Setup: Build index with old single-template format + index_path = temp_index_dir / "old_template.leann" + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="text-embedding-3-small", + embedding_mode="openai", + embedding_options={"prompt_template": "doc: "}, # Old format + ) + builder.add_text("Test document") + builder.build_index(str(index_path)) + + # Reset mock + mock_compute_embeddings.reset_mock() + + # Act: Search + searcher = LeannSearcher(index_path=str(index_path)) + + with patch.object(searcher.backend_impl, "search") as mock_backend_search: + mock_backend_search.return_value = {"labels": [["test_id_0"]], "distances": [[0.9]]} + + searcher.search("my query", top_k=1, recompute_embeddings=False) + + # Assert: Old template was applied + call_args = mock_compute_embeddings.call_args + texts_arg = call_args[0][0] + + assert texts_arg[0] == "doc: my query", ( + f"Old prompt_template should be applied for backward compatibility: " + f"expected 'doc: my query', got '{texts_arg[0]}'" + ) + + def test_search_backward_compat_no_template(self, temp_index_dir, mock_compute_embeddings): + """ + Verify backward compatibility when no template is present in .meta.json. + + Given: Index with no template in .meta.json (very old indexes) + When: LeannSearcher.search("my query") is called + Then: Query embedding computed with "my query" (no template, raw query) + + This ensures the most basic backward compatibility - indexes without + any template support continue to work as before. + + Expected failure: May fail if default template is incorrectly applied, + or if missing template causes error. + """ + # Setup: Build index without any template + index_path = temp_index_dir / "no_template.leann" + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="text-embedding-3-small", + embedding_mode="openai", + # No embedding_options at all + ) + builder.add_text("Test document") + builder.build_index(str(index_path)) + + # Reset mock + mock_compute_embeddings.reset_mock() + + # Act: Search + searcher = LeannSearcher(index_path=str(index_path)) + + with patch.object(searcher.backend_impl, "search") as mock_backend_search: + mock_backend_search.return_value = {"labels": [["test_id_0"]], "distances": [[0.9]]} + + searcher.search("my query", top_k=1, recompute_embeddings=False) + + # Assert: No template applied (raw query) + call_args = mock_compute_embeddings.call_args + texts_arg = call_args[0][0] + + assert texts_arg[0] == "my query", ( + f"No template should be applied when missing from metadata: " + f"expected 'my query', got '{texts_arg[0]}'" + ) + + def test_search_override_via_provider_options(self, temp_index_dir, mock_compute_embeddings): + """ + Verify that explicit provider_options can override stored query template. + + Given: Index with query_prompt_template: "query: " + When: search() called with provider_options={"prompt_template": "override: "} + Then: Query embedding computed with "override: test" (override takes precedence) + + This enables users to experiment with different query templates without + rebuilding the index, or to handle special query types differently. + + Expected failure: provider_options parameter is accepted via **kwargs but + not used. Query embedding computed with raw "test" instead of "override: test" + because override logic is not implemented. + """ + # Setup: Build index with query template + index_path = temp_index_dir / "override_template.leann" + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="text-embedding-3-small", + embedding_mode="openai", + embedding_options={ + "build_prompt_template": "doc: ", + "query_prompt_template": "query: ", + }, + ) + builder.add_text("Test document") + builder.build_index(str(index_path)) + + # Reset mock + mock_compute_embeddings.reset_mock() + + # Act: Search with override + searcher = LeannSearcher(index_path=str(index_path)) + + with patch.object(searcher.backend_impl, "search") as mock_backend_search: + mock_backend_search.return_value = {"labels": [["test_id_0"]], "distances": [[0.9]]} + + # This should accept provider_options parameter + searcher.search( + "test", + top_k=1, + recompute_embeddings=False, + provider_options={"prompt_template": "override: "}, + ) + + # Assert: Override template was applied + call_args = mock_compute_embeddings.call_args + texts_arg = call_args[0][0] + + assert texts_arg[0] == "override: test", ( + f"Override template should take precedence: " + f"expected 'override: test', got '{texts_arg[0]}'" + ) + + +class TestPromptTemplateReuseInChat: + """Tests for prompt template reuse in chat/ask operations.""" + + @pytest.fixture + def temp_index_dir(self): + """Create temporary directory for test indexes.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: + yield Path(tmpdir) + + @pytest.fixture + def mock_embeddings(self): + """Mock compute_embeddings to return dummy embeddings.""" + with patch("leann.api.compute_embeddings") as mock_compute: + mock_compute.return_value = np.array([[0.1, 0.2, 0.3]], dtype=np.float32) + yield mock_compute + + @pytest.fixture + def mock_embedding_server_manager(self): + """Mock EmbeddingServerManager for chat tests.""" + with patch("leann.searcher_base.EmbeddingServerManager") as mock_manager_class: + mock_manager = Mock() + mock_manager.start_server.return_value = (True, 5557) + mock_manager_class.return_value = mock_manager + yield mock_manager + + @pytest.fixture + def index_with_template(self, temp_index_dir, mock_embeddings): + """Build an index with a prompt template.""" + index_path = temp_index_dir / "chat_template_index.leann" + template = "document_query: " + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model="text-embedding-3-small", + embedding_mode="openai", + embedding_options={"prompt_template": template}, + ) + + builder.add_text("Test document for chat") + builder.build_index(str(index_path)) + + return str(index_path), template + + +class TestPromptTemplateIntegrationWithEmbeddingModes: + """Tests for prompt template compatibility with different embedding modes.""" + + @pytest.fixture + def temp_index_dir(self): + """Create temporary directory for test indexes.""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: + yield Path(tmpdir) + + @pytest.mark.parametrize( + "mode,model,template,filename_prefix", + [ + ( + "openai", + "text-embedding-3-small", + "Represent this for searching: ", + "openai_template", + ), + ("ollama", "nomic-embed-text", "search_query: ", "ollama_template"), + ("sentence-transformers", "facebook/contriever", "query: ", "st_template"), + ], + ) + def test_prompt_template_metadata_with_embedding_modes( + self, temp_index_dir, mode, model, template, filename_prefix + ): + """Verify prompt template is saved correctly across different embedding modes. + + Tests that prompt templates are persisted to .meta.json for: + - OpenAI mode (primary use case) + - Ollama mode (also supports templates) + - Sentence-transformers mode (saved for forward compatibility) + + Expected behavior: Template is saved to .meta.json regardless of mode. + """ + with patch("leann.api.compute_embeddings") as mock_compute: + mock_compute.return_value = np.array([[0.1, 0.2, 0.3]], dtype=np.float32) + + index_path = temp_index_dir / f"{filename_prefix}.leann" + + builder = LeannBuilder( + backend_name="hnsw", + embedding_model=model, + embedding_mode=mode, + embedding_options={"prompt_template": template}, + ) + + builder.add_text(f"{mode.capitalize()} test document") + builder.build_index(str(index_path)) + + # Verify metadata + meta_path = temp_index_dir / f"{filename_prefix}.leann.meta.json" + with open(meta_path, encoding="utf-8") as f: + meta_data = json.load(f) + + assert meta_data["embedding_mode"] == mode + # Template should be saved for all modes (even if not used by some) + if "embedding_options" in meta_data: + assert meta_data["embedding_options"]["prompt_template"] == template + + +class TestQueryTemplateApplicationInComputeEmbedding: + """Tests for query template application in compute_query_embedding() (Bug Fix). + + These tests verify that query templates are applied consistently in BOTH + code paths (server and fallback) when computing query embeddings. + + This addresses the bug where query templates were only applied in the + fallback path, not when using the embedding server (the default path). + + Bug Context: + - Issue: Query templates were stored in metadata but only applied during + fallback (direct) computation, not when using embedding server + - Fix: Move template application to BEFORE any computation path in + compute_query_embedding() (searcher_base.py:107-110) + - Impact: Critical for models like EmbeddingGemma that require task-specific + templates for optimal performance + + These tests ensure the fix works correctly and prevent regression. + """ + + @pytest.fixture + def temp_index_with_template(self): + """Create a temporary index with query template in metadata""" + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: + index_dir = Path(tmpdir) + index_file = index_dir / "test.leann" + meta_file = index_dir / "test.leann.meta.json" + + # Create minimal metadata with query template + metadata = { + "version": "1.0", + "backend_name": "hnsw", + "embedding_model": "text-embedding-embeddinggemma-300m-qat", + "dimensions": 768, + "embedding_mode": "openai", + "backend_kwargs": { + "graph_degree": 32, + "complexity": 64, + "distance_metric": "cosine", + }, + "embedding_options": { + "base_url": "http://localhost:1234/v1", + "api_key": "test-key", + "build_prompt_template": "title: none | text: ", + "query_prompt_template": "task: search result | query: ", + }, + } + + meta_file.write_text(json.dumps(metadata, indent=2)) + + # Create minimal HNSW index file (empty is okay for this test) + index_file.write_bytes(b"") + + yield str(index_file) + + def test_query_template_applied_in_fallback_path(self, temp_index_with_template): + """Test that query template is applied when using fallback (direct) path""" + from leann.searcher_base import BaseSearcher + + # Create a concrete implementation for testing + class TestSearcher(BaseSearcher): + def search( + self, + query, + top_k, + complexity=64, + beam_width=1, + prune_ratio=0.0, + recompute_embeddings=False, + pruning_strategy="global", + zmq_port=None, + **kwargs, + ): + return {"labels": [], "distances": []} + + searcher = object.__new__(TestSearcher) + searcher.index_path = Path(temp_index_with_template) + searcher.index_dir = searcher.index_path.parent + + # Load metadata + meta_file = searcher.index_dir / f"{searcher.index_path.name}.meta.json" + with open(meta_file) as f: + searcher.meta = json.load(f) + + searcher.embedding_model = searcher.meta["embedding_model"] + searcher.embedding_mode = searcher.meta.get("embedding_mode", "sentence-transformers") + searcher.embedding_options = searcher.meta.get("embedding_options", {}) + searcher.enable_warmup = False + searcher.use_daemon = False + searcher.daemon_ttl_seconds = 0 + + # Mock compute_embeddings to capture the query text + captured_queries = [] + + def mock_compute_embeddings(texts, model, mode, provider_options=None): + captured_queries.extend(texts) + return np.random.rand(len(texts), 768).astype(np.float32) + + with patch( + "leann.embedding_compute.compute_embeddings", side_effect=mock_compute_embeddings + ): + # Call compute_query_embedding with template (fallback path) + result = searcher.compute_query_embedding( + query="vector database", + use_server_if_available=False, # Force fallback path + query_template="task: search result | query: ", + ) + + # Verify template was applied + assert len(captured_queries) == 1 + assert captured_queries[0] == "task: search result | query: vector database" + assert result.shape == (1, 768) + + def test_query_template_applied_in_server_path(self, temp_index_with_template): + """Test that query template is applied when using server path""" + from leann.searcher_base import BaseSearcher + + # Create a concrete implementation for testing + class TestSearcher(BaseSearcher): + def search( + self, + query, + top_k, + complexity=64, + beam_width=1, + prune_ratio=0.0, + recompute_embeddings=False, + pruning_strategy="global", + zmq_port=None, + **kwargs, + ): + return {"labels": [], "distances": []} + + searcher = object.__new__(TestSearcher) + searcher.index_path = Path(temp_index_with_template) + searcher.index_dir = searcher.index_path.parent + + # Load metadata + meta_file = searcher.index_dir / f"{searcher.index_path.name}.meta.json" + with open(meta_file) as f: + searcher.meta = json.load(f) + + searcher.embedding_model = searcher.meta["embedding_model"] + searcher.embedding_mode = searcher.meta.get("embedding_mode", "sentence-transformers") + searcher.embedding_options = searcher.meta.get("embedding_options", {}) + searcher.enable_warmup = False + searcher.use_daemon = False + searcher.daemon_ttl_seconds = 0 + + # Mock the server methods to capture the query text + captured_queries = [] + + def mock_ensure_server_running(passages_file, port, **kwargs): + return port + + def mock_compute_embedding_via_server(chunks, port): + captured_queries.extend(chunks) + return np.random.rand(len(chunks), 768).astype(np.float32) + + searcher._ensure_server_running = mock_ensure_server_running + searcher._compute_embedding_via_server = mock_compute_embedding_via_server + + # Call compute_query_embedding with template (server path) + result = searcher.compute_query_embedding( + query="vector database", + use_server_if_available=True, # Use server path + query_template="task: search result | query: ", + ) + + # Verify template was applied BEFORE calling server + assert len(captured_queries) == 1 + assert captured_queries[0] == "task: search result | query: vector database" + assert result.shape == (1, 768) + + def test_query_template_without_template_parameter(self, temp_index_with_template): + """Test that query is unchanged when no template is provided""" + from leann.searcher_base import BaseSearcher + + class TestSearcher(BaseSearcher): + def search( + self, + query, + top_k, + complexity=64, + beam_width=1, + prune_ratio=0.0, + recompute_embeddings=False, + pruning_strategy="global", + zmq_port=None, + **kwargs, + ): + return {"labels": [], "distances": []} + + searcher = object.__new__(TestSearcher) + searcher.index_path = Path(temp_index_with_template) + searcher.index_dir = searcher.index_path.parent + + meta_file = searcher.index_dir / f"{searcher.index_path.name}.meta.json" + with open(meta_file) as f: + searcher.meta = json.load(f) + + searcher.embedding_model = searcher.meta["embedding_model"] + searcher.embedding_mode = searcher.meta.get("embedding_mode", "sentence-transformers") + searcher.embedding_options = searcher.meta.get("embedding_options", {}) + searcher.enable_warmup = False + searcher.use_daemon = False + searcher.daemon_ttl_seconds = 0 + + captured_queries = [] + + def mock_compute_embeddings(texts, model, mode, provider_options=None): + captured_queries.extend(texts) + return np.random.rand(len(texts), 768).astype(np.float32) + + with patch( + "leann.embedding_compute.compute_embeddings", side_effect=mock_compute_embeddings + ): + searcher.compute_query_embedding( + query="vector database", + use_server_if_available=False, + query_template=None, # No template + ) + + # Verify query is unchanged + assert len(captured_queries) == 1 + assert captured_queries[0] == "vector database" + + def test_query_template_consistency_between_paths(self, temp_index_with_template): + """Test that both paths apply template identically""" + from leann.searcher_base import BaseSearcher + + class TestSearcher(BaseSearcher): + def search( + self, + query, + top_k, + complexity=64, + beam_width=1, + prune_ratio=0.0, + recompute_embeddings=False, + pruning_strategy="global", + zmq_port=None, + **kwargs, + ): + return {"labels": [], "distances": []} + + searcher = object.__new__(TestSearcher) + searcher.index_path = Path(temp_index_with_template) + searcher.index_dir = searcher.index_path.parent + + meta_file = searcher.index_dir / f"{searcher.index_path.name}.meta.json" + with open(meta_file) as f: + searcher.meta = json.load(f) + + searcher.embedding_model = searcher.meta["embedding_model"] + searcher.embedding_mode = searcher.meta.get("embedding_mode", "sentence-transformers") + searcher.embedding_options = searcher.meta.get("embedding_options", {}) + searcher.enable_warmup = False + searcher.use_daemon = False + searcher.daemon_ttl_seconds = 0 + + query_template = "task: search result | query: " + original_query = "vector database" + + # Capture queries from fallback path + fallback_queries = [] + + def mock_compute_embeddings(texts, model, mode, provider_options=None): + fallback_queries.extend(texts) + return np.random.rand(len(texts), 768).astype(np.float32) + + with patch( + "leann.embedding_compute.compute_embeddings", side_effect=mock_compute_embeddings + ): + searcher.compute_query_embedding( + query=original_query, + use_server_if_available=False, + query_template=query_template, + ) + + # Capture queries from server path + server_queries = [] + + def mock_ensure_server_running(passages_file, port, **kwargs): + return port + + def mock_compute_embedding_via_server(chunks, port): + server_queries.extend(chunks) + return np.random.rand(len(chunks), 768).astype(np.float32) + + searcher._ensure_server_running = mock_ensure_server_running + searcher._compute_embedding_via_server = mock_compute_embedding_via_server + + searcher.compute_query_embedding( + query=original_query, + use_server_if_available=True, + query_template=query_template, + ) + + # Verify both paths produced identical templated queries + assert len(fallback_queries) == 1 + assert len(server_queries) == 1 + assert fallback_queries[0] == server_queries[0] + assert fallback_queries[0] == f"{query_template}{original_query}" + + def test_query_template_with_empty_string(self, temp_index_with_template): + """Test behavior with empty template string""" + from leann.searcher_base import BaseSearcher + + class TestSearcher(BaseSearcher): + def search( + self, + query, + top_k, + complexity=64, + beam_width=1, + prune_ratio=0.0, + recompute_embeddings=False, + pruning_strategy="global", + zmq_port=None, + **kwargs, + ): + return {"labels": [], "distances": []} + + searcher = object.__new__(TestSearcher) + searcher.index_path = Path(temp_index_with_template) + searcher.index_dir = searcher.index_path.parent + + meta_file = searcher.index_dir / f"{searcher.index_path.name}.meta.json" + with open(meta_file) as f: + searcher.meta = json.load(f) + + searcher.embedding_model = searcher.meta["embedding_model"] + searcher.embedding_mode = searcher.meta.get("embedding_mode", "sentence-transformers") + searcher.embedding_options = searcher.meta.get("embedding_options", {}) + searcher.enable_warmup = False + searcher.use_daemon = False + searcher.daemon_ttl_seconds = 0 + + captured_queries = [] + + def mock_compute_embeddings(texts, model, mode, provider_options=None): + captured_queries.extend(texts) + return np.random.rand(len(texts), 768).astype(np.float32) + + with patch( + "leann.embedding_compute.compute_embeddings", side_effect=mock_compute_embeddings + ): + searcher.compute_query_embedding( + query="vector database", + use_server_if_available=False, + query_template="", # Empty string + ) + + # Empty string is falsy, so no template should be applied + assert captured_queries[0] == "vector database" diff --git a/tests/test_react_dual_source.py b/tests/test_react_dual_source.py new file mode 100644 index 0000000..5341777 --- /dev/null +++ b/tests/test_react_dual_source.py @@ -0,0 +1,401 @@ +""" +Tests for ReAct agent dual-source routing (issue #283). +Covers: prompt adaptation, web fallback, visit_page errors, routing, and pipeline. +""" + +from unittest.mock import MagicMock, patch + +from leann.api import SearchResult +from leann.react_agent import ReActAgent +from leann.web_search import WebSearcher + + +def _make_searcher() -> MagicMock: + """Create a lightweight mocked searcher for deterministic unit tests.""" + searcher = MagicMock() + searcher.search.return_value = [ + SearchResult( + id="1", + score=0.91, + text="LEANN achieves 97% storage reduction via graph pruning.", + metadata={"source": "docs"}, + ), + SearchResult( + id="2", + score=0.83, + text="The search function uses HNSW for approximate nearest neighbors.", + metadata={"source": "code"}, + ), + ] + return searcher + + +# ── 1. Dual-source presence ────────────────────────────────────────── + + +def test_prompt_includes_web_tools_when_key_present(): + """When SERPER_API_KEY is set, prompt should list all three tools.""" + searcher = _make_searcher() + agent = ReActAgent(searcher=searcher, llm=MagicMock(), serper_api_key="test-key") + prompt = agent._create_react_prompt("test question", 1, []) + assert "web_search" in prompt + assert "visit_page" in prompt + assert "leann_search" in prompt + assert agent.web_search_available is True + + +def test_prompt_excludes_web_tools_when_no_key(): + """When no SERPER_API_KEY, prompt should only show leann_search.""" + searcher = _make_searcher() + with patch.dict("os.environ", {}, clear=False): + agent = ReActAgent(searcher=searcher, llm=MagicMock(), serper_api_key=None) + prompt = agent._create_react_prompt("test question", 1, []) + assert "leann_search" in prompt + assert "web_search" not in prompt or "not available" in prompt + assert agent.web_search_available is False + + +# ── 2. Routing behavior ───────────────────────────────────────────── + + +def test_local_only_routing(): + """Agent uses leann_search for local queries.""" + searcher = _make_searcher() + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: This is about our codebase.\nAction: leann_search("LEANN storage reduction")', + "Thought: I have the answer.\nAction: Final Answer: LEANN saves 97% storage.", + ] + agent = ReActAgent(searcher=searcher, llm=mock_llm, max_iterations=3) + agent.run("How does LEANN reduce storage?", top_k=2) + + assert len(agent.search_history) >= 1 + assert agent.search_history[0]["source"] == "local" + assert "leann_search" in agent.search_history[0]["action"] + + +def test_web_only_routing(): + """Agent uses web_search for web queries (mocked).""" + searcher = _make_searcher() + + with patch.object(WebSearcher, "search") as mock_web: + mock_web.return_value = [ + { + "title": "Python 3.13 News", + "link": "https://python.org", + "snippet": "New features in 3.13", + } + ] + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: Need current web info.\nAction: web_search("Python 3.13 features")', + "Thought: Got it.\nAction: Final Answer: Python 3.13 has new features.", + ] + agent = ReActAgent( + searcher=searcher, llm=mock_llm, max_iterations=3, serper_api_key="test-key" + ) + agent.run("What's new in Python 3.13?", top_k=2) + + assert len(agent.search_history) >= 1 + assert agent.search_history[0]["source"] == "web" + mock_web.assert_called_once() + + +def test_mixed_routing_local_then_web(): + """Agent uses leann_search first, then web_search.""" + searcher = _make_searcher() + + with patch.object(WebSearcher, "search") as mock_web: + mock_web.return_value = [ + { + "title": "Best Practices", + "link": "https://example.com", + "snippet": "Current best practices...", + } + ] + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: First check our code.\nAction: leann_search("search implementation")', + 'Thought: Now check best practices.\nAction: web_search("vector DB best practices")', + "Thought: I can compare now.\nAction: Final Answer: Our search is good but could improve.", + ] + agent = ReActAgent( + searcher=searcher, llm=mock_llm, max_iterations=5, serper_api_key="test-key" + ) + agent.run("Compare our search with best practices", top_k=2) + + assert len(agent.search_history) >= 2 + assert agent.search_history[0]["source"] == "local" + assert agent.search_history[1]["source"] == "web" + + +# ── 3. Pipeline correctness ───────────────────────────────────────── + + +def test_web_results_formatted_as_observations(): + """Web search results are properly formatted and passed to the next iteration.""" + searcher = _make_searcher() + + with patch.object(WebSearcher, "search") as mock_web: + mock_web.return_value = [ + {"title": "Result A", "link": "https://a.com", "snippet": "Snippet A"}, + {"title": "Result B", "link": "https://b.com", "snippet": "Snippet B"}, + ] + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: Search the web.\nAction: web_search("test query")', + "Thought: Done.\nAction: Final Answer: Found it.", + ] + agent = ReActAgent( + searcher=searcher, llm=mock_llm, max_iterations=3, serper_api_key="test-key" + ) + agent.run("test", top_k=2) + + # The second LLM call should contain the formatted web results + second_prompt = mock_llm.ask.call_args_list[1][0][0] + assert "Result A" in second_prompt or "Snippet A" in second_prompt + + +def test_visit_page_content_truncated(): + """visit_page content is truncated to 15k chars.""" + searcher = _make_searcher() + + with patch.object(WebSearcher, "get_page_content") as mock_fetch: + mock_fetch.return_value = "x" * 20000 + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: Read this page.\nAction: visit_page("https://docs.python.org/3")', + "Thought: Done.\nAction: Final Answer: Got the content.", + ] + agent = ReActAgent( + searcher=searcher, llm=mock_llm, max_iterations=3, serper_api_key="test-key" + ) + agent.run("read docs", top_k=2) + + second_prompt = mock_llm.ask.call_args_list[1][0][0] + # Content should be truncated β€” observation contains at most 15000 chars of content + assert "x" * 15000 in second_prompt + assert "x" * 15001 not in second_prompt + + +def test_leann_search_results_include_scores(): + """Local search results include scores and text snippets.""" + searcher = _make_searcher() + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: Search locally.\nAction: leann_search("LEANN")', + "Thought: Done.\nAction: Final Answer: Found results.", + ] + agent = ReActAgent(searcher=searcher, llm=mock_llm, max_iterations=3) + agent.run("tell me about LEANN", top_k=2) + + second_prompt = mock_llm.ask.call_args_list[1][0][0] + assert "Score:" in second_prompt + assert "Result" in second_prompt + + +# ── 4. Edge cases ──────────────────────────────────────────────────── + + +def test_web_search_no_api_key_graceful(): + """web_search without API key returns clear fallback message, not a crash.""" + searcher = _make_searcher() + + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: Try web.\nAction: web_search("Python 3.13")', + 'Thought: Web failed, try local.\nAction: leann_search("search")', + "Thought: Done.\nAction: Final Answer: Here's what I found locally.", + ] + with patch.dict("os.environ", {}, clear=False): + agent = ReActAgent(searcher=searcher, llm=mock_llm, max_iterations=5, serper_api_key=None) + answer = agent.run("test", top_k=2) + + assert agent.search_history[0]["results_count"] == 0 + assert agent.search_history[0]["source"] == "web" + # Agent should not crash and should produce an answer + assert answer is not None and len(answer) > 0 + + +def test_web_search_invalid_key_graceful(): + """Invalid Serper key returns error, agent continues.""" + searcher = _make_searcher() + + with patch.object(WebSearcher, "search") as mock_web: + mock_web.return_value = [ + {"title": "Error", "link": "", "snippet": "Web Search failed:401 Unauthorized"} + ] + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: Try web.\nAction: web_search("test")', + "Thought: Web failed.\nAction: Final Answer: Could not search web.", + ] + agent = ReActAgent( + searcher=searcher, llm=mock_llm, max_iterations=3, serper_api_key="bad-key" + ) + answer = agent.run("test", top_k=2) + + assert agent.search_history[0]["results_count"] == 0 + assert answer is not None + + +def test_visit_page_404_graceful(): + """visit_page on a 404 URL returns error string, agent continues.""" + searcher = _make_searcher() + + with patch.object(WebSearcher, "get_page_content") as mock_fetch: + mock_fetch.return_value = "Error fetching content: 404 Not Found" + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: Read page.\nAction: visit_page("https://example.com/404")', + "Thought: Page not found.\nAction: Final Answer: Page was not accessible.", + ] + agent = ReActAgent( + searcher=searcher, llm=mock_llm, max_iterations=3, serper_api_key="test-key" + ) + answer = agent.run("read page", top_k=2) + + assert agent.search_history[0]["results_count"] == 0 + assert "not accessible" in answer.lower() or len(answer) > 0 + + +def test_max_iterations_with_mixed_sources(): + """Agent hitting max iterations with mixed sources still produces an answer.""" + searcher = _make_searcher() + + with patch.object(WebSearcher, "search") as mock_web: + mock_web.return_value = [ + {"title": "Web Result", "link": "https://example.com", "snippet": "Some web info"} + ] + mock_llm = MagicMock() + # Never gives a Final Answer β€” forces max iterations + mock_llm.ask.side_effect = [ + 'Thought: Search locally.\nAction: leann_search("storage")', + 'Thought: Now web.\nAction: web_search("vector databases")', + # Max iterations reached β€” the agent will ask for a final answer + "Based on all searches, LEANN is a storage-efficient vector DB.", + ] + agent = ReActAgent( + searcher=searcher, llm=mock_llm, max_iterations=2, serper_api_key="test-key" + ) + answer = agent.run("Compare approaches", top_k=2) + + assert len(agent.search_history) == 2 + sources = {h["source"] for h in agent.search_history} + assert "local" in sources + assert "web" in sources + assert answer is not None and len(answer) > 0 + + +def test_search_history_has_source_field(): + """search_history entries include 'source' field for easy inspection.""" + searcher = _make_searcher() + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: Local search.\nAction: leann_search("test")', + "Thought: Done.\nAction: Final Answer: Done.", + ] + agent = ReActAgent(searcher=searcher, llm=mock_llm, max_iterations=3) + agent.run("test", top_k=2) + + assert "source" in agent.search_history[0] + assert agent.search_history[0]["source"] in ("local", "web") + + +def test_zero_results_on_iteration_two_does_not_abort_early(): + """A single empty tool result must not force an ungrounded final answer (#381).""" + searcher = _make_searcher() + searcher.search.side_effect = [ + [ + SearchResult( + id="1", + score=0.9, + text="Partial LEANN benchmark mention.", + metadata={"source": "docs"}, + ) + ], + [], + [ + SearchResult( + id="2", + score=0.95, + text="LEANN recall@10 = 0.95 vs FAISS 0.97 per the paper.", + metadata={"source": "web"}, + ) + ], + ] + + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: Start local.\nAction: leann_search("LEANN benchmark")', + 'Thought: Narrow local query.\nAction: leann_search("LEANN recall numbers vs faiss")', + 'Thought: Try web.\nAction: web_search("LEANN paper recall comparison FAISS")', + "Thought: Found it.\nAction: Final Answer: LEANN achieves recall@10 = 0.95 vs FAISS 0.97.", + ] + + with patch.object(WebSearcher, "search") as mock_web: + mock_web.return_value = [ + { + "title": "LEANN paper", + "link": "https://example.com/paper", + "snippet": "recall@10 comparison", + } + ] + agent = ReActAgent( + searcher=searcher, + llm=mock_llm, + max_iterations=5, + serper_api_key="test-key", + ) + answer = agent.run("What is LEANN's recall vs FAISS at recall@10?", top_k=2) + + assert len(agent.search_history) == 3 + assert agent.search_history[1]["results_count"] == 0 + assert "0.95" in answer + assert mock_llm.ask.call_count == 4 + + +def test_transient_web_failure_allows_retry_on_next_iteration(): + """Transient web_search failure should be retried, not terminate the loop (#381).""" + searcher = _make_searcher() + searcher.search.return_value = [ + SearchResult( + id="1", + score=0.8, + text="Local setup notes.", + metadata={"source": "docs"}, + ) + ] + + mock_llm = MagicMock() + mock_llm.ask.side_effect = [ + 'Thought: Check local docs.\nAction: leann_search("project setup")', + 'Thought: Check web.\nAction: web_search("latest setup guide")', + 'Thought: Retry web.\nAction: web_search("setup guide tutorial")', + "Thought: Found it.\nAction: Final Answer: per the latest docs, run `make install`.", + ] + + with patch.object(WebSearcher, "search") as mock_web: + mock_web.side_effect = [ + [{"title": "Error", "link": "", "snippet": "Web Search failed:502 Bad Gateway"}], + [ + { + "title": "Setup guide", + "link": "https://example.com/setup", + "snippet": "Run `make install` to set up the project.", + } + ], + ] + agent = ReActAgent( + searcher=searcher, + llm=mock_llm, + max_iterations=5, + serper_api_key="test-key", + ) + answer = agent.run("How do I set up the project?", top_k=2) + + assert len(agent.search_history) == 3 + assert agent.search_history[1]["results_count"] == 0 + assert agent.search_history[2]["results_count"] == 1 + assert "make install" in answer + assert mock_llm.ask.call_count == 4 diff --git a/tests/test_readme_examples.py b/tests/test_readme_examples.py new file mode 100644 index 0000000..8fe0704 --- /dev/null +++ b/tests/test_readme_examples.py @@ -0,0 +1,234 @@ +""" +Test examples from README.md to ensure documentation is accurate. +""" + +import os +import platform +import tempfile +from pathlib import Path + +import numpy as np +import pytest + +TEST_EMBEDDING_MODEL = "test-deterministic-embeddings" +TEST_EMBEDDING_DIMENSIONS = 8 + + +def _deterministic_embeddings( + chunks, + model_name, + mode="sentence-transformers", + use_server=True, + port=None, + is_build=False, + provider_options=None, +): + del model_name, mode, use_server, port, is_build, provider_options + + embeddings = [] + for chunk in chunks: + text = str(chunk).lower() + vector = np.zeros(TEST_EMBEDDING_DIMENSIONS, dtype=np.float32) + if any(term in text for term in ("fantastical", "banana", "crocodile")): + vector[0] = 1.0 + elif any(term in text for term in ("storage", "leann", "saves")): + vector[1] = 1.0 + else: + vector[2] = 1.0 + embeddings.append(vector) + return np.vstack(embeddings) + + +def _deterministic_direct_embeddings( + chunks, + model_name, + mode="sentence-transformers", + is_build=False, + provider_options=None, +): + return _deterministic_embeddings( + chunks, + model_name, + mode=mode, + use_server=False, + is_build=is_build, + provider_options=provider_options, + ) + + +@pytest.fixture +def deterministic_embeddings(monkeypatch): + """Keep README example tests offline and deterministic in CI.""" + monkeypatch.setattr("leann.api.compute_embeddings", _deterministic_embeddings) + monkeypatch.setattr( + "leann.embedding_compute.compute_embeddings", + _deterministic_direct_embeddings, + ) + + +def _test_builder_kwargs(backend_name): + kwargs = { + "backend_name": backend_name, + "embedding_model": TEST_EMBEDDING_MODEL, + "dimensions": TEST_EMBEDDING_DIMENSIONS, + } + if backend_name == "hnsw": + kwargs.update({"is_recompute": False, "is_compact": False}) + return kwargs + + +def _skip_if_backend_unavailable(backend_name): + from leann.api import get_registered_backends + + if backend_name not in get_registered_backends(): + pytest.skip(f"Backend {backend_name!r} is not installed") + + +@pytest.mark.parametrize("backend_name", ["hnsw", "diskann"]) +def test_readme_basic_example(backend_name, deterministic_embeddings): + """Test the basic example from README.md with both backends.""" + _skip_if_backend_unavailable(backend_name) + # Skip on macOS CI due to MPS environment issues with all-MiniLM-L6-v2 + if os.environ.get("CI") == "true" and platform.system() == "Darwin": + pytest.skip("Skipping on macOS CI due to MPS environment issues with all-MiniLM-L6-v2") + # Skip DiskANN on CI (Linux runners) due to C++ extension memory/hardware constraints + if os.environ.get("CI") == "true" and backend_name == "diskann": + pytest.skip("Skip DiskANN tests in CI due to resource constraints and instability") + + # Exercise the README flow without depending on live model downloads in CI. + from leann import LeannBuilder, LeannChat, LeannSearcher + from leann.api import SearchResult + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + INDEX_PATH = str(Path(temp_dir) / f"demo_{backend_name}.leann") + + builder = LeannBuilder(**_test_builder_kwargs(backend_name)) + builder.add_text("LEANN saves 97% storage compared to traditional vector databases.") + builder.add_text("Tung Tung Tung Sahur calledβ€”they need their banana-crocodile hybrid back") + builder.build_index(INDEX_PATH) + + index_dir = Path(INDEX_PATH).parent + assert index_dir.exists() + index_files = list(index_dir.glob(f"{Path(INDEX_PATH).stem}.*")) + assert len(index_files) > 0 + + with LeannSearcher(INDEX_PATH, recompute_embeddings=False, enable_warmup=False) as searcher: + results = searcher.search("fantastical AI-generated creatures", top_k=1) + + assert len(results) > 0 + assert isinstance(results[0], SearchResult) + assert results[0].score != float("-inf"), ( + f"should return valid scores, got {results[0].score}" + ) + assert "banana" in results[0].text or "crocodile" in results[0].text + + chat = LeannChat( + INDEX_PATH, + llm_config={"type": "simulated"}, + recompute_embeddings=False, + ) + response = chat.ask( + "How much storage does LEANN save?", + top_k=1, + recompute_embeddings=False, + ) + + # Verify chat works + assert isinstance(response, str) + assert len(response) > 0 + # Cleanup chat resources + chat.cleanup() + + +def test_readme_imports(): + """Test that the imports shown in README work correctly.""" + # These are the imports shown in README + from leann import LeannBuilder, LeannChat, LeannSearcher + + # Verify they are the correct types + assert callable(LeannBuilder) + assert callable(LeannSearcher) + assert callable(LeannChat) + + +def test_backend_options(deterministic_embeddings): + """Test different backend options mentioned in documentation.""" + # Skip on macOS CI due to MPS environment issues with all-MiniLM-L6-v2 + if os.environ.get("CI") == "true" and platform.system() == "Darwin": + pytest.skip("Skipping on macOS CI due to MPS environment issues with all-MiniLM-L6-v2") + + from leann import LeannBuilder + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + is_ci = os.environ.get("CI") == "true" + + hnsw_path = str(Path(temp_dir) / "test_hnsw.leann") + builder_hnsw = LeannBuilder(**_test_builder_kwargs("hnsw")) + builder_hnsw.add_text("Test document for HNSW backend") + builder_hnsw.build_index(hnsw_path) + assert Path(hnsw_path).parent.exists() + assert len(list(Path(hnsw_path).parent.glob(f"{Path(hnsw_path).stem}.*"))) > 0 + + if is_ci: + pytest.skip( + "Skip DiskANN portion in CI - small datasets trigger MKL parameter " + "errors and pytest-timeout thread kills cause segfaults on Windows" + ) + _skip_if_backend_unavailable("diskann") + + diskann_path = str(Path(temp_dir) / "test_diskann.leann") + builder_diskann = LeannBuilder(**_test_builder_kwargs("diskann")) + builder_diskann.add_text("Test document for DiskANN backend") + builder_diskann.build_index(diskann_path) + assert Path(diskann_path).parent.exists() + assert len(list(Path(diskann_path).parent.glob(f"{Path(diskann_path).stem}.*"))) > 0 + + +@pytest.mark.parametrize("backend_name", ["hnsw", "diskann"]) +def test_llm_config_simulated(backend_name, deterministic_embeddings): + """Test simulated LLM configuration option with both backends.""" + _skip_if_backend_unavailable(backend_name) + # Skip on macOS CI due to MPS environment issues with all-MiniLM-L6-v2 + if os.environ.get("CI") == "true" and platform.system() == "Darwin": + pytest.skip("Skipping on macOS CI due to MPS environment issues with all-MiniLM-L6-v2") + + # Skip DiskANN tests in CI due to hardware requirements + if os.environ.get("CI") == "true" and backend_name == "diskann": + pytest.skip("Skip DiskANN tests in CI - requires specific hardware and large memory") + + from leann import LeannBuilder, LeannChat + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / f"test_{backend_name}.leann") + builder = LeannBuilder(**_test_builder_kwargs(backend_name)) + builder.add_text("Test document for LLM testing") + builder.build_index(index_path) + + llm_config = {"type": "simulated"} + chat = LeannChat(index_path, llm_config=llm_config) + response = chat.ask("What is this document about?", top_k=1, recompute_embeddings=False) + + assert isinstance(response, str) + assert len(response) > 0 + + +@pytest.mark.skip(reason="Requires HF model download and may timeout") +def test_llm_config_hf(): + """Test HuggingFace LLM configuration option.""" + from leann import LeannBuilder, LeannChat + + pytest.importorskip("transformers") # Skip if transformers not installed + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir: + index_path = str(Path(temp_dir) / "test.leann") + builder = LeannBuilder(backend_name="hnsw") + builder.add_text("Test document for LLM testing") + builder.build_index(index_path) + + # Test HF LLM config + llm_config = {"type": "hf", "model": "Qwen/Qwen3-0.6B"} + chat = LeannChat(index_path, llm_config=llm_config) + response = chat.ask("What is this document about?", top_k=1) + + assert isinstance(response, str) + assert len(response) > 0 diff --git a/tests/test_rebuild_cli.py b/tests/test_rebuild_cli.py new file mode 100644 index 0000000..eca86a8 --- /dev/null +++ b/tests/test_rebuild_cli.py @@ -0,0 +1,348 @@ +import asyncio +import json +import pickle +from pathlib import Path + +import pytest +from leann.cli import LeannCLI + + +def test_reconstruct_build_args_replays_stored_build_config(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + docs_root = tmp_path / "docs" + docs_root.mkdir() + docs_file = docs_root / "only.md" + docs_file.write_text("hello", encoding="utf-8") + + cli = LeannCLI() + index_dir = tmp_path / ".leann" / "indexes" / "sample" + index_dir.mkdir(parents=True) + meta_path = index_dir / "documents.leann.meta.json" + meta_path.write_text( + json.dumps( + { + "backend_name": "hnsw", + "embedding_model": "text-embedding-3-small", + "embedding_mode": "openai", + "backend_kwargs": { + "graph_degree": 32, + "complexity": 64, + "num_threads": 1, + "is_compact": False, + "is_recompute": True, + }, + "embedding_options": { + "base_url": "https://metadata.example/v1", + "api_key": "secret-should-not-be-replayed", + "build_prompt_template": "metadata passage: ", + "query_prompt_template": "metadata query: ", + }, + } + ), + encoding="utf-8", + ) + sync_config = { + "roots": [str(docs_root)], + "include_extensions": [".md"], + "ignore_patterns": ["**/.*"], + "build_config": { + "docs": [str(docs_file.resolve())], + "file_types": ".md,.txt", + "include_hidden": True, + "doc_chunk_size": 384, + "doc_chunk_overlap": 96, + "code_chunk_size": 640, + "code_chunk_overlap": 80, + "use_ast_chunking": True, + "ast_chunk_size": 420, + "ast_chunk_overlap": 72, + "ast_fallback_traditional": True, + "graph_degree": 48, + "complexity": 96, + "num_threads": 4, + "compact": True, + "recompute": False, + "embedding_api_base": "https://stored.example/v1", + "embedding_prompt_template": "stored passage: ", + "query_prompt_template": "stored query: ", + }, + } + (index_dir / "sync_roots.json").write_text(json.dumps(sync_config), encoding="utf-8") + + reconstructed = cli._reconstruct_build_args("sample", force=True) + + assert reconstructed is not None + assert "--embedding-api-key" not in reconstructed + parsed = cli.create_parser().parse_args(reconstructed) + assert parsed.docs == [str(docs_file.resolve())] + assert parsed.file_types == ".md,.txt" + assert parsed.include_hidden is True + assert parsed.doc_chunk_size == 384 + assert parsed.doc_chunk_overlap == 96 + assert parsed.code_chunk_size == 640 + assert parsed.code_chunk_overlap == 80 + assert parsed.use_ast_chunking is True + assert parsed.ast_chunk_size == 420 + assert parsed.ast_chunk_overlap == 72 + assert parsed.graph_degree == 48 + assert parsed.complexity == 96 + assert parsed.num_threads == 4 + assert parsed.compact is True + assert parsed.recompute is False + assert parsed.embedding_api_base == "https://stored.example/v1" + assert parsed.embedding_prompt_template == "stored passage: " + assert parsed.query_prompt_template == "stored query: " + assert parsed.force is True + + +def test_reconstruct_build_args_falls_back_to_metadata_and_sync_config(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + docs_root = tmp_path / "docs" + docs_root.mkdir() + + cli = LeannCLI() + index_dir = tmp_path / ".leann" / "indexes" / "legacy" + index_dir.mkdir(parents=True) + (index_dir / "documents.leann.meta.json").write_text( + json.dumps( + { + "backend_name": "hnsw", + "embedding_model": "facebook/contriever", + "embedding_mode": "ollama", + "backend_kwargs": { + "graph_degree": 40, + "complexity": 88, + "num_threads": 3, + "is_compact": True, + "is_recompute": False, + }, + "embedding_options": { + "host": "http://ollama.example:11434", + "prompt_template": "passage: ", + }, + } + ), + encoding="utf-8", + ) + (index_dir / "sync_roots.json").write_text( + json.dumps( + { + "roots": [str(docs_root)], + "include_extensions": [".md", ".py"], + "ignore_patterns": None, + } + ), + encoding="utf-8", + ) + + reconstructed = cli._reconstruct_build_args("legacy") + + assert reconstructed is not None + parsed = cli.create_parser().parse_args(reconstructed) + assert parsed.docs == [str(docs_root)] + assert parsed.file_types == ".md,.py" + assert parsed.include_hidden is True + assert parsed.graph_degree == 40 + assert parsed.complexity == 88 + assert parsed.num_threads == 3 + assert parsed.compact is True + assert parsed.recompute is False + assert parsed.embedding_host == "http://ollama.example:11434" + assert parsed.embedding_prompt_template == "passage: " + + +def test_successful_full_build_persists_rebuild_config(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + docs_root = tmp_path / "docs" + docs_root.mkdir() + docs_file = docs_root / "only.py" + docs_file.write_text("def hello():\n return 'world'\n", encoding="utf-8") + + cli = LeannCLI() + args = cli.create_parser().parse_args( + [ + "build", + "sample", + "--docs", + str(docs_file), + "--backend-name", + "hnsw", + "--file-types", + ".py", + "--include-hidden", + "--doc-chunk-size", + "10", + "--doc-chunk-overlap", + "12", + "--code-chunk-size", + "20", + "--code-chunk-overlap", + "25", + "--use-ast-chunking", + "--ast-chunk-size", + "123", + "--ast-chunk-overlap", + "17", + "--graph-degree", + "44", + "--complexity", + "77", + "--num-threads", + "2", + "--compact", + "--no-recompute", + ] + ) + + class FakeSynchronizer: + def detect_changes(self): + return {str(docs_file.resolve())}, set(), set() + + def create_snapshot(self): + pass + + class SuccessfulBuilder: + def __init__(self, **kwargs): + self.kwargs = kwargs + + def add_text(self, _text, metadata=None): + pass + + def build_index(self, index_path): + target_dir = Path(index_path).parent + target_dir.mkdir(parents=True, exist_ok=True) + (target_dir / "documents.leann.meta.json").write_text( + json.dumps( + { + "backend_name": self.kwargs["backend_name"], + "embedding_model": self.kwargs["embedding_model"], + "embedding_mode": self.kwargs["embedding_mode"], + "backend_kwargs": { + "graph_degree": self.kwargs["graph_degree"], + "complexity": self.kwargs["complexity"], + "num_threads": self.kwargs["num_threads"], + "is_compact": self.kwargs["is_compact"], + "is_recompute": self.kwargs["is_recompute"], + }, + } + ), + encoding="utf-8", + ) + + monkeypatch.setattr(cli, "_build_synchronizers", lambda *_args, **_kwargs: [FakeSynchronizer()]) + monkeypatch.setattr( + cli, + "load_documents", + lambda *_args, **_kwargs: [{"text": "rebuilt", "metadata": {"file_path": str(docs_file)}}], + ) + monkeypatch.setattr(cli, "register_project_dir", lambda: None) + monkeypatch.setattr("leann.cli.LeannBuilder", SuccessfulBuilder) + + asyncio.run(cli.build_index(args)) + + sync_config = json.loads( + (tmp_path / ".leann" / "indexes" / "sample" / "sync_roots.json").read_text(encoding="utf-8") + ) + build_config = sync_config["build_config"] + assert build_config["docs"] == [str(docs_file.resolve())] + assert build_config["file_types"] == ".py" + assert build_config["include_hidden"] is True + assert build_config["doc_chunk_size"] == 10 + assert build_config["doc_chunk_overlap"] == 9 + assert build_config["code_chunk_size"] == 20 + assert build_config["code_chunk_overlap"] == 19 + assert build_config["use_ast_chunking"] is True + assert build_config["ast_chunk_size"] == 123 + assert build_config["ast_chunk_overlap"] == 17 + assert build_config["graph_degree"] == 44 + assert build_config["complexity"] == 77 + assert build_config["num_threads"] == 2 + assert build_config["compact"] is True + assert build_config["recompute"] is False + + +def test_full_rebuild_failure_preserves_existing_index(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + docs_root = tmp_path / "docs" + docs_root.mkdir() + docs_file = docs_root / "only.md" + docs_file.write_text("hello", encoding="utf-8") + + cli = LeannCLI() + index_dir = tmp_path / ".leann" / "indexes" / "sample" + index_dir.mkdir(parents=True) + meta_path = index_dir / "documents.leann.meta.json" + meta_path.write_text( + json.dumps( + { + "backend_name": "hnsw", + "embedding_model": "facebook/contriever", + "embedding_mode": "sentence-transformers", + "backend_kwargs": {"is_compact": False, "is_recompute": True}, + } + ), + encoding="utf-8", + ) + passages_file = index_dir / "documents.leann.passages.jsonl" + passages_file.write_text( + json.dumps({"id": "old", "text": "old text", "metadata": {}}) + "\n", + encoding="utf-8", + ) + offset_file = index_dir / "documents.leann.passages.idx" + with open(offset_file, "wb") as f: + pickle.dump({"old": 0}, f) + original_artifacts = { + path: path.read_bytes() for path in (meta_path, passages_file, offset_file) + } + args = cli.create_parser().parse_args( + [ + "build", + "sample", + "--docs", + str(docs_file), + "--backend-name", + "hnsw", + "--force", + ] + ) + built_paths: list[str] = [] + + class FakeSynchronizer: + def create_snapshot(self): + pass + + class FailingBuilder: + def __init__(self, **_kwargs): + pass + + def add_text(self, _text, metadata=None): + pass + + def build_index(self, index_path): + built_paths.append(index_path) + target_dir = Path(index_path).parent + target_dir.mkdir(parents=True, exist_ok=True) + (target_dir / "documents.leann.passages.jsonl").write_text( + json.dumps({"id": "new", "text": "new text", "metadata": {}}) + "\n", + encoding="utf-8", + ) + raise RuntimeError("simulated build failure") + + monkeypatch.setattr(cli, "_build_synchronizers", lambda *_args, **_kwargs: [FakeSynchronizer()]) + monkeypatch.setattr( + cli, + "load_documents", + lambda *_args, **_kwargs: [{"text": "rebuilt", "metadata": {"file_path": str(docs_file)}}], + ) + monkeypatch.setattr(cli, "register_project_dir", lambda: None) + monkeypatch.setattr("leann.cli.LeannBuilder", FailingBuilder) + + with pytest.raises(RuntimeError, match="simulated build failure"): + asyncio.run(cli.build_index(args)) + + assert built_paths + assert built_paths[0] != str(index_dir / "documents.leann") + assert { + path: path.read_bytes() for path in (meta_path, passages_file, offset_file) + } == original_artifacts + assert not list(index_dir.parent.glob(".sample.rebuild-*")) diff --git a/tests/test_sync.py b/tests/test_sync.py new file mode 100644 index 0000000..f57971a --- /dev/null +++ b/tests/test_sync.py @@ -0,0 +1,155 @@ +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import Mock + +from leann.sync import FileSynchronizer, MerkleTree, hash_data + + +class TestMerkleTreeCompare(unittest.TestCase): + def test_no_changes_if_root_hash_same(self): + tree1 = Mock() + tree2 = Mock() + + tree1.root = Mock(hash="root_hash") + tree2.root = Mock(hash="root_hash") + + added, removed, modified = MerkleTree.compare_with(tree1, tree2) + + self.assertEqual(added, []) + self.assertEqual(removed, []) + self.assertEqual(modified, []) + + def test_added_removed_modified(self): + tree1 = Mock() + tree2 = Mock() + + file_a_new = Mock() + file_b_new = Mock() + file_a_old = Mock() + file_c_old = Mock() + + file_a_new.__eq__ = Mock(return_value=False) + + tree1.root = Mock( + hash="new_root", + children={ + "a.txt": file_a_new, + "b.txt": file_b_new, + }, + ) + + tree2.root = Mock( + hash="old_root", + children={ + "a.txt": file_a_old, + "c.txt": file_c_old, + }, + ) + + added, removed, modified = MerkleTree.compare_with(tree1, tree2) + + self.assertEqual(added, ["c.txt"]) + self.assertEqual(removed, ["b.txt"]) + self.assertEqual(modified, ["a.txt"]) + + +class TestFileSynchronizer(unittest.TestCase): + def test_generate_file_hashes(self): + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "file.txt" + file_path.write_text("hello world", encoding="utf-8") + fs = FileSynchronizer(root_dir=temp_dir, auto_load=False) + result = fs.generate_file_hashes() + assert result == {str(file_path.resolve()): hash_data(file_path.read_bytes())} + + def test_generate_file_hashes_skips_binary_extensions(self): + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + (root / "code.py").write_text("print('hi')", encoding="utf-8") + (root / "image.png").write_bytes(b"\x89PNG\r\n") + fs = FileSynchronizer( + root_dir=temp_dir, + include_extensions=[".py"], + auto_load=False, + ) + result = fs.generate_file_hashes() + assert len(result) == 1 + assert str((root / "code.py").resolve()) in result + + def test_generate_file_hashes_explicit_files_only(self): + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + src = root / "src" + src.mkdir() + readme = root / "README.md" + readme.write_text("# hi", encoding="utf-8") + (src / "module.py").write_text("x = 1", encoding="utf-8") + (root / "assets").mkdir() + (root / "assets" / "icon.png").write_bytes(b"png") + + fs = FileSynchronizer( + explicit_files=[str(readme.resolve())], + include_extensions=[".md"], + auto_load=False, + ) + result = fs.generate_file_hashes() + assert set(result.keys()) == {str(readme.resolve())} + + def test_build_merkle_tree(self): + fs = FileSynchronizer.__new__(FileSynchronizer) + + file_hashes = { + "a.txt": "hashA", + "b.txt": "hashB", + } + + tree = fs.build_merkle_tree(file_hashes) + + assert tree.root is not None + assert set(tree.root.children.keys()) == {"a.txt", "b.txt"} + assert tree.root.children["a.txt"].data == "hashA" + assert tree.root.children["b.txt"].data == "hashB" + + expected_root_data = "a.txt" + "hashA" + "b.txt" + "hashB" + assert tree.root.hash == hash_data(expected_root_data) + + def test_check_for_changes_detected(self): + fs = FileSynchronizer.__new__(FileSynchronizer) + + fs.generate_file_hashes = Mock(return_value={"a.txt": "hash"}) + fs.build_merkle_tree = Mock(return_value=Mock()) + + old_tree = Mock() + new_tree = fs.build_merkle_tree.return_value + + old_tree.compare_with.return_value = (["a.txt"], [], []) + fs.tree = old_tree + + fs.save_snapshot = Mock() + + changes = fs.check_for_changes() + + assert changes == (["a.txt"], [], []) + + fs.build_merkle_tree.assert_called_once_with({"a.txt": "hash"}) + old_tree.compare_with.assert_called_once_with(new_tree) + + fs.save_snapshot.assert_called_once() + assert fs.tree is new_tree + + def test_touch_no_false_positive(self): + with tempfile.TemporaryDirectory() as temp_dir: + docs = Path(temp_dir) + f = docs / "a.txt" + f.write_text("hello", encoding="utf-8") + snapshot = str(docs / "test.pickle") + fs = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + fs.detect_changes() + fs.commit() + + os.utime(f, None) + fs2 = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) + added, removed, modified = fs2.detect_changes() + assert not added and not removed and not modified diff --git a/tests/test_token_truncation.py b/tests/test_token_truncation.py new file mode 100644 index 0000000..bfb3ca2 --- /dev/null +++ b/tests/test_token_truncation.py @@ -0,0 +1,643 @@ +"""Unit tests for token-aware truncation functionality. + +This test suite defines the contract for token truncation functions that prevent +500 errors from Ollama when text exceeds model token limits. These tests verify: + +1. Model token limit retrieval (known and unknown models) +2. Text truncation behavior for single and multiple texts +3. Token counting and truncation accuracy using tiktoken + +All tests are written in Red Phase - they should FAIL initially because the +implementation does not exist yet. +""" + +import pytest +import tiktoken +from leann.embedding_compute import ( + EMBEDDING_MODEL_LIMITS, + get_model_token_limit, + truncate_to_token_limit, +) + + +class TestModelTokenLimits: + """Tests for retrieving model-specific token limits.""" + + def test_get_model_token_limit_known_model(self): + """Verify correct token limit is returned for known models. + + Known models should return their specific token limits from + EMBEDDING_MODEL_LIMITS dictionary. + """ + # Test nomic-embed-text (2048 tokens) + limit = get_model_token_limit("nomic-embed-text") + assert limit == 2048, "nomic-embed-text should have 2048 token limit" + + # Test nomic-embed-text-v1.5 (2048 tokens) + limit = get_model_token_limit("nomic-embed-text-v1.5") + assert limit == 2048, "nomic-embed-text-v1.5 should have 2048 token limit" + + # Test nomic-embed-text-v2 (512 tokens) + limit = get_model_token_limit("nomic-embed-text-v2") + assert limit == 512, "nomic-embed-text-v2 should have 512 token limit" + + # Test OpenAI models (8192 tokens) + limit = get_model_token_limit("text-embedding-3-small") + assert limit == 8192, "text-embedding-3-small should have 8192 token limit" + + def test_get_model_token_limit_unknown_model(self): + """Verify default token limit is returned for unknown models. + + Unknown models should return the default limit (2048) to allow + operation with reasonable safety margin. + """ + # Test with completely unknown model + limit = get_model_token_limit("unknown-model-xyz") + assert limit == 2048, "Unknown models should return default 2048" + + # Test with empty string + limit = get_model_token_limit("") + assert limit == 2048, "Empty model name should return default 2048" + + def test_get_model_token_limit_custom_default(self): + """Verify custom default can be specified for unknown models. + + Allow callers to specify their own default token limit when + model is not in the known models dictionary. + """ + limit = get_model_token_limit("unknown-model", default=4096) + assert limit == 4096, "Should return custom default for unknown models" + + # Known model should ignore custom default + limit = get_model_token_limit("nomic-embed-text", default=4096) + assert limit == 2048, "Known model should ignore custom default" + + def test_embedding_model_limits_dictionary_exists(self): + """Verify EMBEDDING_MODEL_LIMITS dictionary contains expected models. + + The dictionary should be importable and contain at least the + known nomic models with correct token limits. + """ + assert isinstance(EMBEDDING_MODEL_LIMITS, dict), "Should be a dictionary" + assert "nomic-embed-text" in EMBEDDING_MODEL_LIMITS, "Should contain nomic-embed-text" + assert "nomic-embed-text-v1.5" in EMBEDDING_MODEL_LIMITS, ( + "Should contain nomic-embed-text-v1.5" + ) + assert EMBEDDING_MODEL_LIMITS["nomic-embed-text"] == 2048 + assert EMBEDDING_MODEL_LIMITS["nomic-embed-text-v1.5"] == 2048 + assert EMBEDDING_MODEL_LIMITS["nomic-embed-text-v2"] == 512 + # OpenAI models + assert EMBEDDING_MODEL_LIMITS["text-embedding-3-small"] == 8192 + + +class TestTokenTruncation: + """Tests for truncating texts to token limits.""" + + @pytest.fixture + def tokenizer(self): + """Provide tiktoken tokenizer for token counting verification.""" + return tiktoken.get_encoding("cl100k_base") + + def test_truncate_single_text_under_limit(self, tokenizer): + """Verify text under token limit remains unchanged. + + When text is already within the token limit, it should be + returned unchanged with no truncation. + """ + text = "This is a short text that is well under the token limit." + token_count = len(tokenizer.encode(text)) + assert token_count < 100, f"Test setup: text should be short (has {token_count} tokens)" + + # Truncate with generous limit + result = truncate_to_token_limit([text], token_limit=512) + + assert len(result) == 1, "Should return same number of texts" + assert result[0] == text, "Text under limit should be unchanged" + + def test_truncate_single_text_over_limit(self, tokenizer): + """Verify text over token limit is truncated correctly. + + When text exceeds the token limit, it should be truncated to + fit within the limit while maintaining valid token boundaries. + """ + # Create a text that definitely exceeds limit + text = "word " * 200 # ~200 tokens (each "word " is typically 1-2 tokens) + original_token_count = len(tokenizer.encode(text)) + assert original_token_count > 50, ( + f"Test setup: text should be long (has {original_token_count} tokens)" + ) + + # Truncate to 50 tokens + result = truncate_to_token_limit([text], token_limit=50) + + assert len(result) == 1, "Should return same number of texts" + assert result[0] != text, "Text over limit should be truncated" + assert len(result[0]) < len(text), "Truncated text should be shorter" + + # Verify truncated text is within token limit + truncated_token_count = len(tokenizer.encode(result[0])) + assert truncated_token_count <= 50, ( + f"Truncated text should be ≀50 tokens, got {truncated_token_count}" + ) + + def test_truncate_multiple_texts_mixed_lengths(self, tokenizer): + """Verify multiple texts with mixed lengths are handled correctly. + + When processing multiple texts: + - Texts under limit should remain unchanged + - Texts over limit should be truncated independently + - Output list should maintain same order and length + """ + texts = [ + "Short text.", # Under limit + "word " * 200, # Over limit + "Another short one.", # Under limit + "token " * 150, # Over limit + ] + + # Verify test setup + for i, text in enumerate(texts): + token_count = len(tokenizer.encode(text)) + if i in [1, 3]: + assert token_count > 50, f"Text {i} should be over limit (has {token_count} tokens)" + else: + assert token_count < 50, ( + f"Text {i} should be under limit (has {token_count} tokens)" + ) + + # Truncate with 50 token limit + result = truncate_to_token_limit(texts, token_limit=50) + + assert len(result) == len(texts), "Should return same number of texts" + + # Verify each text individually + for i, (original, truncated) in enumerate(zip(texts, result)): + token_count = len(tokenizer.encode(truncated)) + assert token_count <= 50, f"Text {i} should be ≀50 tokens, got {token_count}" + + # Short texts should be unchanged + if i in [0, 2]: + assert truncated == original, f"Short text {i} should be unchanged" + # Long texts should be truncated + else: + assert len(truncated) < len(original), f"Long text {i} should be truncated" + + def test_truncate_empty_list(self): + """Verify empty input list returns empty output list. + + Edge case: empty list should return empty list without errors. + """ + result = truncate_to_token_limit([], token_limit=512) + assert result == [], "Empty input should return empty output" + + def test_truncate_preserves_order(self, tokenizer): + """Verify truncation preserves original text order. + + Output list should maintain the same order as input list, + regardless of which texts were truncated. + """ + texts = [ + "First text " * 50, # Will be truncated + "Second text.", # Won't be truncated + "Third text " * 50, # Will be truncated + ] + + result = truncate_to_token_limit(texts, token_limit=20) + + assert len(result) == 3, "Should preserve list length" + # Check that order is maintained by looking for distinctive words + assert "First" in result[0], "First text should remain in first position" + assert "Second" in result[1], "Second text should remain in second position" + assert "Third" in result[2], "Third text should remain in third position" + + def test_truncate_extremely_long_text(self, tokenizer): + """Verify extremely long texts are truncated efficiently. + + Test with text that far exceeds token limit to ensure + truncation handles extreme cases without performance issues. + """ + # Create very long text (simulate real-world scenario) + text = "token " * 5000 # ~5000+ tokens + original_token_count = len(tokenizer.encode(text)) + assert original_token_count > 1000, "Test setup: text should be very long" + + # Truncate to small limit + result = truncate_to_token_limit([text], token_limit=100) + + assert len(result) == 1 + truncated_token_count = len(tokenizer.encode(result[0])) + assert truncated_token_count <= 100, ( + f"Should truncate to ≀100 tokens, got {truncated_token_count}" + ) + assert len(result[0]) < len(text) // 10, "Should significantly reduce text length" + + def test_truncate_exact_token_limit(self, tokenizer): + """Verify text at exactly token limit is handled correctly. + + Edge case: text with exactly the token limit should either + remain unchanged or be safely truncated by 1 token. + """ + # Create text with approximately 50 tokens + # We'll adjust to get exactly 50 + target_tokens = 50 + text = "word " * 50 + tokens = tokenizer.encode(text) + + # Adjust to get exactly target_tokens + if len(tokens) > target_tokens: + tokens = tokens[:target_tokens] + text = tokenizer.decode(tokens) + elif len(tokens) < target_tokens: + # Add more words + while len(tokenizer.encode(text)) < target_tokens: + text += "word " + tokens = tokenizer.encode(text)[:target_tokens] + text = tokenizer.decode(tokens) + + # Verify we have exactly target_tokens + assert len(tokenizer.encode(text)) == target_tokens, ( + "Test setup: should have exactly 50 tokens" + ) + + result = truncate_to_token_limit([text], token_limit=target_tokens) + + assert len(result) == 1 + result_tokens = len(tokenizer.encode(result[0])) + assert result_tokens <= target_tokens, ( + f"Should be ≀{target_tokens} tokens, got {result_tokens}" + ) + + +class TestLMStudioHybridDiscovery: + """Tests for LM Studio integration in get_model_token_limit() hybrid discovery. + + These tests verify that get_model_token_limit() properly integrates with + the LM Studio SDK bridge for dynamic token limit discovery. The integration + should: + + 1. Detect LM Studio URLs (port 1234 or 'lmstudio'/'lm.studio' in URL) + 2. Convert HTTP URLs to WebSocket format for SDK queries + 3. Query LM Studio SDK and use discovered limit + 4. Fall back to registry when SDK returns None + 5. Execute AFTER Ollama detection but BEFORE registry fallback + + All tests are written in Red Phase - they should FAIL initially because the + LM Studio detection and integration logic does not exist yet in get_model_token_limit(). + """ + + def test_get_model_token_limit_lmstudio_success(self, monkeypatch): + """Verify LM Studio SDK query succeeds and returns detected limit. + + When a LM Studio base_url is detected and the SDK query succeeds, + get_model_token_limit() should return the dynamically discovered + context length without falling back to the registry. + """ + + # Mock _query_lmstudio_context_limit to return successful SDK query + def mock_query_lmstudio(model_name, base_url): + # Verify WebSocket URL was passed (not HTTP) + assert base_url.startswith("ws://"), ( + f"Should convert HTTP to WebSocket format, got: {base_url}" + ) + return 8192 # Successful SDK query + + monkeypatch.setattr( + "leann.embedding_compute._query_lmstudio_context_limit", + mock_query_lmstudio, + ) + + # Test with HTTP URL that should be converted to WebSocket + limit = get_model_token_limit( + model_name="custom-model", base_url="http://localhost:1234/v1" + ) + + assert limit == 8192, "Should return limit from LM Studio SDK query" + + def test_get_model_token_limit_lmstudio_fallback_to_registry(self, monkeypatch): + """Verify fallback to registry when LM Studio SDK returns None. + + When LM Studio SDK query fails (returns None), get_model_token_limit() + should fall back to the EMBEDDING_MODEL_LIMITS registry. + """ + + # Mock _query_lmstudio_context_limit to return None (SDK failure) + def mock_query_lmstudio(model_name, base_url): + return None # SDK query failed + + monkeypatch.setattr( + "leann.embedding_compute._query_lmstudio_context_limit", + mock_query_lmstudio, + ) + + # Test with known model that exists in registry + limit = get_model_token_limit( + model_name="nomic-embed-text", base_url="http://localhost:1234/v1" + ) + + # Should fall back to registry value + assert limit == 2048, "Should fall back to registry when SDK returns None" + + def test_get_model_token_limit_lmstudio_port_detection(self, monkeypatch): + """Verify detection of LM Studio via port 1234. + + get_model_token_limit() should recognize port 1234 as a LM Studio + server and attempt SDK query, regardless of hostname. + """ + query_called = False + + def mock_query_lmstudio(model_name, base_url): + nonlocal query_called + query_called = True + return 4096 + + monkeypatch.setattr( + "leann.embedding_compute._query_lmstudio_context_limit", + mock_query_lmstudio, + ) + + # Test with port 1234 (default LM Studio port) + limit = get_model_token_limit(model_name="test-model", base_url="http://127.0.0.1:1234/v1") + + assert query_called, "Should detect port 1234 and call LM Studio SDK query" + assert limit == 4096, "Should return SDK query result" + + @pytest.mark.parametrize( + "test_url,expected_limit,keyword", + [ + ("http://lmstudio.local:8080/v1", 16384, "lmstudio"), + ("http://api.lm.studio:5000/v1", 32768, "lm.studio"), + ], + ) + def test_get_model_token_limit_lmstudio_url_keyword_detection( + self, monkeypatch, test_url, expected_limit, keyword + ): + """Verify detection of LM Studio via keywords in URL. + + get_model_token_limit() should recognize 'lmstudio' or 'lm.studio' + in the URL as indicating a LM Studio server. + """ + query_called = False + + def mock_query_lmstudio(model_name, base_url): + nonlocal query_called + query_called = True + return expected_limit + + monkeypatch.setattr( + "leann.embedding_compute._query_lmstudio_context_limit", + mock_query_lmstudio, + ) + + limit = get_model_token_limit(model_name="test-model", base_url=test_url) + + assert query_called, f"Should detect '{keyword}' keyword and call SDK query" + assert limit == expected_limit, f"Should return SDK query result for {keyword}" + + @pytest.mark.parametrize( + "input_url,expected_protocol,expected_host", + [ + ("http://localhost:1234/v1", "ws://", "localhost:1234"), + ("https://lmstudio.example.com:1234/v1", "wss://", "lmstudio.example.com:1234"), + ], + ) + def test_get_model_token_limit_protocol_conversion( + self, monkeypatch, input_url, expected_protocol, expected_host + ): + """Verify HTTP/HTTPS URL is converted to WebSocket format for SDK query. + + LM Studio SDK requires WebSocket URLs. get_model_token_limit() should: + 1. Convert 'http://' to 'ws://' + 2. Convert 'https://' to 'wss://' + 3. Remove '/v1' or other path suffixes (SDK expects base URL) + 4. Preserve host and port + """ + conversions_tested = [] + + def mock_query_lmstudio(model_name, base_url): + conversions_tested.append(base_url) + return 8192 + + monkeypatch.setattr( + "leann.embedding_compute._query_lmstudio_context_limit", + mock_query_lmstudio, + ) + + get_model_token_limit(model_name="test-model", base_url=input_url) + + # Verify conversion happened + assert len(conversions_tested) == 1, "Should have called SDK query once" + assert conversions_tested[0].startswith(expected_protocol), ( + f"Should convert to {expected_protocol}" + ) + assert expected_host in conversions_tested[0], ( + f"Should preserve host and port: {expected_host}" + ) + + def test_get_model_token_limit_lmstudio_executes_after_ollama(self, monkeypatch): + """Verify LM Studio detection happens AFTER Ollama detection. + + The hybrid discovery order should be: + 1. Ollama dynamic discovery (port 11434 or 'ollama' in URL) + 2. LM Studio dynamic discovery (port 1234 or 'lmstudio' in URL) + 3. Registry fallback + + If both Ollama and LM Studio patterns match, Ollama should take precedence. + This test verifies that LM Studio is checked but doesn't interfere with Ollama. + """ + ollama_called = False + lmstudio_called = False + + def mock_query_ollama(model_name, base_url): + nonlocal ollama_called + ollama_called = True + return 2048 # Ollama query succeeds + + def mock_query_lmstudio(model_name, base_url): + nonlocal lmstudio_called + lmstudio_called = True + return None # Should not be reached if Ollama succeeds + + monkeypatch.setattr( + "leann.embedding_compute._query_ollama_context_limit", + mock_query_ollama, + ) + monkeypatch.setattr( + "leann.embedding_compute._query_lmstudio_context_limit", + mock_query_lmstudio, + ) + + # Test with Ollama URL + limit = get_model_token_limit( + model_name="test-model", base_url="http://localhost:11434/api" + ) + + assert ollama_called, "Should attempt Ollama query first" + assert not lmstudio_called, "Should not attempt LM Studio query when Ollama succeeds" + assert limit == 2048, "Should return Ollama result" + + def test_get_model_token_limit_lmstudio_not_detected_for_non_lmstudio_urls(self, monkeypatch): + """Verify LM Studio SDK query is NOT called for non-LM Studio URLs. + + Only URLs with port 1234 or 'lmstudio'/'lm.studio' keywords should + trigger LM Studio SDK queries. Other URLs should skip to registry fallback. + """ + lmstudio_called = False + + def mock_query_lmstudio(model_name, base_url): + nonlocal lmstudio_called + lmstudio_called = True + return 8192 + + monkeypatch.setattr( + "leann.embedding_compute._query_lmstudio_context_limit", + mock_query_lmstudio, + ) + + # Test with non-LM Studio URLs + test_cases = [ + "http://localhost:8080/v1", # Different port + "http://openai.example.com/v1", # Different service + "http://localhost:3000/v1", # Another port + ] + + for base_url in test_cases: + lmstudio_called = False # Reset for each test + get_model_token_limit(model_name="nomic-embed-text", base_url=base_url) + assert not lmstudio_called, f"Should NOT call LM Studio SDK for URL: {base_url}" + + def test_get_model_token_limit_lmstudio_case_insensitive_detection(self, monkeypatch): + """Verify LM Studio detection is case-insensitive for keywords. + + Keywords 'lmstudio' and 'lm.studio' should be detected regardless + of case (LMStudio, LMSTUDIO, LmStudio, etc.). + """ + query_called = False + + def mock_query_lmstudio(model_name, base_url): + nonlocal query_called + query_called = True + return 8192 + + monkeypatch.setattr( + "leann.embedding_compute._query_lmstudio_context_limit", + mock_query_lmstudio, + ) + + # Test various case variations + test_cases = [ + "http://LMStudio.local:8080/v1", + "http://LMSTUDIO.example.com/v1", + "http://LmStudio.local/v1", + "http://api.LM.STUDIO:5000/v1", + ] + + for base_url in test_cases: + query_called = False # Reset for each test + limit = get_model_token_limit(model_name="test-model", base_url=base_url) + assert query_called, f"Should detect LM Studio in URL: {base_url}" + assert limit == 8192, f"Should return SDK result for URL: {base_url}" + + +class TestTokenLimitCaching: + """Tests for token limit caching to prevent repeated SDK/API calls. + + Caching prevents duplicate SDK/API calls within the same Python process, + which is important because: + 1. LM Studio SDK load() can load duplicate model instances + 2. Ollama /api/show queries add latency + 3. Registry lookups are pure overhead + + Cache is process-scoped and resets between leann build invocations. + """ + + def setup_method(self): + """Clear cache before each test.""" + from leann.embedding_compute import _token_limit_cache + + _token_limit_cache.clear() + + def test_registry_lookup_is_cached(self): + """Verify that registry lookups are cached.""" + from leann.embedding_compute import _token_limit_cache + + # First call + limit1 = get_model_token_limit("text-embedding-3-small") + assert limit1 == 8192 + + # Verify it's in cache + cache_key = ("text-embedding-3-small", "") + assert cache_key in _token_limit_cache + assert _token_limit_cache[cache_key] == 8192 + + # Second call should use cache + limit2 = get_model_token_limit("text-embedding-3-small") + assert limit2 == 8192 + + def test_default_fallback_is_cached(self): + """Verify that default fallbacks are cached.""" + from leann.embedding_compute import _token_limit_cache + + # First call with unknown model + limit1 = get_model_token_limit("unknown-model-xyz", default=512) + assert limit1 == 512 + + # Verify it's in cache + cache_key = ("unknown-model-xyz", "") + assert cache_key in _token_limit_cache + assert _token_limit_cache[cache_key] == 512 + + # Second call should use cache + limit2 = get_model_token_limit("unknown-model-xyz", default=512) + assert limit2 == 512 + + def test_different_urls_create_separate_cache_entries(self): + """Verify that different base_urls create separate cache entries.""" + from leann.embedding_compute import _token_limit_cache + + # Same model, different URLs + limit1 = get_model_token_limit("nomic-embed-text", base_url="http://localhost:11434") + limit2 = get_model_token_limit("nomic-embed-text", base_url="http://localhost:1234/v1") + + # Both should find the model in registry (2048) + assert limit1 == 2048 + assert limit2 == 2048 + + # But they should be separate cache entries + cache_key1 = ("nomic-embed-text", "http://localhost:11434") + cache_key2 = ("nomic-embed-text", "http://localhost:1234/v1") + + assert cache_key1 in _token_limit_cache + assert cache_key2 in _token_limit_cache + assert len(_token_limit_cache) == 2 + + def test_cache_prevents_repeated_lookups(self): + """Verify that cache prevents repeated registry/API lookups.""" + from leann.embedding_compute import _token_limit_cache + + model_name = "text-embedding-ada-002" + + # First call - should add to cache + assert len(_token_limit_cache) == 0 + limit1 = get_model_token_limit(model_name) + + cache_size_after_first = len(_token_limit_cache) + assert cache_size_after_first == 1 + + # Multiple subsequent calls - cache size should not change + for _ in range(5): + limit = get_model_token_limit(model_name) + assert limit == limit1 + assert len(_token_limit_cache) == cache_size_after_first + + def test_versioned_model_names_cached_correctly(self): + """Verify that versioned model names (e.g., model:tag) are cached.""" + from leann.embedding_compute import _token_limit_cache + + # Model with version tag + limit = get_model_token_limit("nomic-embed-text:latest", base_url="http://localhost:11434") + assert limit == 2048 + + # Should be cached with full name including version + cache_key = ("nomic-embed-text:latest", "http://localhost:11434") + assert cache_key in _token_limit_cache + assert _token_limit_cache[cache_key] == 2048 diff --git a/tests/test_watch_sync_scope.py b/tests/test_watch_sync_scope.py new file mode 100644 index 0000000..7c30602 --- /dev/null +++ b/tests/test_watch_sync_scope.py @@ -0,0 +1,83 @@ +"""Regression tests for leann watch sync scope (#345).""" + +from leann.cli import LeannCLI +from leann.sync import FileSynchronizer + + +def test_resolve_sync_scope_keeps_loose_files_separate(tmp_path): + repo = tmp_path / "repo" + src = repo / "src" + src.mkdir(parents=True) + readme = repo / "README.md" + readme.write_text("# hello", encoding="utf-8") + + cli = LeannCLI() + directories, files = cli._resolve_sync_scope([str(src), str(readme)]) + + assert directories == [str(src.resolve())] + assert files == [str(readme.resolve())] + + +def test_watch_scope_does_not_scan_sibling_media(tmp_path): + repo = tmp_path / "repo" + src = repo / "src" + assets = repo / "assets" + src.mkdir(parents=True) + assets.mkdir() + readme = repo / "README.md" + readme.write_text("# hello", encoding="utf-8") + (src / "main.py").write_text("print('ok')", encoding="utf-8") + (assets / "icon.png").write_bytes(b"png") + + synchronizers = [ + FileSynchronizer( + root_dir=str(src), + include_extensions=[".py", ".md"], + snapshot_path=str(tmp_path / "sync_src.pickle"), + auto_load=False, + ), + FileSynchronizer( + explicit_files=[str(readme.resolve())], + include_extensions=[".py", ".md"], + snapshot_path=str(tmp_path / "sync_readme.pickle"), + auto_load=False, + ), + ] + + hashed_paths: set[str] = set() + for fs in synchronizers: + hashed_paths.update(fs.generate_file_hashes().keys()) + + assert str((src / "main.py").resolve()) in hashed_paths + assert str(readme.resolve()) in hashed_paths + assert str((assets / "icon.png").resolve()) not in hashed_paths + + +def test_mixed_txt_and_bin_directory_skips_bin_without_crash(tmp_path): + """Same dir with .txt and .bin: hash only text, ignore binary (review #377).""" + docs = tmp_path / "docs" + docs.mkdir() + txt = docs / "notes.txt" + bin_file = docs / "payload.bin" + txt.write_text("hello", encoding="utf-8") + bin_file.write_bytes(bytes(range(256))) + + fs = FileSynchronizer( + root_dir=str(docs), + include_extensions=[".txt"], + snapshot_path=str(tmp_path / "sync.pickle"), + auto_load=False, + ) + + hashes = fs.generate_file_hashes() + assert set(hashes.keys()) == {str(txt.resolve())} + assert str(bin_file.resolve()) not in hashes + + fs.create_snapshot() + fs2 = FileSynchronizer( + root_dir=str(docs), + include_extensions=[".txt"], + snapshot_path=str(tmp_path / "sync.pickle"), + ) + added, removed, modified = fs2.detect_changes() + assert not added and not removed and not modified diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..cd7fa7e --- /dev/null +++ b/uv.lock @@ -0,0 +1,6072 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "accelerate" +version = "1.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/72/ff3961c19ee395c3d30ac630ee77bfb0e1b46b87edc504d4f83bb4a89705/accelerate-1.10.1.tar.gz", hash = "sha256:3dea89e433420e4bfac0369cae7e36dcd6a56adfcfd38cdda145c6225eab5df8", size = 392446, upload-time = "2025-08-25T13:57:06.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/a0/d9ef19f780f319c21ee90ecfef4431cbeeca95bec7f14071785c17b6029b/accelerate-1.10.1-py3-none-any.whl", hash = "sha256:3621cff60b9a27ce798857ece05e2b9f56fcc71631cfb31ccf71f0359c311f11", size = 374909, upload-time = "2025-08-25T13:57:04.55Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/dc/ef9394bde9080128ad401ac7ede185267ed637df03b51f05d14d1c99ad67/aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", size = 703921, upload-time = "2025-07-29T05:49:43.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/42/63fccfc3a7ed97eb6e1a71722396f409c46b60a0552d8a56d7aad74e0df5/aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", size = 480288, upload-time = "2025-07-29T05:49:47.851Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a2/7b8a020549f66ea2a68129db6960a762d2393248f1994499f8ba9728bbed/aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", size = 468063, upload-time = "2025-07-29T05:49:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f5/d11e088da9176e2ad8220338ae0000ed5429a15f3c9dfd983f39105399cd/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", size = 1650122, upload-time = "2025-07-29T05:49:51.874Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6b/b60ce2757e2faed3d70ed45dafee48cee7bfb878785a9423f7e883f0639c/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", size = 1624176, upload-time = "2025-07-29T05:49:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/dd/de/8c9fde2072a1b72c4fadecf4f7d4be7a85b1d9a4ab333d8245694057b4c6/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", size = 1696583, upload-time = "2025-07-29T05:49:55.338Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ad/07f863ca3d895a1ad958a54006c6dafb4f9310f8c2fdb5f961b8529029d3/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", size = 1738896, upload-time = "2025-07-29T05:49:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/20/43/2bd482ebe2b126533e8755a49b128ec4e58f1a3af56879a3abdb7b42c54f/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", size = 1643561, upload-time = "2025-07-29T05:49:58.762Z" }, + { url = "https://files.pythonhosted.org/packages/23/40/2fa9f514c4cf4cbae8d7911927f81a1901838baf5e09a8b2c299de1acfe5/aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", size = 1583685, upload-time = "2025-07-29T05:50:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c3/94dc7357bc421f4fb978ca72a201a6c604ee90148f1181790c129396ceeb/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d", size = 1627533, upload-time = "2025-07-29T05:50:02.306Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3f/1f8911fe1844a07001e26593b5c255a685318943864b27b4e0267e840f95/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", size = 1638319, upload-time = "2025-07-29T05:50:04.282Z" }, + { url = "https://files.pythonhosted.org/packages/4e/46/27bf57a99168c4e145ffee6b63d0458b9c66e58bb70687c23ad3d2f0bd17/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", size = 1613776, upload-time = "2025-07-29T05:50:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/1d2d9061a574584bb4ad3dbdba0da90a27fdc795bc227def3a46186a8bc1/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", size = 1693359, upload-time = "2025-07-29T05:50:07.563Z" }, + { url = "https://files.pythonhosted.org/packages/08/98/bee429b52233c4a391980a5b3b196b060872a13eadd41c3a34be9b1469ed/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", size = 1716598, upload-time = "2025-07-29T05:50:09.33Z" }, + { url = "https://files.pythonhosted.org/packages/57/39/b0314c1ea774df3392751b686104a3938c63ece2b7ce0ba1ed7c0b4a934f/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", size = 1644940, upload-time = "2025-07-29T05:50:11.334Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/3dacb8d3f8f512c8ca43e3fa8a68b20583bd25636ffa4e56ee841ffd79ae/aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", size = 429239, upload-time = "2025-07-29T05:50:12.803Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f9/470b5daba04d558c9673ca2034f28d067f3202a40e17804425f0c331c89f/aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", size = 452297, upload-time = "2025-07-29T05:50:14.266Z" }, + { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246, upload-time = "2025-07-29T05:50:15.937Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515, upload-time = "2025-07-29T05:50:17.442Z" }, + { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776, upload-time = "2025-07-29T05:50:19.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977, upload-time = "2025-07-29T05:50:21.665Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645, upload-time = "2025-07-29T05:50:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437, upload-time = "2025-07-29T05:50:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482, upload-time = "2025-07-29T05:50:26.693Z" }, + { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944, upload-time = "2025-07-29T05:50:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020, upload-time = "2025-07-29T05:50:30.032Z" }, + { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292, upload-time = "2025-07-29T05:50:31.983Z" }, + { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451, upload-time = "2025-07-29T05:50:33.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634, upload-time = "2025-07-29T05:50:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238, upload-time = "2025-07-29T05:50:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701, upload-time = "2025-07-29T05:50:39.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758, upload-time = "2025-07-29T05:50:41.292Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868, upload-time = "2025-07-29T05:50:43.063Z" }, + { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273, upload-time = "2025-07-29T05:50:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aiosqlite" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "astchunk" +version = "0.1.0" +source = { editable = "packages/astchunk-leann" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pyrsistent" }, + { name = "tree-sitter" }, + { name = "tree-sitter-c-sharp" }, + { name = "tree-sitter-java" }, + { name = "tree-sitter-python" }, + { name = "tree-sitter-typescript" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = ">=22.0.0" }, + { name = "flake8", marker = "extra == 'dev'", specifier = ">=5.0.0" }, + { name = "isort", marker = "extra == 'dev'", specifier = ">=5.10.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=0.18.0" }, + { name = "numpy", specifier = ">=1.20.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=2.20.0" }, + { name = "pyrsistent", specifier = ">=0.18.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.0.0" }, + { name = "pytest-xdist", marker = "extra == 'test'", specifier = ">=2.5.0" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=5.0.0" }, + { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=1.0.0" }, + { name = "tree-sitter", specifier = ">=0.20.0" }, + { name = "tree-sitter-c-sharp", specifier = ">=0.20.0" }, + { name = "tree-sitter-java", specifier = ">=0.20.0" }, + { name = "tree-sitter-python", specifier = ">=0.20.0" }, + { name = "tree-sitter-typescript", specifier = ">=0.20.0" }, +] +provides-extras = ["dev", "docs", "test"] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "backports-strenum" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/c7/2ed54c32fed313591ffb21edbd48db71e68827d43a61938e5a0bc2b6ec91/backports_strenum-1.3.1.tar.gz", hash = "sha256:77c52407342898497714f0596e86188bb7084f89063226f4ba66863482f42414", size = 7257, upload-time = "2023-12-09T14:36:40.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/56cf20e2ee5127b603b81d5a69580a1a325083e2b921aa8f067da83927c0/backports_strenum-1.3.1-py3-none-any.whl", hash = "sha256:cdcfe36dc897e2615dc793b7d3097f54d359918fc448754a517e6f23044ccf83", size = 8304, upload-time = "2023-12-09T14:36:39.905Z" }, +] + +[[package]] +name = "banks" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "griffe" }, + { name = "jinja2" }, + { name = "platformdirs" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/f8/25ef24814f77f3fd7f0fd3bd1ef3749e38a9dbd23502fbb53034de49900c/banks-2.2.0.tar.gz", hash = "sha256:d1446280ce6e00301e3e952dd754fd8cee23ff277d29ed160994a84d0d7ffe62", size = 179052, upload-time = "2025-07-18T16:28:26.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/d6/f9168956276934162ec8d48232f9920f2985ee45aa7602e3c6b4bc203613/banks-2.2.0-py3-none-any.whl", hash = "sha256:963cd5c85a587b122abde4f4064078def35c50c688c1b9d36f43c92503854e7d", size = 29244, upload-time = "2025-07-18T16:28:27.835Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, +] + +[[package]] +name = "bleach" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083, upload-time = "2024-10-29T18:30:40.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406, upload-time = "2024-10-29T18:30:38.186Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "boto3" +version = "1.40.37" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/7c/c0df9efe277586b3170b64011faef1a86d506e63c4c9fa69b420261e711f/boto3-1.40.37.tar.gz", hash = "sha256:222de1ac6b878c2a2ea75ecfdd876ff3e8bd71930a5f9a3631379dfacc402eda", size = 111595, upload-time = "2025-09-23T19:29:51.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/bb/c0a1e2109da6cb169de85676bbee9b274f5d0550cff9752d76782d5f54c0/boto3-1.40.37-py3-none-any.whl", hash = "sha256:ccc5aa217e41bcc80e243c12e6b4330a010b5752cdda2618d67d0b854cd730d5", size = 139346, upload-time = "2025-09-23T19:29:49.771Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.37" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/16/d5817f8abed763ca084471ed6055be558f76e35901d684b78c9ff9270694/botocore-1.40.37.tar.gz", hash = "sha256:9d2cfc41963a3a8be8ece395292e29afad2e25aa602d02bc0c0c3a598405a318", size = 14353930, upload-time = "2025-09-23T19:29:42.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ec/d214e91f86bc05821a735ce192e829440f2298ab9a891cc7db1a470211fe/botocore-1.40.37-py3-none-any.whl", hash = "sha256:9d4cee2ae0fe273003c6d1a8c9f7b98c4e40a6f74ba2f37eacba27d97fb7734f", size = 14024050, upload-time = "2025-09-23T19:29:39.626Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198, upload-time = "2025-09-17T00:10:35.797Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/8c/44ee01267ec01e26e43ebfdae3f120ec2312aa72fa4c0507ebe41a26739f/cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475", size = 7285044, upload-time = "2025-09-17T00:08:36.807Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/9ae689a25047e0601adfcb159ec4f83c0b4149fdb5c3030cc94cd218141d/cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080", size = 4308182, upload-time = "2025-09-17T00:08:39.388Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/ca6cc9df7118f2fcd142c76b1da0f14340d77518c05b1ebfbbabca6b9e7d/cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e", size = 4572393, upload-time = "2025-09-17T00:08:41.663Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a3/0f5296f63815d8e985922b05c31f77ce44787b3127a67c0b7f70f115c45f/cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6", size = 4308400, upload-time = "2025-09-17T00:08:43.559Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8c/74fcda3e4e01be1d32775d5b4dd841acaac3c1b8fa4d0774c7ac8d52463d/cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8", size = 4015786, upload-time = "2025-09-17T00:08:45.758Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b8/85d23287baeef273b0834481a3dd55bbed3a53587e3b8d9f0898235b8f91/cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28", size = 4982606, upload-time = "2025-09-17T00:08:47.602Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/de61ad5b52433b389afca0bc70f02a7a1f074651221f599ce368da0fe437/cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9", size = 4604234, upload-time = "2025-09-17T00:08:49.879Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1f/dbd4d6570d84748439237a7478d124ee0134bf166ad129267b7ed8ea6d22/cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736", size = 4307669, upload-time = "2025-09-17T00:08:52.321Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fd/ca0a14ce7f0bfe92fa727aacaf2217eb25eb7e4ed513b14d8e03b26e63ed/cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b", size = 4947579, upload-time = "2025-09-17T00:08:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/09c30543bb93401f6f88fce556b3bdbb21e55ae14912c04b7bf355f5f96c/cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab", size = 4603669, upload-time = "2025-09-17T00:08:57.16Z" }, + { url = "https://files.pythonhosted.org/packages/23/9a/38cb01cb09ce0adceda9fc627c9cf98eb890fc8d50cacbe79b011df20f8a/cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75", size = 4435828, upload-time = "2025-09-17T00:08:59.606Z" }, + { url = "https://files.pythonhosted.org/packages/0f/53/435b5c36a78d06ae0bef96d666209b0ecd8f8181bfe4dda46536705df59e/cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5", size = 4709553, upload-time = "2025-09-17T00:09:01.832Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c4/0da6e55595d9b9cd3b6eb5dc22f3a07ded7f116a3ea72629cab595abb804/cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0", size = 3058327, upload-time = "2025-09-17T00:09:03.726Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/cd29a35e0d6e78a0ee61793564c8cff0929c38391cb0de27627bdc7525aa/cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7", size = 3523893, upload-time = "2025-09-17T00:09:06.272Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dd/eea390f3e78432bc3d2f53952375f8b37cb4d37783e626faa6a51e751719/cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0", size = 2932145, upload-time = "2025-09-17T00:09:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fb/c73588561afcd5e24b089952bd210b14676c0c5bf1213376350ae111945c/cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee", size = 7193928, upload-time = "2025-09-17T00:09:10.595Z" }, + { url = "https://files.pythonhosted.org/packages/26/34/0ff0bb2d2c79f25a2a63109f3b76b9108a906dd2a2eb5c1d460b9938adbb/cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd", size = 4293515, upload-time = "2025-09-17T00:09:12.861Z" }, + { url = "https://files.pythonhosted.org/packages/df/b7/d4f848aee24ecd1be01db6c42c4a270069a4f02a105d9c57e143daf6cf0f/cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a", size = 4545619, upload-time = "2025-09-17T00:09:15.397Z" }, + { url = "https://files.pythonhosted.org/packages/44/a5/42fedefc754fd1901e2d95a69815ea4ec8a9eed31f4c4361fcab80288661/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a", size = 4299160, upload-time = "2025-09-17T00:09:17.155Z" }, + { url = "https://files.pythonhosted.org/packages/86/a1/cd21174f56e769c831fbbd6399a1b7519b0ff6280acec1b826d7b072640c/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a", size = 3994491, upload-time = "2025-09-17T00:09:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/a8cbfa1c029987ddc746fd966711d4fa71efc891d37fbe9f030fe5ab4eec/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12", size = 4960157, upload-time = "2025-09-17T00:09:20.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/ae/63a84e6789e0d5a2502edf06b552bcb0fa9ff16147265d5c44a211942abe/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129", size = 4577263, upload-time = "2025-09-17T00:09:23.356Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8f/1b9fa8e92bd9cbcb3b7e1e593a5232f2c1e6f9bd72b919c1a6b37d315f92/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da", size = 4298703, upload-time = "2025-09-17T00:09:25.566Z" }, + { url = "https://files.pythonhosted.org/packages/c3/af/bb95db070e73fea3fae31d8a69ac1463d89d1c084220f549b00dd01094a8/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b", size = 4926363, upload-time = "2025-09-17T00:09:27.451Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3b/d8fb17ffeb3a83157a1cc0aa5c60691d062aceecba09c2e5e77ebfc1870c/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657", size = 4576958, upload-time = "2025-09-17T00:09:29.924Z" }, + { url = "https://files.pythonhosted.org/packages/d9/46/86bc3a05c10c8aa88c8ae7e953a8b4e407c57823ed201dbcba55c4d655f4/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0", size = 4422507, upload-time = "2025-09-17T00:09:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4e/387e5a21dfd2b4198e74968a541cfd6128f66f8ec94ed971776e15091ac3/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0", size = 4683964, upload-time = "2025-09-17T00:09:34.118Z" }, + { url = "https://files.pythonhosted.org/packages/25/a3/f9f5907b166adb8f26762071474b38bbfcf89858a5282f032899075a38a1/cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277", size = 3029705, upload-time = "2025-09-17T00:09:36.381Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/4d3a4f1850db2e71c2b1628d14b70b5e4c1684a1bd462f7fffb93c041c38/cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05", size = 3502175, upload-time = "2025-09-17T00:09:38.261Z" }, + { url = "https://files.pythonhosted.org/packages/52/c7/9f10ad91435ef7d0d99a0b93c4360bea3df18050ff5b9038c489c31ac2f5/cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784", size = 2912354, upload-time = "2025-09-17T00:09:40.078Z" }, + { url = "https://files.pythonhosted.org/packages/98/e5/fbd632385542a3311915976f88e0dfcf09e62a3fc0aff86fb6762162a24d/cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b", size = 7255677, upload-time = "2025-09-17T00:09:42.407Z" }, + { url = "https://files.pythonhosted.org/packages/56/3e/13ce6eab9ad6eba1b15a7bd476f005a4c1b3f299f4c2f32b22408b0edccf/cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8", size = 4301110, upload-time = "2025-09-17T00:09:45.614Z" }, + { url = "https://files.pythonhosted.org/packages/a2/67/65dc233c1ddd688073cf7b136b06ff4b84bf517ba5529607c9d79720fc67/cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead", size = 4562369, upload-time = "2025-09-17T00:09:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/17/db/d64ae4c6f4e98c3dac5bf35dd4d103f4c7c345703e43560113e5e8e31b2b/cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2", size = 4302126, upload-time = "2025-09-17T00:09:49.335Z" }, + { url = "https://files.pythonhosted.org/packages/3d/19/5f1eea17d4805ebdc2e685b7b02800c4f63f3dd46cfa8d4c18373fea46c8/cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32", size = 4009431, upload-time = "2025-09-17T00:09:51.239Z" }, + { url = "https://files.pythonhosted.org/packages/81/b5/229ba6088fe7abccbfe4c5edb96c7a5ad547fac5fdd0d40aa6ea540b2985/cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef", size = 4980739, upload-time = "2025-09-17T00:09:54.181Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9c/50aa38907b201e74bc43c572f9603fa82b58e831bd13c245613a23cff736/cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0", size = 4592289, upload-time = "2025-09-17T00:09:56.731Z" }, + { url = "https://files.pythonhosted.org/packages/5a/33/229858f8a5bb22f82468bb285e9f4c44a31978d5f5830bb4ea1cf8a4e454/cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128", size = 4301815, upload-time = "2025-09-17T00:09:58.548Z" }, + { url = "https://files.pythonhosted.org/packages/52/cb/b76b2c87fbd6ed4a231884bea3ce073406ba8e2dae9defad910d33cbf408/cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca", size = 4943251, upload-time = "2025-09-17T00:10:00.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/0f/f66125ecf88e4cb5b8017ff43f3a87ede2d064cb54a1c5893f9da9d65093/cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc", size = 4591247, upload-time = "2025-09-17T00:10:02.874Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/9f3134ae436b63b463cfdf0ff506a0570da6873adb4bf8c19b8a5b4bac64/cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7", size = 4428534, upload-time = "2025-09-17T00:10:04.994Z" }, + { url = "https://files.pythonhosted.org/packages/89/39/e6042bcb2638650b0005c752c38ea830cbfbcbb1830e4d64d530000aa8dc/cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a", size = 4699541, upload-time = "2025-09-17T00:10:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779, upload-time = "2025-09-17T00:10:08.951Z" }, + { url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226, upload-time = "2025-09-17T00:10:10.769Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" }, + { url = "https://files.pythonhosted.org/packages/14/b9/b260180b31a66859648cfed5c980544ee22b15f8bd20ef82a23f58c0b83e/cryptography-46.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd4b5e2ee4e60425711ec65c33add4e7a626adef79d66f62ba0acfd493af282d", size = 3714683, upload-time = "2025-09-17T00:10:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5a/1cd3ef86e5884edcbf8b27c3aa8f9544e9b9fcce5d3ed8b86959741f4f8e/cryptography-46.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48948940d0ae00483e85e9154bb42997d0b77c21e43a77b7773c8c80de532ac5", size = 3443784, upload-time = "2025-09-17T00:10:18.014Z" }, + { url = "https://files.pythonhosted.org/packages/27/27/077e09fd92075dd1338ea0ffaf5cfee641535545925768350ad90d8c36ca/cryptography-46.0.1-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9c79af2c3058430d911ff1a5b2b96bbfe8da47d5ed961639ce4681886614e70", size = 3722319, upload-time = "2025-09-17T00:10:20.273Z" }, + { url = "https://files.pythonhosted.org/packages/db/32/6fc7250280920418651640d76cee34d91c1e0601d73acd44364570cf041f/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0ca4be2af48c24df689a150d9cd37404f689e2968e247b6b8ff09bff5bcd786f", size = 4249030, upload-time = "2025-09-17T00:10:22.396Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/8d5398b2da15a15110b2478480ab512609f95b45ead3a105c9a9c76f9980/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:13e67c4d3fb8b6bc4ef778a7ccdd8df4cd15b4bcc18f4239c8440891a11245cc", size = 4528009, upload-time = "2025-09-17T00:10:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1c/4012edad2a8977ab386c36b6e21f5065974d37afa3eade83a9968cba4855/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:15b5fd9358803b0d1cc42505a18d8bca81dabb35b5cfbfea1505092e13a9d96d", size = 4248902, upload-time = "2025-09-17T00:10:26.255Z" }, + { url = "https://files.pythonhosted.org/packages/58/a3/257cd5ae677302de8fa066fca9de37128f6729d1e63c04dd6a15555dd450/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e34da95e29daf8a71cb2841fd55df0511539a6cdf33e6f77c1e95e44006b9b46", size = 4527150, upload-time = "2025-09-17T00:10:28.28Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cd/fe6b65e1117ec7631f6be8951d3db076bac3e1b096e3e12710ed071ffc3c/cryptography-46.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34f04b7311174469ab3ac2647469743720f8b6c8b046f238e5cb27905695eb2a", size = 3448210, upload-time = "2025-09-17T00:10:30.145Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/21/8464d133752951c154feafb3b65c297e7d80f301183d220bec4c830f1441/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120fcc53d57903df529c3486962c56528cba5b7d6c57c99537320ed9922c8b86", size = 6073403, upload-time = "2026-05-29T23:11:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/a8/1f/5ef51f5fbaa5d4d3201bb3d7555af028ec1aa4416275ccbf73c9e34e3d2d/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9851b0caa8bfd3bc6fa054eaf57bea7c8e9c3a62db2d2621224677f49f3c53d0", size = 6675244, upload-time = "2026-05-29T23:11:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/fc/64/bb17e4d168569ef7be05c44474fe3dc19278d60a69ba228e45a431c86444/cuda_bindings-13.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:c0c4b1a995098c46695c24257a342dc97d6e6d3f3050b944c9f43bd26d734051", size = 5625597, upload-time = "2026-05-29T23:11:40.808Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" }, + { url = "https://files.pythonhosted.org/packages/93/f7/0e35987a21914f84068061dcf4b61466ccbce1c62ddc9727596d5ed0c26f/cuda_bindings-13.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:507b0e19e7f934c5e30f30f0244ad70a75812619a7d3a0d742543caae1bd50f1", size = 5664286, upload-time = "2026-05-29T23:11:47.719Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/7c/95/872a0392122f1fb43fcb06869790ef3171f37beee9f7db8f441739113570/cuda_bindings-13.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:b134dd8c5c66ae4c4ad814f7aee88fd215353c077010cbc47e3b55ed35ec9eff", size = 5875099, upload-time = "2026-05-29T23:11:54.635Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2f/6a0dd496550c6fafbf6aeb1bf40242eeabb2fd138a43892aabb4be8224c2/cuda_bindings-13.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:18c8c167c8907b8f02531ca810534315c458dabef31f7965095619bf647b9202", size = 5830027, upload-time = "2026-05-29T23:12:01.205Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/2734be44dbc80ac082ec23a86b41c8294992dcb90033645ed1bc50aafe4c/cuda_bindings-13.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:8de12ef60bf40756852cb62bbb40460609269f6ece522903d1cc93d73a3ececb", size = 5961055, upload-time = "2026-05-29T23:12:07.971Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" }, + { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" }, + { url = "https://files.pythonhosted.org/packages/27/2a/b59bcac016ab9985d6b48a5d05b0d698461a159ca03ee11c4abd54da2ac4/cuda_bindings-13.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61120b5e4f4a63f67efd7e7396914cb9ef871bb1f0021e990fb70277be240a4d", size = 6740329, upload-time = "2026-05-29T23:12:15.153Z" }, +] + +[[package]] +name = "cuda-core" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-strenum", marker = "python_full_version < '3.11'" }, + { name = "cuda-pathfinder" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/21/ef85f3e15d394c9ca41fe116d78cd9e28533b9d7ead842f9241b332acf01/cuda_core-1.0.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9632db74eceb1cd72a7c95b61a5e4cfb9cc2291de0503e170334d936cab3316", size = 4788165, upload-time = "2026-05-12T20:11:17.116Z" }, + { url = "https://files.pythonhosted.org/packages/e0/41/c2c07b313c6cbb5d93010200c62b01ddb9f6c6f43a096a75c7b902c42ad6/cuda_core-1.0.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46690b0864417a5f2f9a7d10408e2570cbacae195c890a41286701eefb01ba79", size = 5061723, upload-time = "2026-05-12T20:11:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/b16b0af698a1bf4db337345daa7a44cd372fef107a3b692ffe1e0e6c5cc5/cuda_core-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1693fae113604cf9c114bfe3da15a15981f9d44ae78ecceb0b23e24c628ad19b", size = 4742003, upload-time = "2026-05-12T20:11:21.943Z" }, + { url = "https://files.pythonhosted.org/packages/41/4b/4ac1d0639241da756c634add606f93a7f3a39bef12f70e1fb4b40cc53c21/cuda_core-1.0.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3effd11283bc46fd06348c2fd18a0941ba7718a6f447343858c944c1a93a6dab", size = 4784340, upload-time = "2026-05-12T20:11:23.961Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/bb3e701f4af504e5e39e837135dc80022ec4c84858b2886ad577fe696a77/cuda_core-1.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1934517ff8a9dcd21b3f4a28e15e12643164b7d3ec187a4ee7560e22fd2dfc17", size = 5059041, upload-time = "2026-05-12T20:11:26.045Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e3/3ffaca2eabc71d0f9d29368fabc8ffb309353f05f418ea4c7eb5f223cf09/cuda_core-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:95c91d434a9baca066646cefa577227385104670a02fbe8e3defaadda84becf5", size = 4746198, upload-time = "2026-05-12T20:11:28.405Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a0/1daeae599cadd612689dbbf70d7da1c01883964fc2fbc7386f3c630a68cf/cuda_core-1.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6816dc020aee6103d8071bc02d8e4e1d91f2b49596f666896d608d92224d79d1", size = 4789856, upload-time = "2026-05-12T20:11:30.862Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4d/603557ab3cb171cc2a61d3678a39cb4dae3fd21275078bfbd1c0b0b5230b/cuda_core-1.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be7b65311bf78964b7905adbf3c0f8f717d432f2854dc45169277729bf60f1e2", size = 5106023, upload-time = "2026-05-12T20:11:33.509Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1a/ae079963c9df7f4274227eb63cf8f6083a532a6443adb340d951fd21c626/cuda_core-1.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a5c1aa3b738a7599ea289498d038fe625d259fd7ab795394541eee58a8e29bc", size = 4663076, upload-time = "2026-05-12T20:11:35.784Z" }, + { url = "https://files.pythonhosted.org/packages/57/f9/a6676b1fa555fad5748a945f4b530b51b898b4771a1e5d9f3520d3f415ea/cuda_core-1.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c427e5025096d96fcd5092fdc85d5d5e4ac3dea007914e90472ed52f27220446", size = 4749800, upload-time = "2026-05-12T20:11:38.012Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9d/4534a9564a812ee95b43db7324f9b25cbffda001bb348bb5b3f90dad50b9/cuda_core-1.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b392178202c652368883dbe3773cee14f3e1ed6b8bf45d1a1bcdd37c73604e06", size = 5078597, upload-time = "2026-05-12T20:11:40.836Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7c/2f68b0bdeb7dd36204f752468254d6b4487c6d82e9e442cfbe815a656eac/cuda_core-1.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:b99e3ca9bf3bd2c7d3028e5dc541b00e432e21373816889cdf2722b675bd9be8", size = 4647545, upload-time = "2026-05-12T20:11:43.494Z" }, + { url = "https://files.pythonhosted.org/packages/c2/45/55b07d643c87f1234b3cbc9d8383c0962b368ba1d6686a1919d6f6001af4/cuda_core-1.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9b0f115a68f2f84c0f6d9b7e863a29517f6dfe5f7b7d07d1d9da8904754e9a2", size = 4816184, upload-time = "2026-05-12T20:11:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/51/08/1aeffc9a529a7f94c9cee9bfd3a991743398b5f90aab30f06f2a4bc8205e/cuda_core-1.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af2db9e50e81d73e4f0b72ad279d0a9c789372393938fe75c17236b9ed974d7d", size = 5104496, upload-time = "2026-05-12T20:11:48.518Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/965330a44bcfa548793cf13244083528a597da2a18ff42d00fe8ef91ba03/cuda_core-1.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:29783ba03d36960b6612b15ff97123e88cfadfb7b1884f3581839f6bfba4b29f", size = 4765708, upload-time = "2026-05-12T20:11:51.107Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ab/db09228d5a8c124a93514726d2e18f31824f66d3a6769ee4e51721dd64cf/cuda_core-1.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4410bf1ef15c2ec23dccc302da76893c9354b530dee422e3277b116231bf5fe1", size = 4996094, upload-time = "2026-05-12T20:11:53.575Z" }, + { url = "https://files.pythonhosted.org/packages/29/e7/8ced56d6c6fa32b7385a8dccefd1424e3c1201bfee3385d9710609a43d16/cuda_core-1.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0f1324486bb90be6bde28bd0afbaf78a38827948d37f93f90318a01da8a3f8c", size = 5212461, upload-time = "2026-05-12T20:11:56.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/06/36236c44ed4025a6cbf5b6450364fc29e0f3d35ef4becb13b168fcd271f4/cuda_core-1.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:180bc808166483b0d6658a7c0d3f1312083b220f301de49476c1bc459cc46cf8", size = 5551965, upload-time = "2026-05-12T20:11:58.84Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" }, +] + +[[package]] +name = "cuda-python" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings" }, + { name = "cuda-core" }, + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/31/7ff3f7768eded7535c621abc2fecb9d181a34ea4cae3afe682feb796f242/cuda_python-13.3.1-py3-none-any.whl", hash = "sha256:280b014139ab447b6dd70a377db1596f310d6e887d9d342e6651b919ec145fb3", size = 8295, upload-time = "2026-05-29T23:28:47.012Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + +[[package]] +name = "datasets" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/a4/73f8e6ef52c535e1d20d5b2ca83bfe6de399d8b8b8a61ccc8d63d60735aa/datasets-4.1.1.tar.gz", hash = "sha256:7d8d5ba8b12861d2c44bfff9c83484ebfafff1ff553371e5901a8d3aab5450e2", size = 579324, upload-time = "2025-09-18T13:14:27.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/c8/09012ac195a0aab58755800d2efdc0e7d5905053509f12cb5d136c911cda/datasets-4.1.1-py3-none-any.whl", hash = "sha256:62e4f6899a36be9ec74a7e759a6951253cc85b3fcfa0a759b0efa8353b149dac", size = 503623, upload-time = "2025-09-18T13:14:25.111Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/36/b57c6e818d909f6e59c0182252921cf435e0951126a97e11de37e72ab5e1/debugpy-1.8.17-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542", size = 2098021, upload-time = "2025-09-17T16:33:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/be/01/0363c7efdd1e9febd090bb13cee4fb1057215b157b2979a4ca5ccb678217/debugpy-1.8.17-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3", size = 3087399, upload-time = "2025-09-17T16:33:24.292Z" }, + { url = "https://files.pythonhosted.org/packages/79/bc/4a984729674aa9a84856650438b9665f9a1d5a748804ac6f37932ce0d4aa/debugpy-1.8.17-cp310-cp310-win32.whl", hash = "sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4", size = 5230292, upload-time = "2025-09-17T16:33:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/2b9b3092d0cf81a5aa10c86271999453030af354d1a5a7d6e34c574515d7/debugpy-1.8.17-cp310-cp310-win_amd64.whl", hash = "sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a", size = 5261885, upload-time = "2025-09-17T16:33:27.592Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" }, + { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" }, + { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" }, + { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" }, + { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386, upload-time = "2025-09-17T16:33:54.594Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100, upload-time = "2025-09-17T16:33:56.353Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002, upload-time = "2025-09-17T16:33:58.231Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047, upload-time = "2025-09-17T16:34:00.586Z" }, + { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899, upload-time = "2025-09-17T16:34:02.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254, upload-time = "2025-09-17T16:34:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203, upload-time = "2025-09-17T16:34:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493, upload-time = "2025-09-17T16:34:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, +] + +[[package]] +name = "dirtyjson" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/04/d24f6e645ad82ba0ef092fa17d9ef7a21953781663648a01c9371d9e8e98/dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd", size = 30782, upload-time = "2022-11-28T23:32:33.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197, upload-time = "2022-11-28T23:32:31.219Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docx2txt" +version = "0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/07/4486a038624e885e227fe79111914c01f55aa70a51920ff1a7f2bd216d10/docx2txt-0.9.tar.gz", hash = "sha256:18013f6229b14909028b19aa7bf4f8f3d6e4632d7b089ab29f7f0a4d1f660e28", size = 3613, upload-time = "2025-03-24T20:59:25.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/51/756e71bec48ece0ecc2a10e921ef2756e197dcb7e478f2b43673b6683902/docx2txt-0.9-py3-none-any.whl", hash = "sha256:e3718c0653fd6f2fcf4b51b02a61452ad1c38a4c163bcf0a6fd9486cd38f529a", size = 4025, upload-time = "2025-03-24T20:59:24.394Z" }, +] + +[[package]] +name = "einops" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805, upload-time = "2025-02-09T03:17:00.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359, upload-time = "2025-02-09T03:17:01.998Z" }, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "evaluate" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "datasets" }, + { name = "dill" }, + { name = "fsspec", extra = ["http"] }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/d0/0c17a8e6e8dc7245f22dea860557c32bae50fc4d287ae030cb0e8ab8720f/evaluate-0.4.6.tar.gz", hash = "sha256:e07036ca12b3c24331f83ab787f21cc2dbf3631813a1631e63e40897c69a3f21", size = 65716, upload-time = "2025-09-18T13:06:30.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/af/3e990d8d4002bbc9342adb4facd59506e653da93b2417de0fa6027cb86b1/evaluate-0.4.6-py3-none-any.whl", hash = "sha256:bca85bc294f338377b7ac2f861e21c308b11b2a285f510d7d5394d5df437db29", size = 84069, upload-time = "2025-09-18T13:06:29.265Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + +[[package]] +name = "flashlib" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numba" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "nvidia-cutlass-dsl" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "triton" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/7e/28ff85b9cba087a9681eb619d3cf28422327adff7ef96e09f142ec019590/flashlib-0.1.0.tar.gz", hash = "sha256:5f8b6e4b4348de3777755520d6452361af608c740ee4eee346e35f8f48032a0e", size = 512269, upload-time = "2026-05-26T21:15:10.949Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/34/d1741d01a37d8b230c65888b9be21e166ddee26751f84878273c63735aaa/flashlib-0.1.0-py3-none-any.whl", hash = "sha256:2a6437a4120e5e8579c0fb63bc9308e1198e89e6246357f82bcf7d65edbdba99", size = 617493, upload-time = "2026-05-26T21:15:09.432Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/d9/4eabd956fe123651a1f0efe29d9758b3837b5ae9a98934bdb571117033bb/fonttools-4.60.0.tar.gz", hash = "sha256:8f5927f049091a0ca74d35cce7f78e8f7775c83a6901a8fbe899babcc297146a", size = 3553671, upload-time = "2025-09-17T11:34:01.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/1e/7c2d660cd2a6718961946f76b6af25ae8c7ad0e2a93a34c9bf8b955cb77f/fonttools-4.60.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:151282a235c36024168c21c02193e939e8b28c73d5fa0b36ae1072671d8fa134", size = 2809773, upload-time = "2025-09-17T11:31:52.648Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/35cb2e17d984e712f0f7241b1b8bf06bc1b0da345f11620acd78a7eb1f0e/fonttools-4.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3f32cc42d485d9b1546463b9a7a92bdbde8aef90bac3602503e04c2ddb27e164", size = 2345916, upload-time = "2025-09-17T11:31:55.817Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/39e50212f47bad254255734903accb4f44143faf2b950ba67a61f0bfb26a/fonttools-4.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:336b89d169c40379b8ccef418c877edbc28840b553099c9a739b0db2bcbb57c5", size = 4863583, upload-time = "2025-09-17T11:31:57.708Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/e701ba6a439119fe312f1ad738369519b446503b02d3f0f75424111686f1/fonttools-4.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39a38d950b2b04cd6da729586e6b51d686b0c27d554a2154a6a35887f87c09b1", size = 4793647, upload-time = "2025-09-17T11:31:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/d5/04/a48f5f7cce1653a876d6b57d9626c1364bcb430780bbbdd475662bbbf759/fonttools-4.60.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7067dd03e0296907a5c6184285807cbb7bc0bf61a584ffebbf97c2b638d8641a", size = 4842891, upload-time = "2025-09-17T11:32:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/dd/af/0f2b742f6b489a62c6f5a2239867c6d203e3ba358cb48dfc940baee41932/fonttools-4.60.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:342753fe1a1bd2e6896e7a4e936a67c0f441d6897bd11477f718e772d6e63e88", size = 4953569, upload-time = "2025-09-17T11:32:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2b/23c4dde4a869aa138f5fb63fb124e6accb0d643600b437f4eca0f2637ea2/fonttools-4.60.0-cp310-cp310-win32.whl", hash = "sha256:0746c2b2b32087da2ac5f81e14d319c44cb21127d419bc60869daed089790e3d", size = 2231022, upload-time = "2025-09-17T11:32:06.617Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1c/d53dd15d3392d8f69aa3bc49ca7bdfaea06aa875dc3a641eca85433c90b3/fonttools-4.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:b83b32e5e8918f8e0ccd79816fc2f914e30edc6969ab2df6baf4148e72dbcc11", size = 2275804, upload-time = "2025-09-17T11:32:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/da/3d/c57731fbbf204ef1045caca28d5176430161ead73cd9feac3e9d9ef77ee6/fonttools-4.60.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a9106c202d68ff5f9b4a0094c4d7ad2eaa7e9280f06427b09643215e706eb016", size = 2830883, upload-time = "2025-09-17T11:32:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/b7a6ebaed464ce441c755252cc222af11edc651d17c8f26482f429cc2c0e/fonttools-4.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9da3a4a3f2485b156bb429b4f8faa972480fc01f553f7c8c80d05d48f17eec89", size = 2356005, upload-time = "2025-09-17T11:32:13.248Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c2/ea834e921324e2051403e125c1fe0bfbdde4951a7c1784e4ae6bdbd286cc/fonttools-4.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f84de764c6057b2ffd4feb50ddef481d92e348f0c70f2c849b723118d352bf3", size = 5041201, upload-time = "2025-09-17T11:32:15.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3c/1c64a338e9aa410d2d0728827d5bb1301463078cb225b94589f27558b427/fonttools-4.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800b3fa0d5c12ddff02179d45b035a23989a6c597a71c8035c010fff3b2ef1bb", size = 4977696, upload-time = "2025-09-17T11:32:17.674Z" }, + { url = "https://files.pythonhosted.org/packages/07/cc/c8c411a0d9732bb886b870e052f20658fec9cf91118314f253950d2c1d65/fonttools-4.60.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd68f60b030277f292a582d31c374edfadc60bb33d51ec7b6cd4304531819ba", size = 5020386, upload-time = "2025-09-17T11:32:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/1d3bc07cf92e7f4fc27f06d4494bf6078dc595b2e01b959157a4fd23df12/fonttools-4.60.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:53328e3ca9e5c8660ef6de07c35f8f312c189b757535e12141be7a8ec942de6e", size = 5131575, upload-time = "2025-09-17T11:32:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/5a/16/08db3917ee19e89d2eb0ee637d37cd4136c849dc421ff63f406b9165c1a1/fonttools-4.60.0-cp311-cp311-win32.whl", hash = "sha256:d493c175ddd0b88a5376e61163e3e6fde3be8b8987db9b092e0a84650709c9e7", size = 2229297, upload-time = "2025-09-17T11:32:24.834Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0b/76764da82c0dfcea144861f568d9e83f4b921e84f2be617b451257bb25a7/fonttools-4.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc2770c9dc49c2d0366e9683f4d03beb46c98042d7ccc8ddbadf3459ecb051a7", size = 2277193, upload-time = "2025-09-17T11:32:27.094Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9b/706ebf84b55ab03439c1f3a94d6915123c0d96099f4238b254fdacffe03a/fonttools-4.60.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8c68928a438d60dfde90e2f09aa7f848ed201176ca6652341744ceec4215859f", size = 2831953, upload-time = "2025-09-17T11:32:29.39Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/782f485be450846e4f3aecff1f10e42af414fc6e19d235c70020f64278e1/fonttools-4.60.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7133821249097cffabf0624eafd37f5a3358d5ce814febe9db688e3673e724e", size = 2351716, upload-time = "2025-09-17T11:32:31.46Z" }, + { url = "https://files.pythonhosted.org/packages/39/77/ad8d2a6ecc19716eb488c8cf118de10f7802e14bdf61d136d7b52358d6b1/fonttools-4.60.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3638905d3d77ac8791127ce181f7cb434f37e4204d8b2e31b8f1e154320b41f", size = 4922729, upload-time = "2025-09-17T11:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/6b/48/aa543037c6e7788e1bc36b3f858ac70a59d32d0f45915263d0b330a35140/fonttools-4.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7968a26ef010ae89aabbb2f8e9dec1e2709a2541bb8620790451ee8aeb4f6fbf", size = 4967188, upload-time = "2025-09-17T11:32:35.74Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/e407d2028adc6387947eff8f2940b31f4ed40b9a83c2c7bbc8b9255126e2/fonttools-4.60.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ef01ca7847c356b0fe026b7b92304bc31dc60a4218689ee0acc66652c1a36b2", size = 4910043, upload-time = "2025-09-17T11:32:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/16/ef/e78519b3c296ef757a21b792fc6a785aa2ef9a2efb098083d8ed5f6ee2ba/fonttools-4.60.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f3482d7ed7867edfcf785f77c1dffc876c4b2ddac19539c075712ff2a0703cf5", size = 5061980, upload-time = "2025-09-17T11:32:40.457Z" }, + { url = "https://files.pythonhosted.org/packages/00/4c/ad72444d1e3ef704ee90af8d5abf198016a39908d322bf41235562fb01a0/fonttools-4.60.0-cp312-cp312-win32.whl", hash = "sha256:8c937c4fe8addff575a984c9519433391180bf52cf35895524a07b520f376067", size = 2217750, upload-time = "2025-09-17T11:32:42.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/55/3e8ac21963e130242f5a9ea2ebc57f5726d704bf4dcca89088b5b637b2d3/fonttools-4.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:99b06d5d6f29f32e312adaed0367112f5ff2d300ea24363d377ec917daf9e8c5", size = 2266025, upload-time = "2025-09-17T11:32:44.8Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6b/d090cd54abe88192fe3010f573508b2592cf1d1f98b14bcb799a8ad20525/fonttools-4.60.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:97100ba820936cdb5148b634e0884f0088699c7e2f1302ae7bba3747c7a19fb3", size = 2824791, upload-time = "2025-09-17T11:32:47.002Z" }, + { url = "https://files.pythonhosted.org/packages/97/8c/7ccb5a27aac9a535623fe04935fb9f469a4f8a1253991af9fbac2fe88c17/fonttools-4.60.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:03fccf84f377f83e99a5328a9ebe6b41e16fcf64a1450c352b6aa7e0deedbc01", size = 2347081, upload-time = "2025-09-17T11:32:49.204Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1a/c14f0bb20b4cb7849dc0519f0ab0da74318d52236dc23168530569958599/fonttools-4.60.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a3ef06671f862cd7da78ab105fbf8dce9da3634a8f91b3a64ed5c29c0ac6a9a8", size = 4902095, upload-time = "2025-09-17T11:32:51.848Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a0/c7c91f07c40de5399cbaec7d25e04c9afac6c8f80036a98c125efdb5fe1a/fonttools-4.60.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f2195faf96594c238462c420c7eff97d1aa51de595434f806ec3952df428616", size = 4959137, upload-time = "2025-09-17T11:32:54.185Z" }, + { url = "https://files.pythonhosted.org/packages/38/d2/169e49498df9f2c721763aa39b0bf3d08cb762864ebc8a8ddb99f5ba7ec8/fonttools-4.60.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3887008865fa4f56cff58a1878f1300ba81a4e34f76daf9b47234698493072ee", size = 4900467, upload-time = "2025-09-17T11:32:56.664Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/bfb56b89c3eab8bcb739c7fd1e8a43285c8dd833e1e1d18d4f54f2f641af/fonttools-4.60.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5567bd130378f21231d3856d8f0571dcdfcd77e47832978c26dabe572d456daa", size = 5043508, upload-time = "2025-09-17T11:32:58.944Z" }, + { url = "https://files.pythonhosted.org/packages/77/30/2b511c7eb99faee1fd9a0b42e984fb91275da3d681da650af4edf409d0fd/fonttools-4.60.0-cp313-cp313-win32.whl", hash = "sha256:699d0b521ec0b188ac11f2c14ccf6a926367795818ddf2bd00a273e9a052dd20", size = 2216037, upload-time = "2025-09-17T11:33:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/3d/73/a2cc5ee4faeb0302cc81942c27f3b516801bf489fdc422a1b20090fff695/fonttools-4.60.0-cp313-cp313-win_amd64.whl", hash = "sha256:24296163268e7c800009711ce5c0e9997be8882c0bd546696c82ef45966163a6", size = 2265190, upload-time = "2025-09-17T11:33:03.935Z" }, + { url = "https://files.pythonhosted.org/packages/86/dd/a126706e45e0ce097cef6de4108b5597795acaa945fdbdd922dbc090d335/fonttools-4.60.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b6fe3efdc956bdad95145cea906ad9ff345c17b706356dfc1098ce3230591343", size = 2821835, upload-time = "2025-09-17T11:33:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/90/5c17f311bbd983fd614b82a7a06da967b5d3c87e3e61cf34de6029a92ff4/fonttools-4.60.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:764b2aaab839762a3aa3207e5b3f0e0dfa41799e0b091edec5fcbccc584fdab5", size = 2344536, upload-time = "2025-09-17T11:33:08.574Z" }, + { url = "https://files.pythonhosted.org/packages/60/67/48c1a6229b2a5668c4111fbd1694ca417adedc1254c5cd2f9a11834c429d/fonttools-4.60.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b81c7c47d9e78106a4d70f1dbeb49150513171715e45e0d2661809f2b0e3f710", size = 4842494, upload-time = "2025-09-17T11:33:11.338Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/83b0b37d02b7e321cbe2b8fcec0aa18571f0a47d3dc222196404371d83b6/fonttools-4.60.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799ff60ee66b300ebe1fe6632b1cc55a66400fe815cef7b034d076bce6b1d8fc", size = 4943203, upload-time = "2025-09-17T11:33:13.285Z" }, + { url = "https://files.pythonhosted.org/packages/c9/07/11163e49497c53392eaca210a474104e4987c17ca7731f8754ba0d416a67/fonttools-4.60.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f9878abe155ddd1b433bab95d027a686898a6afba961f3c5ca14b27488f2d772", size = 4889233, upload-time = "2025-09-17T11:33:15.175Z" }, + { url = "https://files.pythonhosted.org/packages/60/90/e85005d955cb26e7de015d5678778b8cc3293c0f3d717865675bd641fbfc/fonttools-4.60.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ded432b7133ea4602fdb4731a4a7443a8e9548edad28987b99590cf6da626254", size = 4998335, upload-time = "2025-09-17T11:33:17.217Z" }, + { url = "https://files.pythonhosted.org/packages/2a/82/0374ad53729de6e3788ecdb8a3731ce6592c5ffa9bff823cef2ffe0164af/fonttools-4.60.0-cp314-cp314-win32.whl", hash = "sha256:5d97cf3a9245316d5978628c05642b939809c4f55ca632ca40744cb9de6e8d4a", size = 2219840, upload-time = "2025-09-17T11:33:19.494Z" }, + { url = "https://files.pythonhosted.org/packages/11/c3/804cd47453dcafb7976f9825b43cc0e61a2fe30eddb971b681cd72c4ca65/fonttools-4.60.0-cp314-cp314-win_amd64.whl", hash = "sha256:61b9ef46dd5e9dcb6f437eb0cc5ed83d5049e1bf9348e31974ffee1235db0f8f", size = 2269891, upload-time = "2025-09-17T11:33:21.743Z" }, + { url = "https://files.pythonhosted.org/packages/75/bf/1bd760aca04098e7028b4e0e5f73b41ff74b322275698071454652476a44/fonttools-4.60.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bba7e3470cf353e1484a36dfb4108f431c2859e3f6097fe10118eeae92166773", size = 2893361, upload-time = "2025-09-17T11:33:23.68Z" }, + { url = "https://files.pythonhosted.org/packages/25/35/7a2c09aa990ed77f34924def383f44fc576a5596cc3df8438071e1baa1ac/fonttools-4.60.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5ac6439a38c27b3287063176b3303b34982024b01e2e95bba8ac1e45f6d41c1", size = 2374086, upload-time = "2025-09-17T11:33:25.988Z" }, + { url = "https://files.pythonhosted.org/packages/77/a9/f85ed2493e82837ff73421f3f7a1c3ae8f0b14051307418c916d9563da1f/fonttools-4.60.0-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4acd21e9f125a1257da59edf7a6e9bd4abd76282770715c613f1fe482409e9f9", size = 4848766, upload-time = "2025-09-17T11:33:28.018Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/29830eda31ae9231a06d5246e5d0c686422d03456ed666e13576c24c3f97/fonttools-4.60.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4a6fc53039ea047e35dc62b958af9cd397eedbc3fa42406d2910ae091b9ae37", size = 5084613, upload-time = "2025-09-17T11:33:30.562Z" }, + { url = "https://files.pythonhosted.org/packages/48/01/615905e7db2568fe1843145077e680443494b7caab2089527b7e112c7606/fonttools-4.60.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ef34f44eadf133e94e82c775a33ee3091dd37ee0161c5f5ea224b46e3ce0fb8e", size = 4956620, upload-time = "2025-09-17T11:33:32.497Z" }, + { url = "https://files.pythonhosted.org/packages/97/8e/64e65255871ec2f13b6c00b5b12d08b928b504867cfb7e7ed73e5e941832/fonttools-4.60.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d112cae3e7ad1bb5d7f7a60365fcf6c181374648e064a8c07617b240e7c828ee", size = 4973202, upload-time = "2025-09-17T11:33:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/04d16243eb441e8de61074c7809e92d2e35df4cd11af5632e486bc630dab/fonttools-4.60.0-cp314-cp314t-win32.whl", hash = "sha256:0f7b2c251dc338973e892a1e153016114e7a75f6aac7a49b84d5d1a4c0608d08", size = 2281217, upload-time = "2025-09-17T11:33:36.965Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/09bd2f9f28ef0d6f3620fa19699d11c4bc83ff8a2786d8ccdd97c209b19a/fonttools-4.60.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a72771106bc7434098db35abecd84d608857f6e116d3ef00366b213c502ce9", size = 2344738, upload-time = "2025-09-17T11:33:39.372Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/247d3e54eb5ed59e94e09866cfc4f9567e274fbf310ba390711851f63b3b/fonttools-4.60.0-py3-none-any.whl", hash = "sha256:496d26e4d14dcccdd6ada2e937e4d174d3138e3d73f5c9b6ec6eb2fd1dab4f66", size = 1142186, upload-time = "2025-09-17T11:33:59.287Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", size = 81304, upload-time = "2025-06-09T22:59:46.226Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", size = 47735, upload-time = "2025-06-09T22:59:48.133Z" }, + { url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", size = 46775, upload-time = "2025-06-09T22:59:49.564Z" }, + { url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", size = 224644, upload-time = "2025-06-09T22:59:51.35Z" }, + { url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", size = 222125, upload-time = "2025-06-09T22:59:52.884Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", size = 233455, upload-time = "2025-06-09T22:59:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", size = 227339, upload-time = "2025-06-09T22:59:56.187Z" }, + { url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", size = 212969, upload-time = "2025-06-09T22:59:57.604Z" }, + { url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", size = 222862, upload-time = "2025-06-09T22:59:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", size = 222492, upload-time = "2025-06-09T23:00:01.026Z" }, + { url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", size = 238250, upload-time = "2025-06-09T23:00:03.401Z" }, + { url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", size = 218720, upload-time = "2025-06-09T23:00:05.282Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", size = 232585, upload-time = "2025-06-09T23:00:07.962Z" }, + { url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", size = 234248, upload-time = "2025-06-09T23:00:09.428Z" }, + { url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", size = 221621, upload-time = "2025-06-09T23:00:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", size = 39578, upload-time = "2025-06-09T23:00:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", size = 43830, upload-time = "2025-06-09T23:00:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" }, + { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" }, + { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333, upload-time = "2025-06-09T23:00:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724, upload-time = "2025-06-09T23:00:21.705Z" }, + { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842, upload-time = "2025-06-09T23:00:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767, upload-time = "2025-06-09T23:00:25.103Z" }, + { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130, upload-time = "2025-06-09T23:00:27.061Z" }, + { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301, upload-time = "2025-06-09T23:00:29.02Z" }, + { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606, upload-time = "2025-06-09T23:00:30.514Z" }, + { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372, upload-time = "2025-06-09T23:00:31.966Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860, upload-time = "2025-06-09T23:00:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893, upload-time = "2025-06-09T23:00:35.002Z" }, + { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323, upload-time = "2025-06-09T23:00:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149, upload-time = "2025-06-09T23:00:37.963Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565, upload-time = "2025-06-09T23:00:39.753Z" }, + { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019, upload-time = "2025-06-09T23:00:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/e0/bab50af11c2d75c9c4a2a26a5254573c0bd97cea152254401510950486fa/fsspec-2025.9.0.tar.gz", hash = "sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19", size = 304847, upload-time = "2025-09-02T19:10:49.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7", size = 199289, upload-time = "2025-09-02T19:10:47.708Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "gitignore-parser" +version = "0.1.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/51/e391a1a4238f18d0abb47be479b07af265ad4519022cf51b7da47ef82487/gitignore_parser-0.1.13.tar.gz", hash = "sha256:c7e10c8190accb8ae57fb3711889e73a9c0dbc04d4222b91ace8a4bf64d2f746", size = 5603, upload-time = "2025-08-25T06:33:22.704Z" } + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, + { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, + { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "griffe" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/31/feeddfce1748c4a233ec1aa5b7396161c07ae1aa9b7bdbc9a72c3c7dd768/hf_xet-1.1.10.tar.gz", hash = "sha256:408aef343800a2102374a883f283ff29068055c111f003ff840733d3b715bb97", size = 487910, upload-time = "2025-09-12T20:10:27.12Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/a2/343e6d05de96908366bdc0081f2d8607d61200be2ac802769c4284cc65bd/hf_xet-1.1.10-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:686083aca1a6669bc85c21c0563551cbcdaa5cf7876a91f3d074a030b577231d", size = 2761466, upload-time = "2025-09-12T20:10:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/31/f9/6215f948ac8f17566ee27af6430ea72045e0418ce757260248b483f4183b/hf_xet-1.1.10-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:71081925383b66b24eedff3013f8e6bbd41215c3338be4b94ba75fd75b21513b", size = 2623807, upload-time = "2025-09-12T20:10:21.118Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/86397573efefff941e100367bbda0b21496ffcdb34db7ab51912994c32a2/hf_xet-1.1.10-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6bceb6361c80c1cc42b5a7b4e3efd90e64630bcf11224dcac50ef30a47e435", size = 3186960, upload-time = "2025-09-12T20:10:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/01/a7/0b2e242b918cc30e1f91980f3c4b026ff2eedaf1e2ad96933bca164b2869/hf_xet-1.1.10-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eae7c1fc8a664e54753ffc235e11427ca61f4b0477d757cc4eb9ae374b69f09c", size = 3087167, upload-time = "2025-09-12T20:10:17.255Z" }, + { url = "https://files.pythonhosted.org/packages/4a/25/3e32ab61cc7145b11eee9d745988e2f0f4fafda81b25980eebf97d8cff15/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0a0005fd08f002180f7a12d4e13b22be277725bc23ed0529f8add5c7a6309c06", size = 3248612, upload-time = "2025-09-12T20:10:24.093Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3d/ab7109e607ed321afaa690f557a9ada6d6d164ec852fd6bf9979665dc3d6/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f", size = 3353360, upload-time = "2025-09-12T20:10:25.563Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0e/471f0a21db36e71a2f1752767ad77e92d8cde24e974e03d662931b1305ec/hf_xet-1.1.10-cp37-abi3-win_amd64.whl", hash = "sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045", size = 2804691, upload-time = "2025-09-12T20:10:28.433Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.35.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/42/0e7be334a6851cd7d51cc11717cb95e89333ebf0064431c0255c56957526/huggingface_hub-0.35.1.tar.gz", hash = "sha256:3585b88c5169c64b7e4214d0e88163d4a709de6d1a502e0cd0459e9ee2c9c572", size = 461374, upload-time = "2025-09-23T13:43:47.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/60/4acf0c8a3925d9ff491dc08fe84d37e09cfca9c3b885e0db3d4dedb98cea/huggingface_hub-0.35.1-py3-none-any.whl", hash = "sha256:2f0e2709c711e3040e31d3e0418341f7092910f1462dd00350c4e97af47280a8", size = 563340, upload-time = "2025-09-23T13:43:45.343Z" }, +] + +[package.optional-dependencies] +inference = [ + { name = "aiohttp" }, +] + +[[package]] +name = "identify" +version = "2.6.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "ipykernel" +version = "6.29.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367, upload-time = "2024-07-01T14:07:22.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173, upload-time = "2024-07-01T14:07:19.603Z" }, +] + +[[package]] +name = "ipython" +version = "8.37.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" }, +] + +[[package]] +name = "ipython" +version = "9.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/71/a86262bf5a68bf211bcc71fe302af7e05f18a2852fdc610a854d20d085e6/ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113", size = 4389137, upload-time = "2025-08-29T12:15:21.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl", hash = "sha256:88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72", size = 612426, upload-time = "2025-08-29T12:15:18.866Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/c0/a3bb4cc13aced219dd18191ea66e874266bd8aa7b96744e495e1c733aa2d/jiter-0.11.0.tar.gz", hash = "sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4", size = 167094, upload-time = "2025-09-15T09:20:38.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/21/7dd1235a19e26979be6098e87e4cced2e061752f3a40a17bbce6dea7fae1/jiter-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3893ce831e1c0094a83eeaf56c635a167d6fa8cc14393cc14298fd6fdc2a2449", size = 309875, upload-time = "2025-09-15T09:18:48.41Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/462b54708aa85b135733ccba70529dd68a18511bf367a87c5fd28676c841/jiter-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:25c625b9b61b5a8725267fdf867ef2e51b429687f6a4eef211f4612e95607179", size = 316505, upload-time = "2025-09-15T09:18:51.057Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/14e2eeaac6a47bff27d213834795472355fd39769272eb53cb7aa83d5aa8/jiter-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4ca85fb6a62cf72e1c7f5e34ddef1b660ce4ed0886ec94a1ef9777d35eaa1f", size = 337613, upload-time = "2025-09-15T09:18:52.358Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ed/a5f1f8419c92b150a7c7fb5ccba1fb1e192887ad713d780e70874f0ce996/jiter-0.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:572208127034725e79c28437b82414028c3562335f2b4f451d98136d0fc5f9cd", size = 361438, upload-time = "2025-09-15T09:18:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f5/70682c023dfcdd463a53faf5d30205a7d99c51d70d3e303c932d0936e5a2/jiter-0.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494ba627c7f550ad3dabb21862864b8f2216098dc18ff62f37b37796f2f7c325", size = 486180, upload-time = "2025-09-15T09:18:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/7c/39/020d08cbab4eab48142ad88b837c41eb08a15c0767fdb7c0d3265128a44b/jiter-0.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8da18a99f58bca3ecc2d2bba99cac000a924e115b6c4f0a2b98f752b6fbf39a", size = 376681, upload-time = "2025-09-15T09:18:57.553Z" }, + { url = "https://files.pythonhosted.org/packages/52/10/b86733f6e594cf51dd142f37c602d8df87c554c5844958deaab0de30eb5d/jiter-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ffd3b0fff3fabbb02cc09910c08144db6bb5697a98d227a074401e01ee63dd", size = 348685, upload-time = "2025-09-15T09:18:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ee/8861665e83a9e703aa5f65fddddb6225428e163e6b0baa95a7f9a8fb9aae/jiter-0.11.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fe6530aa738a4f7d4e4702aa8f9581425d04036a5f9e25af65ebe1f708f23be", size = 385573, upload-time = "2025-09-15T09:19:00.593Z" }, + { url = "https://files.pythonhosted.org/packages/25/74/05afec03600951f128293813b5a208c9ba1bf587c57a344c05a42a69e1b1/jiter-0.11.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e35d66681c133a03d7e974e7eedae89720fe8ca3bd09f01a4909b86a8adf31f5", size = 516669, upload-time = "2025-09-15T09:19:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/93/d1/2e5bfe147cfbc2a5eef7f73eb75dc5c6669da4fa10fc7937181d93af9495/jiter-0.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c59459beca2fbc9718b6f1acb7bfb59ebc3eb4294fa4d40e9cb679dafdcc6c60", size = 508767, upload-time = "2025-09-15T09:19:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/87/50/597f71307e10426b5c082fd05d38c615ddbdd08c3348d8502963307f0652/jiter-0.11.0-cp310-cp310-win32.whl", hash = "sha256:b7b0178417b0dcfc5f259edbc6db2b1f5896093ed9035ee7bab0f2be8854726d", size = 205476, upload-time = "2025-09-15T09:19:05.594Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/1e5214b3272e311754da26e63edec93a183811d4fc2e0118addec365df8b/jiter-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:11df2bf99fb4754abddd7f5d940a48e51f9d11624d6313ca4314145fcad347f0", size = 204708, upload-time = "2025-09-15T09:19:06.955Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/a69fefeef09c2eaabae44b935a1aa81517e49639c0a0c25d861cb18cd7ac/jiter-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cb5d9db02979c3f49071fce51a48f4b4e4cf574175fb2b11c7a535fa4867b222", size = 309503, upload-time = "2025-09-15T09:19:08.191Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d5/a6aba9e6551f32f9c127184f398208e4eddb96c59ac065c8a92056089d28/jiter-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1dc6a123f3471c4730db7ca8ba75f1bb3dcb6faeb8d46dd781083e7dee88b32d", size = 317688, upload-time = "2025-09-15T09:19:09.918Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/5e86f57c1883971cdc8535d0429c2787bf734840a231da30a3be12850562/jiter-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09858f8d230f031c7b8e557429102bf050eea29c77ad9c34c8fe253c5329acb7", size = 337418, upload-time = "2025-09-15T09:19:11.078Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/a71d8a24c2a70664970574a8e0b766663f5ef788f7fe1cc20ee0c016d488/jiter-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbe2196c4a0ce760925a74ab4456bf644748ab0979762139626ad138f6dac72d", size = 361423, upload-time = "2025-09-15T09:19:13.286Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e5/b09076f4e7fd9471b91e16f9f3dc7330b161b738f3b39b2c37054a36e26a/jiter-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5beb56d22b63647bafd0b74979216fdee80c580c0c63410be8c11053860ffd09", size = 486367, upload-time = "2025-09-15T09:19:14.546Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/98cb3a36f5e62f80cd860f0179f948d9eab5a316d55d3e1bab98d9767af5/jiter-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97025d09ef549795d8dc720a824312cee3253c890ac73c621721ddfc75066789", size = 376335, upload-time = "2025-09-15T09:19:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d8/ec74886497ea393c29dbd7651ddecc1899e86404a6b1f84a3ddab0ab59fd/jiter-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d50880a6da65d8c23a2cf53c412847d9757e74cc9a3b95c5704a1d1a24667347", size = 348981, upload-time = "2025-09-15T09:19:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/24/93/d22ad7fa3b86ade66c86153ceea73094fc2af8b20c59cb7fceab9fea4704/jiter-0.11.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:452d80a1c86c095a242007bd9fc5d21b8a8442307193378f891cb8727e469648", size = 385797, upload-time = "2025-09-15T09:19:19.121Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bd/e25ff4a4df226e9b885f7cb01ee4b9dc74e3000e612d6f723860d71a1f34/jiter-0.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e84e58198d4894668eec2da660ffff60e0f3e60afa790ecc50cb12b0e02ca1d4", size = 516597, upload-time = "2025-09-15T09:19:20.301Z" }, + { url = "https://files.pythonhosted.org/packages/be/fb/beda613db7d93ffa2fdd2683f90f2f5dce8daf4bc2d0d2829e7de35308c6/jiter-0.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df64edcfc5dd5279a791eea52aa113d432c933119a025b0b5739f90d2e4e75f1", size = 508853, upload-time = "2025-09-15T09:19:22.075Z" }, + { url = "https://files.pythonhosted.org/packages/20/64/c5b0d93490634e41e38e2a15de5d54fdbd2c9f64a19abb0f95305b63373c/jiter-0.11.0-cp311-cp311-win32.whl", hash = "sha256:144fc21337d21b1d048f7f44bf70881e1586401d405ed3a98c95a114a9994982", size = 205140, upload-time = "2025-09-15T09:19:23.351Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e6/c347c0e6f5796e97d4356b7e5ff0ce336498b7f4ef848fae621a56f1ccf3/jiter-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:b0f32e644d241293b892b1a6dd8f0b9cc029bfd94c97376b2681c36548aabab7", size = 204311, upload-time = "2025-09-15T09:19:24.591Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/3009b112b8f673e568ef79af9863d8309a15f0a8cdcc06ed6092051f377e/jiter-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb7b377688cc3850bbe5c192a6bd493562a0bc50cbc8b047316428fbae00ada", size = 305510, upload-time = "2025-09-15T09:19:25.893Z" }, + { url = "https://files.pythonhosted.org/packages/fe/82/15514244e03b9e71e086bbe2a6de3e4616b48f07d5f834200c873956fb8c/jiter-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b7cbe3f25bd0d8abb468ba4302a5d45617ee61b2a7a638f63fee1dc086be99", size = 316521, upload-time = "2025-09-15T09:19:27.525Z" }, + { url = "https://files.pythonhosted.org/packages/92/94/7a2e905f40ad2d6d660e00b68d818f9e29fb87ffe82774f06191e93cbe4a/jiter-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a7f0ec81d5b7588c5cade1eb1925b91436ae6726dc2df2348524aeabad5de6", size = 338214, upload-time = "2025-09-15T09:19:28.727Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/5791ed5bdc76f12110158d3316a7a3ec0b1413d018b41c5ed399549d3ad5/jiter-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07630bb46ea2a6b9c6ed986c6e17e35b26148cce2c535454b26ee3f0e8dcaba1", size = 361280, upload-time = "2025-09-15T09:19:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7f/b7d82d77ff0d2cb06424141000176b53a9e6b16a1125525bb51ea4990c2e/jiter-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7764f27d28cd4a9cbc61704dfcd80c903ce3aad106a37902d3270cd6673d17f4", size = 487895, upload-time = "2025-09-15T09:19:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/44/10a1475d46f1fc1fd5cc2e82c58e7bca0ce5852208e0fa5df2f949353321/jiter-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4a6c4a737d486f77f842aeb22807edecb4a9417e6700c7b981e16d34ba7c72", size = 378421, upload-time = "2025-09-15T09:19:32.746Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5f/0dc34563d8164d31d07bc09d141d3da08157a68dcd1f9b886fa4e917805b/jiter-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf408d2a0abd919b60de8c2e7bc5eeab72d4dafd18784152acc7c9adc3291591", size = 347932, upload-time = "2025-09-15T09:19:34.612Z" }, + { url = "https://files.pythonhosted.org/packages/f7/de/b68f32a4fcb7b4a682b37c73a0e5dae32180140cd1caf11aef6ad40ddbf2/jiter-0.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdef53eda7d18e799625023e1e250dbc18fbc275153039b873ec74d7e8883e09", size = 386959, upload-time = "2025-09-15T09:19:35.994Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/c08c92e713b6e28972a846a81ce374883dac2f78ec6f39a0dad9f2339c3a/jiter-0.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:53933a38ef7b551dd9c7f1064f9d7bb235bb3168d0fa5f14f0798d1b7ea0d9c5", size = 517187, upload-time = "2025-09-15T09:19:37.426Z" }, + { url = "https://files.pythonhosted.org/packages/89/b5/4a283bec43b15aad54fcae18d951f06a2ec3f78db5708d3b59a48e9c3fbd/jiter-0.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11840d2324c9ab5162fc1abba23bc922124fedcff0d7b7f85fffa291e2f69206", size = 509461, upload-time = "2025-09-15T09:19:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/f8bad793010534ea73c985caaeef8cc22dfb1fedb15220ecdf15c623c07a/jiter-0.11.0-cp312-cp312-win32.whl", hash = "sha256:4f01a744d24a5f2bb4a11657a1b27b61dc038ae2e674621a74020406e08f749b", size = 206664, upload-time = "2025-09-15T09:19:40.096Z" }, + { url = "https://files.pythonhosted.org/packages/ed/42/5823ec2b1469395a160b4bf5f14326b4a098f3b6898fbd327366789fa5d3/jiter-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:29fff31190ab3a26de026da2f187814f4b9c6695361e20a9ac2123e4d4378a4c", size = 203520, upload-time = "2025-09-15T09:19:41.798Z" }, + { url = "https://files.pythonhosted.org/packages/97/c4/d530e514d0f4f29b2b68145e7b389cbc7cac7f9c8c23df43b04d3d10fa3e/jiter-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4441a91b80a80249f9a6452c14b2c24708f139f64de959943dfeaa6cb915e8eb", size = 305021, upload-time = "2025-09-15T09:19:43.523Z" }, + { url = "https://files.pythonhosted.org/packages/7a/77/796a19c567c5734cbfc736a6f987affc0d5f240af8e12063c0fb93990ffa/jiter-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff85fc6d2a431251ad82dbd1ea953affb5a60376b62e7d6809c5cd058bb39471", size = 314384, upload-time = "2025-09-15T09:19:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/14/9c/824334de0b037b91b6f3fa9fe5a191c83977c7ec4abe17795d3cb6d174cf/jiter-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5e86126d64706fd28dfc46f910d496923c6f95b395138c02d0e252947f452bd", size = 337389, upload-time = "2025-09-15T09:19:46.094Z" }, + { url = "https://files.pythonhosted.org/packages/a2/95/ed4feab69e6cf9b2176ea29d4ef9d01a01db210a3a2c8a31a44ecdc68c38/jiter-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad8bd82165961867a10f52010590ce0b7a8c53da5ddd8bbb62fef68c181b921", size = 360519, upload-time = "2025-09-15T09:19:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0c/2ad00f38d3e583caba3909d95b7da1c3a7cd82c0aa81ff4317a8016fb581/jiter-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b42c2cd74273455ce439fd9528db0c6e84b5623cb74572305bdd9f2f2961d3df", size = 487198, upload-time = "2025-09-15T09:19:49.116Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/919b64cf3499b79bdfba6036da7b0cac5d62d5c75a28fb45bad7819e22f0/jiter-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0062dab98172dd0599fcdbf90214d0dcde070b1ff38a00cc1b90e111f071982", size = 377835, upload-time = "2025-09-15T09:19:50.468Z" }, + { url = "https://files.pythonhosted.org/packages/29/7f/8ebe15b6e0a8026b0d286c083b553779b4dd63db35b43a3f171b544de91d/jiter-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb948402821bc76d1f6ef0f9e19b816f9b09f8577844ba7140f0b6afe994bc64", size = 347655, upload-time = "2025-09-15T09:19:51.726Z" }, + { url = "https://files.pythonhosted.org/packages/8e/64/332127cef7e94ac75719dda07b9a472af6158ba819088d87f17f3226a769/jiter-0.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25a5b1110cca7329fd0daf5060faa1234be5c11e988948e4f1a1923b6a457fe1", size = 386135, upload-time = "2025-09-15T09:19:53.075Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/557b63527442f84c14774159948262a9d4fabb0d61166f11568f22fc60d2/jiter-0.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bf11807e802a214daf6c485037778843fadd3e2ec29377ae17e0706ec1a25758", size = 516063, upload-time = "2025-09-15T09:19:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/86/13/4164c819df4a43cdc8047f9a42880f0ceef5afeb22e8b9675c0528ebdccd/jiter-0.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166", size = 508139, upload-time = "2025-09-15T09:19:55.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/70/6e06929b401b331d41ddb4afb9f91cd1168218e3371972f0afa51c9f3c31/jiter-0.11.0-cp313-cp313-win32.whl", hash = "sha256:8e36924dad32c48d3c5e188d169e71dc6e84d6cb8dedefea089de5739d1d2f80", size = 206369, upload-time = "2025-09-15T09:19:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0d/8185b8e15de6dce24f6afae63380e16377dd75686d56007baa4f29723ea1/jiter-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:452d13e4fd59698408087235259cebe67d9d49173b4dacb3e8d35ce4acf385d6", size = 202538, upload-time = "2025-09-15T09:19:58.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/3a/d61707803260d59520721fa326babfae25e9573a88d8b7b9cb54c5423a59/jiter-0.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:089f9df9f69532d1339e83142438668f52c97cd22ee2d1195551c2b1a9e6cf33", size = 313737, upload-time = "2025-09-15T09:19:59.638Z" }, + { url = "https://files.pythonhosted.org/packages/cd/cc/c9f0eec5d00f2a1da89f6bdfac12b8afdf8d5ad974184863c75060026457/jiter-0.11.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ed1fe69a8c69bf0f2a962d8d706c7b89b50f1332cd6b9fbda014f60bd03a03", size = 346183, upload-time = "2025-09-15T09:20:01.442Z" }, + { url = "https://files.pythonhosted.org/packages/a6/87/fc632776344e7aabbab05a95a0075476f418c5d29ab0f2eec672b7a1f0ac/jiter-0.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba", size = 204225, upload-time = "2025-09-15T09:20:03.102Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/e7f45be7d3969bdf2e3cd4b816a7a1d272507cd0edd2d6dc4b07514f2d9a/jiter-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9a6dff27eca70930bdbe4cbb7c1a4ba8526e13b63dc808c0670083d2d51a4a72", size = 304414, upload-time = "2025-09-15T09:20:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/06/32/13e8e0d152631fcc1907ceb4943711471be70496d14888ec6e92034e2caf/jiter-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ae2a7593a62132c7d4c2abbee80bbbb94fdc6d157e2c6cc966250c564ef774", size = 314223, upload-time = "2025-09-15T09:20:05.631Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/abedd5b5a20ca083f778d96bba0d2366567fcecb0e6e34ff42640d5d7a18/jiter-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b13a431dba4b059e9e43019d3022346d009baf5066c24dcdea321a303cde9f0", size = 337306, upload-time = "2025-09-15T09:20:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/30d59bdc1204c86aa975ec72c48c482fee6633120ee9c3ab755e4dfefea8/jiter-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af62e84ca3889604ebb645df3b0a3f3bcf6b92babbff642bd214616f57abb93a", size = 360565, upload-time = "2025-09-15T09:20:08.283Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/567288e0d2ed9fa8f7a3b425fdaf2cb82b998633c24fe0d98f5417321aa8/jiter-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f3b32bb723246e6b351aecace52aba78adb8eeb4b2391630322dc30ff6c773", size = 486465, upload-time = "2025-09-15T09:20:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/18/6e/7b72d09273214cadd15970e91dd5ed9634bee605176107db21e1e4205eb1/jiter-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:adcab442f4a099a358a7f562eaa54ed6456fb866e922c6545a717be51dbed7d7", size = 377581, upload-time = "2025-09-15T09:20:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/58/52/4db456319f9d14deed325f70102577492e9d7e87cf7097bda9769a1fcacb/jiter-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9967c2ab338ee2b2c0102fd379ec2693c496abf71ffd47e4d791d1f593b68e2", size = 347102, upload-time = "2025-09-15T09:20:12.175Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b4/433d5703c38b26083aec7a733eb5be96f9c6085d0e270a87ca6482cbf049/jiter-0.11.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e7d0bed3b187af8b47a981d9742ddfc1d9b252a7235471ad6078e7e4e5fe75c2", size = 386477, upload-time = "2025-09-15T09:20:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7a/a60bfd9c55b55b07c5c441c5085f06420b6d493ce9db28d069cc5b45d9f3/jiter-0.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:f6fe0283e903ebc55f1a6cc569b8c1f3bf4abd026fed85e3ff8598a9e6f982f0", size = 516004, upload-time = "2025-09-15T09:20:14.848Z" }, + { url = "https://files.pythonhosted.org/packages/2e/46/f8363e5ecc179b4ed0ca6cb0a6d3bfc266078578c71ff30642ea2ce2f203/jiter-0.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5821e3d66606b29ae5b497230b304f1376f38137d69e35f8d2bd5f310ff73", size = 507855, upload-time = "2025-09-15T09:20:16.176Z" }, + { url = "https://files.pythonhosted.org/packages/90/33/396083357d51d7ff0f9805852c288af47480d30dd31d8abc74909b020761/jiter-0.11.0-cp314-cp314-win32.whl", hash = "sha256:c2d13ba7567ca8799f17c76ed56b1d49be30df996eb7fa33e46b62800562a5e2", size = 205802, upload-time = "2025-09-15T09:20:17.661Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/eb06ca556b2551d41de7d03bf2ee24285fa3d0c58c5f8d95c64c9c3281b1/jiter-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb4790497369d134a07fc763cc88888c46f734abdd66f9fdf7865038bf3a8f40", size = 313405, upload-time = "2025-09-15T09:20:18.918Z" }, + { url = "https://files.pythonhosted.org/packages/af/22/7ab7b4ec3a1c1f03aef376af11d23b05abcca3fb31fbca1e7557053b1ba2/jiter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406", size = 347102, upload-time = "2025-09-15T09:20:20.16Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/ce100253c80063a7b8b406e1d1562657fd4b9b4e1b562db40e68645342fb/jiter-0.11.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:902b43386c04739229076bd1c4c69de5d115553d982ab442a8ae82947c72ede7", size = 336380, upload-time = "2025-09-15T09:20:36.867Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, + { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, + { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, + { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, + { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, + { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, + { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "leann-backend-diskann" +version = "0.3.7" +source = { editable = "packages/leann-backend-diskann" } +dependencies = [ + { name = "leann-core" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "protobuf" }, +] + +[package.metadata] +requires-dist = [ + { name = "leann-core", specifier = "==0.3.7" }, + { name = "numpy" }, + { name = "protobuf", specifier = ">=3.19.0" }, +] + +[[package]] +name = "leann-backend-flashlib" +version = "0.3.6" +source = { editable = "packages/leann-backend-flashlib" } +dependencies = [ + { name = "flashlib" }, + { name = "leann-core" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "torch" }, +] + +[package.metadata] +requires-dist = [ + { name = "flashlib" }, + { name = "leann-backend-hnsw", marker = "extra == 'query-server'", specifier = ">=0.3.6" }, + { name = "leann-core", specifier = ">=0.3.6" }, + { name = "msgpack", marker = "extra == 'query-server'", specifier = ">=1.0.0" }, + { name = "numpy", specifier = ">=1.20.0" }, + { name = "pyzmq", marker = "extra == 'query-server'", specifier = ">=23.0.0" }, + { name = "torch" }, +] +provides-extras = ["query-server"] + +[[package]] +name = "leann-backend-flashlib-ivf" +version = "0.3.6" +source = { editable = "packages/leann-backend-flashlib-ivf" } +dependencies = [ + { name = "flashlib" }, + { name = "leann-core" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "torch" }, +] + +[package.metadata] +requires-dist = [ + { name = "flashlib" }, + { name = "leann-backend-hnsw", marker = "extra == 'query-server'", specifier = ">=0.3.6" }, + { name = "leann-core", specifier = ">=0.3.6" }, + { name = "msgpack", marker = "extra == 'query-server'", specifier = ">=1.0.0" }, + { name = "numpy", specifier = ">=1.20.0" }, + { name = "pyzmq", marker = "extra == 'query-server'", specifier = ">=23.0.0" }, + { name = "torch" }, +] +provides-extras = ["query-server"] + +[[package]] +name = "leann-backend-hnsw" +version = "0.3.7" +source = { editable = "packages/leann-backend-hnsw" } +dependencies = [ + { name = "leann-core" }, + { name = "msgpack" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pyzmq" }, +] + +[package.metadata] +requires-dist = [ + { name = "leann-core", specifier = "==0.3.7" }, + { name = "msgpack", specifier = ">=1.0.0" }, + { name = "numpy" }, + { name = "pyzmq", specifier = ">=23.0.0" }, +] + +[[package]] +name = "leann-core" +version = "0.3.7" +source = { editable = "packages/leann-core" } +dependencies = [ + { name = "accelerate" }, + { name = "gitignore-parser" }, + { name = "huggingface-hub" }, + { name = "llama-index-core" }, + { name = "llama-index-embeddings-huggingface" }, + { name = "llama-index-readers-file" }, + { name = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "msgpack" }, + { name = "nbconvert" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "openai" }, + { name = "pdfplumber" }, + { name = "psutil" }, + { name = "pymupdf" }, + { name = "pypdf2" }, + { name = "python-dotenv" }, + { name = "pyzmq" }, + { name = "requests" }, + { name = "sentence-transformers" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, +] + +[package.metadata] +requires-dist = [ + { name = "accelerate", specifier = ">=0.20.0" }, + { name = "accelerate", marker = "extra == 'colab'", specifier = ">=0.20.0,<1.0.0" }, + { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.115.0" }, + { name = "gitignore-parser", specifier = ">=0.1.12" }, + { name = "huggingface-hub", specifier = ">=0.20.0" }, + { name = "llama-index-core", specifier = ">=0.12.0" }, + { name = "llama-index-embeddings-huggingface", specifier = ">=0.5.5" }, + { name = "llama-index-readers-file", specifier = ">=0.4.0" }, + { name = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'", specifier = ">=0.26.3" }, + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'", specifier = ">=0.26.0" }, + { name = "msgpack", specifier = ">=1.0.0" }, + { name = "nbconvert", specifier = ">=7.0.0" }, + { name = "numpy", specifier = ">=1.20.0,<3" }, + { name = "openai", specifier = ">=1.0.0" }, + { name = "pdfplumber", specifier = ">=0.10.0" }, + { name = "psutil", specifier = ">=5.8.0" }, + { name = "pydantic", marker = "extra == 'server'", specifier = ">=2.0.0" }, + { name = "pymupdf", specifier = ">=1.23.0" }, + { name = "pypdf2", specifier = ">=3.0.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "pyzmq", specifier = ">=23.0.0" }, + { name = "requests", specifier = ">=2.25.0" }, + { name = "sentence-transformers", specifier = ">=3.0.0" }, + { name = "torch", specifier = ">=2.0.0" }, + { name = "torch", marker = "python_full_version < '3.13' and sys_platform == 'linux' and extra == 'cpu'", specifier = "==2.2.2" }, + { name = "torch", marker = "extra == 'colab'", specifier = ">=2.0.0,<3.0.0" }, + { name = "tqdm", specifier = ">=4.60.0" }, + { name = "transformers", marker = "python_full_version < '3.10'", specifier = ">=4.30.0,<4.46" }, + { name = "transformers", marker = "python_full_version >= '3.10'", specifier = ">=4.53.1,<4.58" }, + { name = "transformers", marker = "python_full_version >= '3.10' and extra == 'colab'", specifier = ">=4.53.1,<4.58" }, + { name = "transformers", marker = "python_full_version < '3.10' and extra == 'colab'", specifier = ">=4.30.0,<4.46" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.34.0" }, +] +provides-extras = ["cpu", "colab", "server"] + +[[package]] +name = "leann-workspace" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "astchunk" }, + { name = "boto3" }, + { name = "colorama" }, + { name = "datasets" }, + { name = "einops" }, + { name = "evaluate" }, + { name = "gitignore-parser" }, + { name = "ipykernel" }, + { name = "leann-backend-hnsw" }, + { name = "leann-core" }, + { name = "llama-index" }, + { name = "llama-index-embeddings-huggingface" }, + { name = "llama-index-readers-file" }, + { name = "llama-index-vector-stores-faiss" }, + { name = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "msgpack" }, + { name = "nbconvert" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "ollama" }, + { name = "openai" }, + { name = "pathspec" }, + { name = "pdfplumber" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "pybind11" }, + { name = "pymupdf" }, + { name = "pypdf2" }, + { name = "pypdfium2" }, + { name = "requests" }, + { name = "seaborn" }, + { name = "sentence-transformers" }, + { name = "sglang" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "tree-sitter" }, + { name = "tree-sitter-c-sharp" }, + { name = "tree-sitter-java" }, + { name = "tree-sitter-python" }, + { name = "tree-sitter-typescript" }, + { name = "typer" }, +] + +[package.optional-dependencies] +diskann = [ + { name = "leann-backend-diskann" }, +] +documents = [ + { name = "beautifulsoup4" }, + { name = "docx2txt" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "python-docx" }, +] +flashlib = [ + { name = "leann-backend-flashlib" }, +] +flashlib-ivf = [ + { name = "leann-backend-flashlib-ivf" }, +] + +[package.dev-dependencies] +dev = [ + { name = "huggingface-hub" }, + { name = "matplotlib" }, +] +lint = [ + { name = "pre-commit" }, + { name = "ruff" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "astchunk", editable = "packages/astchunk-leann" }, + { name = "beautifulsoup4", marker = "extra == 'documents'", specifier = ">=4.13.0" }, + { name = "boto3" }, + { name = "colorama" }, + { name = "datasets", specifier = ">=2.15.0" }, + { name = "docx2txt", marker = "extra == 'documents'", specifier = ">=0.9" }, + { name = "einops" }, + { name = "evaluate" }, + { name = "gitignore-parser", specifier = ">=0.1.12" }, + { name = "ipykernel", specifier = "==6.29.5" }, + { name = "leann-backend-diskann", marker = "extra == 'diskann'", editable = "packages/leann-backend-diskann" }, + { name = "leann-backend-flashlib", marker = "extra == 'flashlib'", editable = "packages/leann-backend-flashlib" }, + { name = "leann-backend-flashlib-ivf", marker = "extra == 'flashlib-ivf'", editable = "packages/leann-backend-flashlib-ivf" }, + { name = "leann-backend-hnsw", editable = "packages/leann-backend-hnsw" }, + { name = "leann-core", editable = "packages/leann-core" }, + { name = "llama-index", specifier = ">=0.12.44" }, + { name = "llama-index-embeddings-huggingface", specifier = ">=0.5.5" }, + { name = "llama-index-readers-file", specifier = ">=0.4.0" }, + { name = "llama-index-vector-stores-faiss", specifier = ">=0.4.0" }, + { name = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'", specifier = ">=0.26.3" }, + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'", specifier = ">=0.26.0" }, + { name = "msgpack", specifier = ">=1.1.1" }, + { name = "nbconvert", specifier = ">=7.16.6" }, + { name = "numpy", specifier = ">=1.26.0" }, + { name = "ollama" }, + { name = "openai", specifier = ">=1.0.0" }, + { name = "openpyxl", marker = "extra == 'documents'", specifier = ">=3.1.0" }, + { name = "pandas", marker = "extra == 'documents'", specifier = ">=2.2.0" }, + { name = "pathspec", specifier = ">=0.12.1" }, + { name = "pdfplumber", specifier = ">=0.11.0" }, + { name = "protobuf", specifier = "==4.25.3" }, + { name = "psutil", specifier = ">=5.8.0" }, + { name = "pybind11", specifier = ">=3.0.0" }, + { name = "pymupdf", specifier = ">=1.26.0" }, + { name = "pypdf2", specifier = ">=3.0.0" }, + { name = "pypdfium2", specifier = ">=4.30.0" }, + { name = "python-docx", marker = "extra == 'documents'", specifier = ">=0.8.11" }, + { name = "requests", specifier = ">=2.25.0" }, + { name = "seaborn" }, + { name = "sentence-transformers", specifier = ">=3.0.0" }, + { name = "sglang" }, + { name = "torch" }, + { name = "torchvision", specifier = ">=0.23.0" }, + { name = "tqdm" }, + { name = "transformers", marker = "python_full_version < '3.10'", specifier = "<4.46" }, + { name = "transformers", marker = "python_full_version >= '3.10'", specifier = ">=4.53.1,<4.58" }, + { name = "tree-sitter", specifier = ">=0.20.0" }, + { name = "tree-sitter-c-sharp", specifier = ">=0.20.0" }, + { name = "tree-sitter-java", specifier = ">=0.20.0" }, + { name = "tree-sitter-python", specifier = ">=0.20.0" }, + { name = "tree-sitter-typescript", specifier = ">=0.20.0" }, + { name = "typer", specifier = ">=0.12.3" }, +] +provides-extras = ["diskann", "flashlib", "flashlib-ivf", "documents"] + +[package.metadata.requires-dev] +dev = [ + { name = "huggingface-hub", specifier = ">=0.20.0" }, + { name = "matplotlib" }, +] +lint = [ + { name = "pre-commit", specifier = ">=3.5.0" }, + { name = "ruff", specifier = "==0.12.7" }, +] +test = [ + { name = "pytest", specifier = ">=9.0" }, + { name = "pytest-cov", specifier = ">=4.0" }, + { name = "pytest-timeout", specifier = ">=2.0" }, + { name = "pytest-xdist", specifier = ">=3.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, +] + +[[package]] +name = "llama-cloud" +version = "0.1.35" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/72/816e6e900448e1b4a8137d90e65876b296c5264a23db6ae888bd3e6660ba/llama_cloud-0.1.35.tar.gz", hash = "sha256:200349d5d57424d7461f304cdb1355a58eea3e6ca1e6b0d75c66b2e937216983", size = 106403, upload-time = "2025-07-28T17:22:06.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/d2/8d18a021ab757cea231428404f21fe3186bf1ebaac3f57a73c379483fd3f/llama_cloud-0.1.35-py3-none-any.whl", hash = "sha256:b7abab4423118e6f638d2f326749e7a07c6426543bea6da99b623c715b22af71", size = 303280, upload-time = "2025-07-28T17:22:04.946Z" }, +] + +[[package]] +name = "llama-cloud-services" +version = "0.6.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "llama-cloud" }, + { name = "llama-index-core" }, + { name = "platformdirs" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/0c/8ca87d33bea0340a8ed791f36390112aeb29fd3eebfd64b6aef6204a03f0/llama_cloud_services-0.6.54.tar.gz", hash = "sha256:baf65d9bffb68f9dca98ac6e22908b6675b2038b021e657ead1ffc0e43cbd45d", size = 53468, upload-time = "2025-08-01T20:09:20.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/48/4e295e3f791b279885a2e584f71e75cbe4ac84e93bba3c36e2668f60a8ac/llama_cloud_services-0.6.54-py3-none-any.whl", hash = "sha256:07f595f7a0ba40c6a1a20543d63024ca7600fe65c4811d1951039977908997be", size = 63874, upload-time = "2025-08-01T20:09:20.076Z" }, +] + +[[package]] +name = "llama-index" +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-cli" }, + { name = "llama-index-core" }, + { name = "llama-index-embeddings-openai" }, + { name = "llama-index-indices-managed-llama-cloud" }, + { name = "llama-index-llms-openai" }, + { name = "llama-index-readers-file" }, + { name = "llama-index-readers-llama-parse" }, + { name = "nltk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/b4/7dc38b594bc83e8f6fc1fb5acf4032e09728df4bd0f3be22e4ede35361eb/llama_index-0.14.2.tar.gz", hash = "sha256:ff3b20c99cc372261f4d8e75b59e35ee7e3d9f99c83ad0a566f66d4fff4fb0a9", size = 8408, upload-time = "2025-09-16T05:11:04.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/5b/f3c128a9acb94f4ae37234f0fb750d24aee65cb1ba5061ee62eeb2d6d4c9/llama_index-0.14.2-py3-none-any.whl", hash = "sha256:a0ad81fb9143120083c1aa00dfed41c4267d7c272cca42f7c4903f7cd262e75b", size = 7422, upload-time = "2025-09-16T05:11:03.15Z" }, +] + +[[package]] +name = "llama-index-cli" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-embeddings-openai" }, + { name = "llama-index-llms-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/e3/ac6928586e20cfd327a2a38a00781cbc8fae923edcd0316c23e38aae1537/llama_index_cli-0.5.1.tar.gz", hash = "sha256:0446159d85c56c29022c1c830c9886f670d5f59d69343c3c029a3b20eda1a9d8", size = 24821, upload-time = "2025-09-12T15:22:44.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/16/b53af5b23921d1e18f57b7a79d557b34554df295c63f5c59d5bee1f5fb47/llama_index_cli-0.5.1-py3-none-any.whl", hash = "sha256:5429b2fd7960df7724c2955b6e6901f6fa910b7b5ecef411c979a8b545a6b7e2", size = 28179, upload-time = "2025-09-12T15:22:43.169Z" }, +] + +[[package]] +name = "llama-index-core" +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aiosqlite" }, + { name = "banks" }, + { name = "dataclasses-json" }, + { name = "deprecated" }, + { name = "dirtyjson" }, + { name = "filetype" }, + { name = "fsspec" }, + { name = "httpx" }, + { name = "llama-index-workflows" }, + { name = "nest-asyncio" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nltk" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pillow" }, + { name = "platformdirs" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "tenacity" }, + { name = "tiktoken" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "typing-inspect" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/82/a89342afc1639d2186546fca1c17a0b0d57e21e62e2bb16e838e41f22f71/llama_index_core-0.14.2.tar.gz", hash = "sha256:352eb4c3d73fb848bfe08f2d0f9f9aefd4c0789b651ada3c9fd17f638ac1c87b", size = 11577624, upload-time = "2025-09-16T03:43:21.598Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/93/df13d337edc51e77a449e21299cf82d4f267018d73fefd05659e269edaa4/llama_index_core-0.14.2-py3-none-any.whl", hash = "sha256:96e536ece3f81bbf2169c772965d23e215f9e31aa15a761dd50fa05d3d1e53b9", size = 11918780, upload-time = "2025-09-16T03:43:18.849Z" }, +] + +[[package]] +name = "llama-index-embeddings-huggingface" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", extra = ["inference"] }, + { name = "llama-index-core" }, + { name = "sentence-transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/a0/77beca4ed28af68db6ab9c647b3fa75fae905d33ace96e91010cc9b96027/llama_index_embeddings_huggingface-0.6.1.tar.gz", hash = "sha256:3b21ffeda22f8221ed55778bb3daed71664ab07b341f1dd2f408963bd20355b9", size = 8694, upload-time = "2025-09-08T20:25:27.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/b0/4d327cb2f2e039606feb9345b4de975090b9659c4ee84b00443bb149a80d/llama_index_embeddings_huggingface-0.6.1-py3-none-any.whl", hash = "sha256:b63990cf71ee7a36c51f36657133fcf76130e9bf5dcf9eb5a73a5087106d6881", size = 8903, upload-time = "2025-09-08T20:25:27.038Z" }, +] + +[[package]] +name = "llama-index-embeddings-openai" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/36/90336d054a5061a3f5bc17ac2c18ef63d9d84c55c14d557de484e811ea4d/llama_index_embeddings_openai-0.5.1.tar.gz", hash = "sha256:1c89867a48b0d0daa3d2d44f5e76b394b2b2ef9935932daf921b9e77939ccda8", size = 7020, upload-time = "2025-09-08T20:17:44.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/4a/8ab11026cf8deff8f555aa73919be0bac48332683111e5fc4290f352dc50/llama_index_embeddings_openai-0.5.1-py3-none-any.whl", hash = "sha256:a2fcda3398bbd987b5ce3f02367caee8e84a56b930fdf43cc1d059aa9fd20ca5", size = 7011, upload-time = "2025-09-08T20:17:44.015Z" }, +] + +[[package]] +name = "llama-index-indices-managed-llama-cloud" +version = "0.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "llama-cloud" }, + { name = "llama-index-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/4a/79044fcb3209583d1ffe0c2a7c19dddfb657a03faeb9fe0cf5a74027e646/llama_index_indices_managed_llama_cloud-0.9.4.tar.gz", hash = "sha256:b5e00752ab30564abf19c57595a2107f5697c3b03b085817b4fca84a38ebbd59", size = 15146, upload-time = "2025-09-08T20:29:58.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/6a/0e33245df06afc9766c46a1fe92687be8a09da5d0d0128bc08d84a9f5efa/llama_index_indices_managed_llama_cloud-0.9.4-py3-none-any.whl", hash = "sha256:535a08811046803ca6ab7f8e9d510e926aa5306608b02201ad3d9d21701383bc", size = 17005, upload-time = "2025-09-08T20:29:57.876Z" }, +] + +[[package]] +name = "llama-index-instrumentation" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/e5/a3628da5d716d6bbc2c0a8d39b629dff81b33d5625c5b934e1456370064f/llama_index_instrumentation-0.4.1.tar.gz", hash = "sha256:a79d0dd2baba34f05ff4354d63a99b212322635b8afa6cc96ed00a7e11ebfdc3", size = 45788, upload-time = "2025-09-15T03:53:00.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/c414f4dc9a7dd90d050c387489436bab2d678a566b704ede2f5b62f82ad7/llama_index_instrumentation-0.4.1-py3-none-any.whl", hash = "sha256:0d3ac926d0db3d39c0ec34ee72da5322d61e06b87fe956407e4a1e7a2708b936", size = 15063, upload-time = "2025-09-15T03:52:59.098Z" }, +] + +[[package]] +name = "llama-index-llms-openai" +version = "0.5.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/fe/ac57ecb9b5ea4243e097fbc3f5de22f6bd1a787b72a7c80542af80afbf4d/llama_index_llms_openai-0.5.6.tar.gz", hash = "sha256:92533e83be2eb321d84a01a84fb2bf4506bf684c410cd94ccb29ae6c949a27d4", size = 24239, upload-time = "2025-09-08T20:46:25.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/e2/d78be8cbc645668eba088223d63114a076758626fe12e3b4ec9efa2ba342/llama_index_llms_openai-0.5.6-py3-none-any.whl", hash = "sha256:a93a897fe733a6d7b668cbc6cca546e644054ddf5497821141b2d4b5ffb6ea80", size = 25368, upload-time = "2025-09-08T20:46:23.79Z" }, +] + +[[package]] +name = "llama-index-readers-file" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "defusedxml" }, + { name = "llama-index-core" }, + { name = "pandas" }, + { name = "pypdf" }, + { name = "striprtf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/d9/c67ad2b9cba8dacf1d4a55fe5432357b6eceaecfb096a0de5c1cbd959b98/llama_index_readers_file-0.5.4.tar.gz", hash = "sha256:5e766f32597622e66529464101914548ad683770a0a5d2bdc9ee84eb3a110332", size = 32565, upload-time = "2025-09-08T20:39:40.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/e3/76d72a7281b9c88d488908731c9034e1ee1a2cad5aa1dead76b051eca989/llama_index_readers_file-0.5.4-py3-none-any.whl", hash = "sha256:135be5ddda66c5b35883911918b2d99f67a2ab010d180af5630c872ea9509d45", size = 51827, upload-time = "2025-09-08T20:39:39.408Z" }, +] + +[[package]] +name = "llama-index-readers-llama-parse" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-parse" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/77/5bfaab20e6ec8428dbf2352e18be550c957602723d69383908176b5686cd/llama_index_readers_llama_parse-0.5.1.tar.gz", hash = "sha256:2b78b73faa933e30e6c69df351e4e9f36dfe2ae142e2ab3969ddd2ac48930e37", size = 3858, upload-time = "2025-09-08T20:41:29.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/81/52410c7245dcbf1a54756a9ce3892cdd167ec0b884d696de1304ca3f452e/llama_index_readers_llama_parse-0.5.1-py3-none-any.whl", hash = "sha256:0d41450ed29b0c49c024e206ef6c8e662b1854e77a1c5faefed3b958be54f880", size = 3203, upload-time = "2025-09-08T20:41:28.438Z" }, +] + +[[package]] +name = "llama-index-vector-stores-faiss" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/ee/d1de8a6bfc3a06d6c6212ba114cb428d0e475b8a1f10492b8f62ca309230/llama_index_vector_stores_faiss-0.5.1.tar.gz", hash = "sha256:bec0890d48dc6ba4f982e3c644938cdf88dd15e6802f56a3138f70b4cc2fa632", size = 5907, upload-time = "2025-09-08T20:44:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/2f/9c1e80c819f5eebec227aaf9c62be7dbb62489853b514737263768739343/llama_index_vector_stores_faiss-0.5.1-py3-none-any.whl", hash = "sha256:de03474cf1c3df65a197ecd4b7f056bb0e4f401b3328723ec244b5b26bd51853", size = 7600, upload-time = "2025-09-08T20:44:22.114Z" }, +] + +[[package]] +name = "llama-index-workflows" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-instrumentation" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/be/f1358bc7c0a624b6f6239fdf460060b06699d1259bdea12722ab9617fa20/llama_index_workflows-2.4.0.tar.gz", hash = "sha256:24adc797409b2b575f942ffeec687773126843d251bec4c2b9fd984acf1e6ccb", size = 1290953, upload-time = "2025-09-23T22:58:51.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/a9/97d1e96be588d97a27ab2a966ca2c456eac06ebf2df9af36bacb07debd28/llama_index_workflows-2.4.0-py3-none-any.whl", hash = "sha256:639536d67fbb9c9d75c0789763f774231e6957397afc7368a79900b86a73d44b", size = 62109, upload-time = "2025-09-23T22:58:50.186Z" }, +] + +[[package]] +name = "llama-parse" +version = "0.6.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-cloud-services" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/f6/93b5d123c480bc8c93e6dc3ea930f4f8df8da27f829bb011100ba3ce23dc/llama_parse-0.6.54.tar.gz", hash = "sha256:c707b31152155c9bae84e316fab790bbc8c85f4d8825ce5ee386ebeb7db258f1", size = 3577, upload-time = "2025-08-01T20:09:23.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/50/c5ccd2a50daa0a10c7f3f7d4e6992392454198cd8a7d99fcb96cb60d0686/llama_parse-0.6.54-py3-none-any.whl", hash = "sha256:c66c8d51cf6f29a44eaa8595a595de5d2598afc86e5a33a4cebe5fe228036920", size = 4879, upload-time = "2025-08-01T20:09:22.651Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f5/a1bde3aa8c43524b0acaf3f72fb3d80a32dd29dbb42d7dc434f84584cdcc/llvmlite-0.47.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41270b0b1310717f717cf6f2a9c68d3c43bd7905c33f003825aebc361d0d1b17", size = 37232772, upload-time = "2026-03-31T18:28:12.198Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fb/76d88fc05ee1f9c1a6efe39eb493c4a727e5d1690412469017cd23bcb776/llvmlite-0.47.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9d118bc1dd7623e0e65ca9ac485ec6dd543c3b77bc9928ddc45ebd34e1e30a7", size = 56275179, upload-time = "2026-03-31T18:28:15.725Z" }, + { url = "https://files.pythonhosted.org/packages/4d/08/29da7f36217abd56a0c389ef9a18bea47960826e691ced1a36c92c6ce93c/llvmlite-0.47.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea5cfb04a6ab5b18e46be72b41b015975ba5980c4ddb41f1975b83e19031063", size = 55128632, upload-time = "2026-03-31T18:28:19.946Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/5e12e9ed447d65f04acf6fcf2d79cded2355640b5131a46cee4c99a5949d/llvmlite-0.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:166b896a2262a2039d5fc52df5ee1659bd1ccd081183df7a2fba1b74702dd5ea", size = 38138402, upload-time = "2026-03-31T18:28:23.327Z" }, + { url = "https://files.pythonhosted.org/packages/34/0b/b9d1911cfefa61399821dfb37f486d83e0f42630a8d12f7194270c417002/llvmlite-0.47.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74090f0dcfd6f24ebbef3f21f11e38111c4d7e6919b54c4416e1e357c3446b07", size = 37232770, upload-time = "2026-03-31T18:28:26.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb", size = 56275177, upload-time = "2026-03-31T18:28:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/48a53fedf01cb1f3f43ef200be17ebf83c8d9a04018d3783c1a226c342c2/llvmlite-0.47.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12a69d4bb05f402f30477e21eeabe81911e7c251cecb192bed82cd83c9db10d8", size = 55128631, upload-time = "2026-03-31T18:28:36.046Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/59227d06bdc96e23322713c381af4e77420949d8cd8a042c79e0043096cc/llvmlite-0.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:c37d6eb7aaabfa83ab9c2ff5b5cdb95a5e6830403937b2c588b7490724e05327", size = 38138400, upload-time = "2026-03-31T18:28:40.076Z" }, + { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, + { url = "https://files.pythonhosted.org/packages/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a3c6a735d4e1041808434f9d440faa3d78d9b4af2ee64d05a66f351883b6ceec", size = 37232771, upload-time = "2026-03-31T18:29:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb", size = 56275178, upload-time = "2026-03-31T18:29:05.748Z" }, + { url = "https://files.pythonhosted.org/packages/d6/da/b32cafcb926fb0ce2aa25553bf32cb8764af31438f40e2481df08884c947/llvmlite-0.47.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c6951e2b29930227963e53ee152441f0e14be92e9d4231852102d986c761e40", size = 55128632, upload-time = "2026-03-31T18:29:11.235Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e", size = 38138695, upload-time = "2026-03-31T18:29:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d4/33c8af00f0bf6f552d74f3a054f648af2c5bc6bece97972f3bfadce4f5ec/llvmlite-0.47.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:de966c626c35c9dff5ae7bf12db25637738d0df83fc370cf793bc94d43d92d14", size = 37232773, upload-time = "2026-03-31T18:29:19.453Z" }, + { url = "https://files.pythonhosted.org/packages/64/1d/a760e993e0c0ba6db38d46b9f48f6c7dceb8ac838824997fb9e25f97bc04/llvmlite-0.47.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddbccff2aeaff8670368340a158abefc032fe9b3ccf7d9c496639263d00151aa", size = 56275176, upload-time = "2026-03-31T18:29:24.149Z" }, + { url = "https://files.pythonhosted.org/packages/84/3b/e679bc3b29127182a7f4aa2d2e9e5bea42adb93fb840484147d59c236299/llvmlite-0.47.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a7b778a2e144fc64468fb9bf509ac1226c9813a00b4d7afea5d988c4e22fca", size = 55128631, upload-time = "2026-03-31T18:29:29.536Z" }, + { url = "https://files.pythonhosted.org/packages/be/f7/19e2a09c62809c9e63bbd14ce71fb92c6ff7b7b3045741bb00c781efc3c9/llvmlite-0.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:694e3c2cdc472ed2bd8bd4555ca002eec4310961dd58ef791d508f57b5cc4c94", size = 39153826, upload-time = "2026-03-31T18:29:33.681Z" }, + { url = "https://files.pythonhosted.org/packages/40/a1/581a8c707b5e80efdbbe1dd94527404d33fe50bceb71f39d5a7e11bd57b7/llvmlite-0.47.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:92ec8a169a20b473c1c54d4695e371bde36489fc1efa3688e11e99beba0abf9c", size = 37232772, upload-time = "2026-03-31T18:29:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/11/03/16090dd6f74ba2b8b922276047f15962fbeea0a75d5601607edb301ba945/llvmlite-0.47.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa1cbd800edd3b20bc141521f7fd45a6185a5b84109aa6855134e81397ffe72b", size = 56275178, upload-time = "2026-03-31T18:29:42.58Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cb/0abf1dd4c5286a95ffe0c1d8c67aec06b515894a0dd2ac97f5e27b82ab0b/llvmlite-0.47.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6725179b89f03b17dabe236ff3422cb8291b4c1bf40af152826dfd34e350ae8", size = 55128632, upload-time = "2026-03-31T18:29:46.939Z" }, + { url = "https://files.pythonhosted.org/packages/4f/79/d3bbab197e86e0ff4f9c07122895b66a3e0d024247fcff7f12c473cb36d9/llvmlite-0.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6842cf6f707ec4be3d985a385ad03f72b2d724439e118fcbe99b2929964f0453", size = 39153839, upload-time = "2026-03-31T18:29:51.004Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, + { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, + { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "marshmallow" +version = "3.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/59/c3e6453a9676ffba145309a73c462bb407f4400de7de3f2b41af70720a3c/matplotlib-3.10.6.tar.gz", hash = "sha256:ec01b645840dd1996df21ee37f208cd8ba57644779fa20464010638013d3203c", size = 34804264, upload-time = "2025-08-30T00:14:25.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/dc/ab89f7a5efd0cbaaebf2c3cf1881f4cba20c8925bb43f64511059df76895/matplotlib-3.10.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bc7316c306d97463a9866b89d5cc217824e799fa0de346c8f68f4f3d27c8693d", size = 8247159, upload-time = "2025-08-30T00:12:30.507Z" }, + { url = "https://files.pythonhosted.org/packages/30/a5/ddaee1a383ab28174093644fff7438eddb87bf8dbd58f7b85f5cdd6b2485/matplotlib-3.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d00932b0d160ef03f59f9c0e16d1e3ac89646f7785165ce6ad40c842db16cc2e", size = 8108011, upload-time = "2025-08-30T00:12:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/a53f69bb0522db352b1135bb57cd9fe00fd7252072409392d991d3a755d0/matplotlib-3.10.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fa4c43d6bfdbfec09c733bca8667de11bfa4970e8324c471f3a3632a0301c15", size = 8680518, upload-time = "2025-08-30T00:12:34.387Z" }, + { url = "https://files.pythonhosted.org/packages/5f/31/e059ddce95f68819b005a2d6820b2d6ed0307827a04598891f00649bed2d/matplotlib-3.10.6-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea117a9c1627acaa04dbf36265691921b999cbf515a015298e54e1a12c3af837", size = 9514997, upload-time = "2025-08-30T00:12:36.272Z" }, + { url = "https://files.pythonhosted.org/packages/66/d5/28b408a7c0f07b41577ee27e4454fe329e78ca21fe46ae7a27d279165fb5/matplotlib-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08fc803293b4e1694ee325896030de97f74c141ccff0be886bb5915269247676", size = 9566440, upload-time = "2025-08-30T00:12:41.675Z" }, + { url = "https://files.pythonhosted.org/packages/2d/99/8325b3386b479b1d182ab1a7fd588fd393ff00a99dc04b7cf7d06668cf0f/matplotlib-3.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:2adf92d9b7527fbfb8818e050260f0ebaa460f79d61546374ce73506c9421d09", size = 8108186, upload-time = "2025-08-30T00:12:43.621Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/5d3665aa44c49005aaacaa68ddea6fcb27345961cd538a98bb0177934ede/matplotlib-3.10.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:905b60d1cb0ee604ce65b297b61cf8be9f4e6cfecf95a3fe1c388b5266bc8f4f", size = 8257527, upload-time = "2025-08-30T00:12:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/8c/af/30ddefe19ca67eebd70047dabf50f899eaff6f3c5e6a1a7edaecaf63f794/matplotlib-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bac38d816637343e53d7185d0c66677ff30ffb131044a81898b5792c956ba76", size = 8119583, upload-time = "2025-08-30T00:12:47.236Z" }, + { url = "https://files.pythonhosted.org/packages/d3/29/4a8650a3dcae97fa4f375d46efcb25920d67b512186f8a6788b896062a81/matplotlib-3.10.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:942a8de2b5bfff1de31d95722f702e2966b8a7e31f4e68f7cd963c7cd8861cf6", size = 8692682, upload-time = "2025-08-30T00:12:48.781Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/b793b9cb061cfd5d42ff0f69d1822f8d5dbc94e004618e48a97a8373179a/matplotlib-3.10.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3276c85370bc0dfca051ec65c5817d1e0f8f5ce1b7787528ec8ed2d524bbc2f", size = 9521065, upload-time = "2025-08-30T00:12:50.602Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c5/53de5629f223c1c66668d46ac2621961970d21916a4bc3862b174eb2a88f/matplotlib-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9df5851b219225731f564e4b9e7f2ac1e13c9e6481f941b5631a0f8e2d9387ce", size = 9576888, upload-time = "2025-08-30T00:12:52.92Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/0a18d6d7d2d0a2e66585032a760d13662e5250c784d53ad50434e9560991/matplotlib-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:abb5d9478625dd9c9eb51a06d39aae71eda749ae9b3138afb23eb38824026c7e", size = 8115158, upload-time = "2025-08-30T00:12:54.863Z" }, + { url = "https://files.pythonhosted.org/packages/07/b3/1a5107bb66c261e23b9338070702597a2d374e5aa7004b7adfc754fbed02/matplotlib-3.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:886f989ccfae63659183173bb3fced7fd65e9eb793c3cc21c273add368536951", size = 7992444, upload-time = "2025-08-30T00:12:57.067Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1a/7042f7430055d567cc3257ac409fcf608599ab27459457f13772c2d9778b/matplotlib-3.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31ca662df6a80bd426f871105fdd69db7543e28e73a9f2afe80de7e531eb2347", size = 8272404, upload-time = "2025-08-30T00:12:59.112Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5d/1d5f33f5b43f4f9e69e6a5fe1fb9090936ae7bc8e2ff6158e7a76542633b/matplotlib-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1678bb61d897bb4ac4757b5ecfb02bfb3fddf7f808000fb81e09c510712fda75", size = 8128262, upload-time = "2025-08-30T00:13:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/67/c3/135fdbbbf84e0979712df58e5e22b4f257b3f5e52a3c4aacf1b8abec0d09/matplotlib-3.10.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:56cd2d20842f58c03d2d6e6c1f1cf5548ad6f66b91e1e48f814e4fb5abd1cb95", size = 8697008, upload-time = "2025-08-30T00:13:03.24Z" }, + { url = "https://files.pythonhosted.org/packages/9c/be/c443ea428fb2488a3ea7608714b1bd85a82738c45da21b447dc49e2f8e5d/matplotlib-3.10.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:662df55604a2f9a45435566d6e2660e41efe83cd94f4288dfbf1e6d1eae4b0bb", size = 9530166, upload-time = "2025-08-30T00:13:05.951Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/48441422b044d74034aea2a3e0d1a49023f12150ebc58f16600132b9bbaf/matplotlib-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08f141d55148cd1fc870c3387d70ca4df16dee10e909b3b038782bd4bda6ea07", size = 9593105, upload-time = "2025-08-30T00:13:08.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/c3/994ef20eb4154ab84cc08d033834555319e4af970165e6c8894050af0b3c/matplotlib-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:590f5925c2d650b5c9d813c5b3b5fc53f2929c3f8ef463e4ecfa7e052044fb2b", size = 8122784, upload-time = "2025-08-30T00:13:10.367Z" }, + { url = "https://files.pythonhosted.org/packages/57/b8/5c85d9ae0e40f04e71bedb053aada5d6bab1f9b5399a0937afb5d6b02d98/matplotlib-3.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:f44c8d264a71609c79a78d50349e724f5d5fc3684ead7c2a473665ee63d868aa", size = 7992823, upload-time = "2025-08-30T00:13:12.24Z" }, + { url = "https://files.pythonhosted.org/packages/a0/db/18380e788bb837e724358287b08e223b32bc8dccb3b0c12fa8ca20bc7f3b/matplotlib-3.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:819e409653c1106c8deaf62e6de6b8611449c2cd9939acb0d7d4e57a3d95cc7a", size = 8273231, upload-time = "2025-08-30T00:13:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/38dd49445b297e0d4f12a322c30779df0d43cb5873c7847df8a82e82ec67/matplotlib-3.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59c8ac8382fefb9cb71308dde16a7c487432f5255d8f1fd32473523abecfecdf", size = 8128730, upload-time = "2025-08-30T00:13:15.556Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84e82d9e0fd70c70bc55739defbd8055c54300750cbacf4740c9673a24d6933a", size = 8698539, upload-time = "2025-08-30T00:13:17.297Z" }, + { url = "https://files.pythonhosted.org/packages/71/34/44c7b1f075e1ea398f88aeabcc2907c01b9cc99e2afd560c1d49845a1227/matplotlib-3.10.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25f7a3eb42d6c1c56e89eacd495661fc815ffc08d9da750bca766771c0fd9110", size = 9529702, upload-time = "2025-08-30T00:13:19.248Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7f/e5c2dc9950c7facaf8b461858d1b92c09dd0cf174fe14e21953b3dda06f7/matplotlib-3.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9c862d91ec0b7842920a4cfdaaec29662195301914ea54c33e01f1a28d014b2", size = 9593742, upload-time = "2025-08-30T00:13:21.181Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1d/70c28528794f6410ee2856cd729fa1f1756498b8d3126443b0a94e1a8695/matplotlib-3.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:1b53bd6337eba483e2e7d29c5ab10eee644bc3a2491ec67cc55f7b44583ffb18", size = 8122753, upload-time = "2025-08-30T00:13:23.44Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/0e1670501fc7d02d981564caf7c4df42974464625935424ca9654040077c/matplotlib-3.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:cbd5eb50b7058b2892ce45c2f4e92557f395c9991f5c886d1bb74a1582e70fd6", size = 7992973, upload-time = "2025-08-30T00:13:26.632Z" }, + { url = "https://files.pythonhosted.org/packages/b1/4e/60780e631d73b6b02bd7239f89c451a72970e5e7ec34f621eda55cd9a445/matplotlib-3.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:acc86dd6e0e695c095001a7fccff158c49e45e0758fdf5dcdbb0103318b59c9f", size = 8316869, upload-time = "2025-08-30T00:13:28.262Z" }, + { url = "https://files.pythonhosted.org/packages/f8/15/baa662374a579413210fc2115d40c503b7360a08e9cc254aa0d97d34b0c1/matplotlib-3.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e228cd2ffb8f88b7d0b29e37f68ca9aaf83e33821f24a5ccc4f082dd8396bc27", size = 8178240, upload-time = "2025-08-30T00:13:30.007Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3f/3c38e78d2aafdb8829fcd0857d25aaf9e7dd2dfcf7ec742765b585774931/matplotlib-3.10.6-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:658bc91894adeab669cf4bb4a186d049948262987e80f0857216387d7435d833", size = 8711719, upload-time = "2025-08-30T00:13:31.72Z" }, + { url = "https://files.pythonhosted.org/packages/96/4b/2ec2bbf8cefaa53207cc56118d1fa8a0f9b80642713ea9390235d331ede4/matplotlib-3.10.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8913b7474f6dd83ac444c9459c91f7f0f2859e839f41d642691b104e0af056aa", size = 9541422, upload-time = "2025-08-30T00:13:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/83/7d/40255e89b3ef11c7871020563b2dd85f6cb1b4eff17c0f62b6eb14c8fa80/matplotlib-3.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:091cea22e059b89f6d7d1a18e2c33a7376c26eee60e401d92a4d6726c4e12706", size = 9594068, upload-time = "2025-08-30T00:13:35.833Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a9/0213748d69dc842537a113493e1c27daf9f96bd7cc316f933dc8ec4de985/matplotlib-3.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:491e25e02a23d7207629d942c666924a6b61e007a48177fdd231a0097b7f507e", size = 8200100, upload-time = "2025-08-30T00:13:37.668Z" }, + { url = "https://files.pythonhosted.org/packages/be/15/79f9988066ce40b8a6f1759a934ea0cde8dc4adc2262255ee1bc98de6ad0/matplotlib-3.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3d80d60d4e54cda462e2cd9a086d85cd9f20943ead92f575ce86885a43a565d5", size = 8042142, upload-time = "2025-08-30T00:13:39.426Z" }, + { url = "https://files.pythonhosted.org/packages/7c/58/e7b6d292beae6fb4283ca6fb7fa47d7c944a68062d6238c07b497dd35493/matplotlib-3.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:70aaf890ce1d0efd482df969b28a5b30ea0b891224bb315810a3940f67182899", size = 8273802, upload-time = "2025-08-30T00:13:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f6/7882d05aba16a8cdd594fb9a03a9d3cca751dbb6816adf7b102945522ee9/matplotlib-3.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1565aae810ab79cb72e402b22facfa6501365e73ebab70a0fdfb98488d2c3c0c", size = 8131365, upload-time = "2025-08-30T00:13:42.664Z" }, + { url = "https://files.pythonhosted.org/packages/94/bf/ff32f6ed76e78514e98775a53715eca4804b12bdcf35902cdd1cf759d324/matplotlib-3.10.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3b23315a01981689aa4e1a179dbf6ef9fbd17143c3eea77548c2ecfb0499438", size = 9533961, upload-time = "2025-08-30T00:13:44.372Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/6bf88c2fc2da7708a2ff8d2eeb5d68943130f50e636d5d3dcf9d4252e971/matplotlib-3.10.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30fdd37edf41a4e6785f9b37969de57aea770696cb637d9946eb37470c94a453", size = 9804262, upload-time = "2025-08-30T00:13:46.614Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7a/e05e6d9446d2d577b459427ad060cd2de5742d0e435db3191fea4fcc7e8b/matplotlib-3.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bc31e693da1c08012c764b053e702c1855378e04102238e6a5ee6a7117c53a47", size = 9595508, upload-time = "2025-08-30T00:13:48.731Z" }, + { url = "https://files.pythonhosted.org/packages/39/fb/af09c463ced80b801629fd73b96f726c9f6124c3603aa2e480a061d6705b/matplotlib-3.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:05be9bdaa8b242bc6ff96330d18c52f1fc59c6fb3a4dd411d953d67e7e1baf98", size = 8252742, upload-time = "2025-08-30T00:13:50.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f9/b682f6db9396d9ab8f050c0a3bfbb5f14fb0f6518f08507c04cc02f8f229/matplotlib-3.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:f56a0d1ab05d34c628592435781d185cd99630bdfd76822cd686fb5a0aecd43a", size = 8124237, upload-time = "2025-08-30T00:13:54.3Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d2/b69b4a0923a3c05ab90527c60fdec899ee21ca23ede7f0fb818e6620d6f2/matplotlib-3.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:94f0b4cacb23763b64b5dace50d5b7bfe98710fed5f0cef5c08135a03399d98b", size = 8316956, upload-time = "2025-08-30T00:13:55.932Z" }, + { url = "https://files.pythonhosted.org/packages/28/e9/dc427b6f16457ffaeecb2fc4abf91e5adb8827861b869c7a7a6d1836fa73/matplotlib-3.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cc332891306b9fb39462673d8225d1b824c89783fee82840a709f96714f17a5c", size = 8178260, upload-time = "2025-08-30T00:14:00.942Z" }, + { url = "https://files.pythonhosted.org/packages/c4/89/1fbd5ad611802c34d1c7ad04607e64a1350b7fb9c567c4ec2c19e066ed35/matplotlib-3.10.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee1d607b3fb1590deb04b69f02ea1d53ed0b0bf75b2b1a5745f269afcbd3cdd3", size = 9541422, upload-time = "2025-08-30T00:14:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/65fec8716025b22c1d72d5a82ea079934c76a547696eaa55be6866bc89b1/matplotlib-3.10.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:376a624a218116461696b27b2bbf7a8945053e6d799f6502fc03226d077807bf", size = 9803678, upload-time = "2025-08-30T00:14:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b0/40fb2b3a1ab9381bb39a952e8390357c8be3bdadcf6d5055d9c31e1b35ae/matplotlib-3.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:83847b47f6524c34b4f2d3ce726bb0541c48c8e7692729865c3df75bfa0f495a", size = 9594077, upload-time = "2025-08-30T00:14:07.012Z" }, + { url = "https://files.pythonhosted.org/packages/76/34/c4b71b69edf5b06e635eee1ed10bfc73cf8df058b66e63e30e6a55e231d5/matplotlib-3.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c7e0518e0d223683532a07f4b512e2e0729b62674f1b3a1a69869f98e6b1c7e3", size = 8342822, upload-time = "2025-08-30T00:14:09.041Z" }, + { url = "https://files.pythonhosted.org/packages/e8/62/aeabeef1a842b6226a30d49dd13e8a7a1e81e9ec98212c0b5169f0a12d83/matplotlib-3.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:4dd83e029f5b4801eeb87c64efd80e732452781c16a9cf7415b7b63ec8f374d7", size = 8172588, upload-time = "2025-08-30T00:14:11.166Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/2551e45bea2938e0363ccdd54fa08dae7605ce782d4332497d31a7b97672/matplotlib-3.10.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:13fcd07ccf17e354398358e0307a1f53f5325dca22982556ddb9c52837b5af41", size = 8241220, upload-time = "2025-08-30T00:14:12.888Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/0f4c6e8b98105fdb162a4efde011af204ca47d7c05d735aff480ebfead1b/matplotlib-3.10.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:470fc846d59d1406e34fa4c32ba371039cd12c2fe86801159a965956f2575bd1", size = 8104624, upload-time = "2025-08-30T00:14:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/27/27/c29696702b9317a6ade1ba6f8861e02d7423f18501729203d7a80b686f23/matplotlib-3.10.6-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7173f8551b88f4ef810a94adae3128c2530e0d07529f7141be7f8d8c365f051", size = 8682271, upload-time = "2025-08-30T00:14:17.273Z" }, + { url = "https://files.pythonhosted.org/packages/12/bb/02c35a51484aae5f49bd29f091286e7af5f3f677a9736c58a92b3c78baeb/matplotlib-3.10.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f2d684c3204fa62421bbf770ddfebc6b50130f9cad65531eeba19236d73bb488", size = 8252296, upload-time = "2025-08-30T00:14:19.49Z" }, + { url = "https://files.pythonhosted.org/packages/7d/85/41701e3092005aee9a2445f5ee3904d9dbd4a7df7a45905ffef29b7ef098/matplotlib-3.10.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6f4a69196e663a41d12a728fab8751177215357906436804217d6d9cf0d4d6cf", size = 8116749, upload-time = "2025-08-30T00:14:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/16/53/8d8fa0ea32a8c8239e04d022f6c059ee5e1b77517769feccd50f1df43d6d/matplotlib-3.10.6-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d6ca6ef03dfd269f4ead566ec6f3fb9becf8dab146fb999022ed85ee9f6b3eb", size = 8693933, upload-time = "2025-08-30T00:14:22.942Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistune" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/02/a7fb8b21d4d55ac93cdcde9d3638da5dd0ebdd3a4fed76c7725e10b81cbe/mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164", size = 94588, upload-time = "2025-08-29T07:20:43.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" }, +] + +[[package]] +name = "mlx" +version = "0.29.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mlx-metal", marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/25/b5a25a48945aad74dbfe027f36b824014b3fec4cae64fdb9cc72793e18b3/mlx-0.29.1-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:6a10d589a439346be1b7d8d0bbfe6cba45f1ef592f406b247b2da7131a9242c2", size = 546323, upload-time = "2025-09-12T00:17:25.858Z" }, + { url = "https://files.pythonhosted.org/packages/68/5e/2e40f67193f22370142b255bf557bc1b7aad9e3431991285bf65e65940d9/mlx-0.29.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:673505b631f05041d7634366c8f52bf38d80439af44592f6e4291e59bb17a243", size = 546321, upload-time = "2025-09-12T00:17:59.129Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/2e2cdcfa593f331db9d4b08e06d55413289d7ba58ca529f0185a4244a3dd/mlx-0.29.1-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:4fd9e75f778cbd4a1f0060d090824863da8935a0e071f4747a4bc8f4b8d83838", size = 546321, upload-time = "2025-09-12T00:17:39.593Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ca/258aaef43db68f0cc5489eaf6f09b9b3f5f186cc271d84526c5add8e8c86/mlx-0.29.1-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4da94700d8d19966f56962d16fe12510e41c3a85d3c90c6fb532016a8ab3c6d5", size = 546491, upload-time = "2025-09-12T00:17:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/73e59439ffd9eb03e34a6d8c1c73cf56326f9d561f54cf238ca8b1edbad8/mlx-0.29.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0edf6c2c34bdd073741f583ea72ad538bbec26ebf66dc907478bbd68f942b5b8", size = 546489, upload-time = "2025-09-12T00:17:45.024Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/1448db66bb98dd1453089fcf2b285fa7bad744e549609927fa636d160ae9/mlx-0.29.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:03dd298a3818ef32a764edbf602b2233739ab8eb3fd095b8a2ff4c994d373cdf", size = 546488, upload-time = "2025-09-12T00:17:30.04Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/907e49f4b25c3bc98a3b407ac46c47181f587442840a3b462903c9b2458d/mlx-0.29.1-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:1c437c32931ab69e514dfec25d13e319cbe43b2bd524047cbbe103e8be32d903", size = 546945, upload-time = "2025-09-12T00:17:58.841Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8f/c9a166a5070aeff9b5bd8b7ecc681113c764ceab645d7bfee0da4e6b79c4/mlx-0.29.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e0a7b9ac66a8000eef75ec2cb4394a2133a2b7b2290606ade47d7c1a1d05d16e", size = 546939, upload-time = "2025-09-12T00:17:21.042Z" }, + { url = "https://files.pythonhosted.org/packages/ad/28/5d6a4c3708630550dc35d1de67ddac1011b8112f313004fe98dba559f8c7/mlx-0.29.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6b5795655fe1a313bbfa52f16a4b23226bef32e47bfc81d3885e9487ca34e7f7", size = 546936, upload-time = "2025-09-12T00:18:00.329Z" }, + { url = "https://files.pythonhosted.org/packages/66/62/7691ea664123d6e1fc0626207d5f1a6ed2b92b71059f4be42634e89b479e/mlx-0.29.1-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:e86644cef409a00dd46eb9debf0796899623c686d16cc25b6e83078fb5081eba", size = 546904, upload-time = "2025-09-12T00:17:43.197Z" }, + { url = "https://files.pythonhosted.org/packages/44/b8/1a77cafb6302703fe5576b2298f533cb36b6721fa6d9c41a9d6078c14a89/mlx-0.29.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:fd27d49f631ecc9d0a766327e65236738e338c74c7be504c22a1e53801eb40d1", size = 546909, upload-time = "2025-09-12T00:17:15.127Z" }, + { url = "https://files.pythonhosted.org/packages/79/f1/1f4ddf70d1f77993e25f25fb0ab8f5579d81fce6a2a554400c75b447c148/mlx-0.29.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:aaeacf864163b645ddd58c57e65290bf4c8cd493378e89dd11c00d2c9c42b42d", size = 546904, upload-time = "2025-09-12T00:17:09.691Z" }, +] + +[[package]] +name = "mlx-lm" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "mlx" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/8d/44e0d873f0859426bfbcde076e63a75e6700f89baad9e91c90fa7fd1d000/mlx_lm-0.28.0.tar.gz", hash = "sha256:42d0304e45a09e68c73a851210c019a6fd3fc61221632c9448cf03b31bc4cac5", size = 203441, upload-time = "2025-09-17T21:27:25.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/1e/cdae9675b02e5658241abf9ce9372eb5dc1a44a835f6b62c56864b0505fb/mlx_lm-0.28.0-py3-none-any.whl", hash = "sha256:a2002b14a73ac6d88685a5e6ff5c5d86811157288468bf16aff7a94e6c872f3d", size = 277949, upload-time = "2025-09-17T21:27:23.995Z" }, +] + +[[package]] +name = "mlx-metal" +version = "0.29.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/b4/c96f54061fff12c2acc06f2cd402aae4a9cba52e40aae51f71ae508ef206/mlx_metal-0.29.1-py3-none-macosx_13_0_arm64.whl", hash = "sha256:b9dadd432948eab196ed110db0dc745795fd516b7124c0d3c4d176fee678a07a", size = 34983555, upload-time = "2025-09-12T00:19:44.815Z" }, + { url = "https://files.pythonhosted.org/packages/82/3a/45c9ea1b6741a5dc80ad0b57eeee09e544a0d89ca66c7ad6cc55887c00d8/mlx_metal-0.29.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:824b939b721a964a455aeea4d0e956e4cc945f3333522c1e72a077ae774bca49", size = 34712571, upload-time = "2025-09-12T00:19:26.183Z" }, + { url = "https://files.pythonhosted.org/packages/64/7f/294c8cac159661d732e5c01f841e07edfd2ea90651d39faca6579b3cdbf4/mlx_metal-0.29.1-py3-none-macosx_15_0_arm64.whl", hash = "sha256:ebd9ba8e83213f929663b92b8065b451a4276c7002ed83eae0fc8dde721c50c5", size = 34704543, upload-time = "2025-09-12T00:18:59.595Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555, upload-time = "2025-06-13T06:52:51.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/52/f30da112c1dc92cf64f57d08a273ac771e7b29dea10b4b30369b2d7e8546/msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed", size = 81799, upload-time = "2025-06-13T06:51:37.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/7bfc0def2f04ab4145f7f108e3563f9b4abae4ab0ed78a61f350518cc4d2/msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8", size = 78278, upload-time = "2025-06-13T06:51:38.534Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c5/df5d6c1c39856bc55f800bf82778fd4c11370667f9b9e9d51b2f5da88f20/msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2", size = 402805, upload-time = "2025-06-13T06:51:39.538Z" }, + { url = "https://files.pythonhosted.org/packages/20/8e/0bb8c977efecfe6ea7116e2ed73a78a8d32a947f94d272586cf02a9757db/msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4", size = 408642, upload-time = "2025-06-13T06:51:41.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/731d52c1aeec52006be6d1f8027c49fdc2cfc3ab7cbe7c28335b2910d7b6/msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0", size = 395143, upload-time = "2025-06-13T06:51:42.575Z" }, + { url = "https://files.pythonhosted.org/packages/2b/92/b42911c52cda2ba67a6418ffa7d08969edf2e760b09015593c8a8a27a97d/msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26", size = 395986, upload-time = "2025-06-13T06:51:43.807Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/8ae165337e70118d4dab651b8b562dd5066dd1e6dd57b038f32ebc3e2f07/msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75", size = 402682, upload-time = "2025-06-13T06:51:45.534Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/555851cb98dcbd6ce041df1eacb25ac30646575e9cd125681aa2f4b1b6f1/msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338", size = 406368, upload-time = "2025-06-13T06:51:46.97Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/39a26add4ce16f24e99eabb9005e44c663db00e3fce17d4ae1ae9d61df99/msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd", size = 65004, upload-time = "2025-06-13T06:51:48.582Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/73dfa3e9d5d7450d39debde5b0d848139f7de23bd637a4506e36c9800fd6/msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8", size = 71548, upload-time = "2025-06-13T06:51:49.558Z" }, + { url = "https://files.pythonhosted.org/packages/7f/83/97f24bf9848af23fe2ba04380388216defc49a8af6da0c28cc636d722502/msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558", size = 82728, upload-time = "2025-06-13T06:51:50.68Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/2eaa388267a78401f6e182662b08a588ef4f3de6f0eab1ec09736a7aaa2b/msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d", size = 79279, upload-time = "2025-06-13T06:51:51.72Z" }, + { url = "https://files.pythonhosted.org/packages/f8/46/31eb60f4452c96161e4dfd26dbca562b4ec68c72e4ad07d9566d7ea35e8a/msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0", size = 423859, upload-time = "2025-06-13T06:51:52.749Z" }, + { url = "https://files.pythonhosted.org/packages/45/16/a20fa8c32825cc7ae8457fab45670c7a8996d7746ce80ce41cc51e3b2bd7/msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f", size = 429975, upload-time = "2025-06-13T06:51:53.97Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/6c958e07692367feeb1a1594d35e22b62f7f476f3c568b002a5ea09d443d/msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704", size = 413528, upload-time = "2025-06-13T06:51:55.507Z" }, + { url = "https://files.pythonhosted.org/packages/75/05/ac84063c5dae79722bda9f68b878dc31fc3059adb8633c79f1e82c2cd946/msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2", size = 413338, upload-time = "2025-06-13T06:51:57.023Z" }, + { url = "https://files.pythonhosted.org/packages/69/e8/fe86b082c781d3e1c09ca0f4dacd457ede60a13119b6ce939efe2ea77b76/msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2", size = 422658, upload-time = "2025-06-13T06:51:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2b/bafc9924df52d8f3bb7c00d24e57be477f4d0f967c0a31ef5e2225e035c7/msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752", size = 427124, upload-time = "2025-06-13T06:51:59.969Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3b/1f717e17e53e0ed0b68fa59e9188f3f610c79d7151f0e52ff3cd8eb6b2dc/msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295", size = 65016, upload-time = "2025-06-13T06:52:01.294Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/9d1780768d3b249accecc5a38c725eb1e203d44a191f7b7ff1941f7df60c/msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458", size = 72267, upload-time = "2025-06-13T06:52:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/389b9c593eda2b8551b2e7126ad3a06af6f9b44274eb3a4f054d48ff7e47/msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238", size = 82359, upload-time = "2025-06-13T06:52:03.909Z" }, + { url = "https://files.pythonhosted.org/packages/ab/65/7d1de38c8a22cf8b1551469159d4b6cf49be2126adc2482de50976084d78/msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157", size = 79172, upload-time = "2025-06-13T06:52:05.246Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bd/cacf208b64d9577a62c74b677e1ada005caa9b69a05a599889d6fc2ab20a/msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce", size = 425013, upload-time = "2025-06-13T06:52:06.341Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ec/fd869e2567cc9c01278a736cfd1697941ba0d4b81a43e0aa2e8d71dab208/msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a", size = 426905, upload-time = "2025-06-13T06:52:07.501Z" }, + { url = "https://files.pythonhosted.org/packages/55/2a/35860f33229075bce803a5593d046d8b489d7ba2fc85701e714fc1aaf898/msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c", size = 407336, upload-time = "2025-06-13T06:52:09.047Z" }, + { url = "https://files.pythonhosted.org/packages/8c/16/69ed8f3ada150bf92745fb4921bd621fd2cdf5a42e25eb50bcc57a5328f0/msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b", size = 409485, upload-time = "2025-06-13T06:52:10.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b6/0c398039e4c6d0b2e37c61d7e0e9d13439f91f780686deb8ee64ecf1ae71/msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef", size = 412182, upload-time = "2025-06-13T06:52:11.644Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/0cf4a6ecb9bc960d624c93effaeaae75cbf00b3bc4a54f35c8507273cda1/msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a", size = 419883, upload-time = "2025-06-13T06:52:12.806Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/9697c211720fa71a2dfb632cad6196a8af3abea56eece220fde4674dc44b/msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c", size = 65406, upload-time = "2025-06-13T06:52:14.271Z" }, + { url = "https://files.pythonhosted.org/packages/c0/23/0abb886e80eab08f5e8c485d6f13924028602829f63b8f5fa25a06636628/msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4", size = 72558, upload-time = "2025-06-13T06:52:15.252Z" }, + { url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677, upload-time = "2025-06-13T06:52:16.64Z" }, + { url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603, upload-time = "2025-06-13T06:52:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504, upload-time = "2025-06-13T06:52:18.982Z" }, + { url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a", size = 423749, upload-time = "2025-06-13T06:52:20.211Z" }, + { url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac", size = 404458, upload-time = "2025-06-13T06:52:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b", size = 405976, upload-time = "2025-06-13T06:52:22.995Z" }, + { url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7", size = 408607, upload-time = "2025-06-13T06:52:24.152Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172, upload-time = "2025-06-13T06:52:25.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323", size = 65347, upload-time = "2025-06-13T06:52:26.846Z" }, + { url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69", size = 72341, upload-time = "2025-06-13T06:52:27.835Z" }, +] + +[[package]] +name = "multidict" +version = "6.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/6b/86f353088c1358e76fd30b0146947fddecee812703b604ee901e85cd2a80/multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", size = 77054, upload-time = "2025-08-11T12:06:02.99Z" }, + { url = "https://files.pythonhosted.org/packages/19/5d/c01dc3d3788bb877bd7f5753ea6eb23c1beeca8044902a8f5bfb54430f63/multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", size = 44914, upload-time = "2025-08-11T12:06:05.264Z" }, + { url = "https://files.pythonhosted.org/packages/46/44/964dae19ea42f7d3e166474d8205f14bb811020e28bc423d46123ddda763/multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", size = 44601, upload-time = "2025-08-11T12:06:06.627Z" }, + { url = "https://files.pythonhosted.org/packages/31/20/0616348a1dfb36cb2ab33fc9521de1f27235a397bf3f59338e583afadd17/multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", size = 224821, upload-time = "2025-08-11T12:06:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/14/26/5d8923c69c110ff51861af05bd27ca6783011b96725d59ccae6d9daeb627/multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", size = 242608, upload-time = "2025-08-11T12:06:09.697Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/e2ad3ba9459aa34fa65cf1f82a5c4a820a2ce615aacfb5143b8817f76504/multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", size = 222324, upload-time = "2025-08-11T12:06:10.905Z" }, + { url = "https://files.pythonhosted.org/packages/19/db/4ed0f65701afbc2cb0c140d2d02928bb0fe38dd044af76e58ad7c54fd21f/multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", size = 253234, upload-time = "2025-08-11T12:06:12.658Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5160c9813269e39ae14b73debb907bfaaa1beee1762da8c4fb95df4764ed/multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", size = 251613, upload-time = "2025-08-11T12:06:13.97Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/48d1bd111fc2f8fb98b2ed7f9a115c55a9355358432a19f53c0b74d8425d/multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", size = 241649, upload-time = "2025-08-11T12:06:15.204Z" }, + { url = "https://files.pythonhosted.org/packages/85/2a/f7d743df0019408768af8a70d2037546a2be7b81fbb65f040d76caafd4c5/multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", size = 239238, upload-time = "2025-08-11T12:06:16.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b8/4f4bb13323c2d647323f7919201493cf48ebe7ded971717bfb0f1a79b6bf/multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", size = 233517, upload-time = "2025-08-11T12:06:18.107Z" }, + { url = "https://files.pythonhosted.org/packages/33/29/4293c26029ebfbba4f574febd2ed01b6f619cfa0d2e344217d53eef34192/multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", size = 243122, upload-time = "2025-08-11T12:06:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/20/60/a1c53628168aa22447bfde3a8730096ac28086704a0d8c590f3b63388d0c/multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", size = 248992, upload-time = "2025-08-11T12:06:20.661Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3b/55443a0c372f33cae5d9ec37a6a973802884fa0ab3586659b197cf8cc5e9/multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", size = 243708, upload-time = "2025-08-11T12:06:21.891Z" }, + { url = "https://files.pythonhosted.org/packages/7c/60/a18c6900086769312560b2626b18e8cca22d9e85b1186ba77f4755b11266/multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", size = 237498, upload-time = "2025-08-11T12:06:23.206Z" }, + { url = "https://files.pythonhosted.org/packages/11/3d/8bdd8bcaff2951ce2affccca107a404925a2beafedd5aef0b5e4a71120a6/multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", size = 41415, upload-time = "2025-08-11T12:06:24.77Z" }, + { url = "https://files.pythonhosted.org/packages/c0/53/cab1ad80356a4cd1b685a254b680167059b433b573e53872fab245e9fc95/multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", size = 46046, upload-time = "2025-08-11T12:06:25.893Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9a/874212b6f5c1c2d870d0a7adc5bb4cfe9b0624fa15cdf5cf757c0f5087ae/multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", size = 43147, upload-time = "2025-08-11T12:06:27.534Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7f/90a7f01e2d005d6653c689039977f6856718c75c5579445effb7e60923d1/multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", size = 76472, upload-time = "2025-08-11T12:06:29.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a3/bed07bc9e2bb302ce752f1dabc69e884cd6a676da44fb0e501b246031fdd/multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", size = 44634, upload-time = "2025-08-11T12:06:30.374Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4b/ceeb4f8f33cf81277da464307afeaf164fb0297947642585884f5cad4f28/multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", size = 44282, upload-time = "2025-08-11T12:06:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/436a5da8702b06866189b69f655ffdb8f70796252a8772a77815f1812679/multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", size = 229696, upload-time = "2025-08-11T12:06:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0e/915160be8fecf1fca35f790c08fb74ca684d752fcba62c11daaf3d92c216/multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", size = 246665, upload-time = "2025-08-11T12:06:34.448Z" }, + { url = "https://files.pythonhosted.org/packages/08/ee/2f464330acd83f77dcc346f0b1a0eaae10230291450887f96b204b8ac4d3/multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", size = 225485, upload-time = "2025-08-11T12:06:35.672Z" }, + { url = "https://files.pythonhosted.org/packages/71/cc/9a117f828b4d7fbaec6adeed2204f211e9caf0a012692a1ee32169f846ae/multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", size = 257318, upload-time = "2025-08-11T12:06:36.98Z" }, + { url = "https://files.pythonhosted.org/packages/25/77/62752d3dbd70e27fdd68e86626c1ae6bccfebe2bb1f84ae226363e112f5a/multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", size = 254689, upload-time = "2025-08-11T12:06:38.233Z" }, + { url = "https://files.pythonhosted.org/packages/00/6e/fac58b1072a6fc59af5e7acb245e8754d3e1f97f4f808a6559951f72a0d4/multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", size = 246709, upload-time = "2025-08-11T12:06:39.517Z" }, + { url = "https://files.pythonhosted.org/packages/01/ef/4698d6842ef5e797c6db7744b0081e36fb5de3d00002cc4c58071097fac3/multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", size = 243185, upload-time = "2025-08-11T12:06:40.796Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c9/d82e95ae1d6e4ef396934e9b0e942dfc428775f9554acf04393cce66b157/multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", size = 237838, upload-time = "2025-08-11T12:06:42.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/f94af5c36baaa75d44fab9f02e2a6bcfa0cd90acb44d4976a80960759dbc/multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", size = 246368, upload-time = "2025-08-11T12:06:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fe/29f23460c3d995f6a4b678cb2e9730e7277231b981f0b234702f0177818a/multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", size = 253339, upload-time = "2025-08-11T12:06:45.597Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/fd59449204426187b82bf8a75f629310f68c6adc9559dc922d5abe34797b/multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", size = 246933, upload-time = "2025-08-11T12:06:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/19/52/d5d6b344f176a5ac3606f7a61fb44dc746e04550e1a13834dff722b8d7d6/multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", size = 242225, upload-time = "2025-08-11T12:06:48.588Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d3/5b2281ed89ff4d5318d82478a2a2450fcdfc3300da48ff15c1778280ad26/multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", size = 41306, upload-time = "2025-08-11T12:06:49.95Z" }, + { url = "https://files.pythonhosted.org/packages/74/7d/36b045c23a1ab98507aefd44fd8b264ee1dd5e5010543c6fccf82141ccef/multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", size = 46029, upload-time = "2025-08-11T12:06:51.082Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5e/553d67d24432c5cd52b49047f2d248821843743ee6d29a704594f656d182/multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", size = 43017, upload-time = "2025-08-11T12:06:52.243Z" }, + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload-time = "2025-08-11T12:06:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload-time = "2025-08-11T12:06:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload-time = "2025-08-11T12:06:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload-time = "2025-08-11T12:06:57.213Z" }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload-time = "2025-08-11T12:07:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload-time = "2025-08-11T12:07:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload-time = "2025-08-11T12:07:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload-time = "2025-08-11T12:07:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload-time = "2025-08-11T12:07:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload-time = "2025-08-11T12:07:21.163Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload-time = "2025-08-11T12:07:22.392Z" }, + { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload-time = "2025-08-11T12:07:23.636Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload-time = "2025-08-11T12:07:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload-time = "2025-08-11T12:07:26.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload-time = "2025-08-11T12:07:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload-time = "2025-08-11T12:07:29.303Z" }, + { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload-time = "2025-08-11T12:07:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload-time = "2025-08-11T12:07:32.205Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload-time = "2025-08-11T12:07:33.623Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload-time = "2025-08-11T12:07:34.958Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload-time = "2025-08-11T12:07:36.588Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload-time = "2025-08-11T12:07:39.735Z" }, + { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload-time = "2025-08-11T12:07:41.525Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload-time = "2025-08-11T12:07:43.405Z" }, + { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload-time = "2025-08-11T12:07:45.082Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload-time = "2025-08-11T12:07:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload-time = "2025-08-11T12:07:48.402Z" }, + { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload-time = "2025-08-11T12:07:49.679Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload-time = "2025-08-11T12:07:51.318Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload-time = "2025-08-11T12:07:52.965Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload-time = "2025-08-11T12:07:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload-time = "2025-08-11T12:07:55.914Z" }, + { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload-time = "2025-08-11T12:07:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload-time = "2025-08-11T12:07:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload-time = "2025-08-11T12:08:01.037Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload-time = "2025-08-11T12:08:02.96Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload-time = "2025-08-11T12:08:04.746Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload-time = "2025-08-11T12:08:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload-time = "2025-08-11T12:08:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload-time = "2025-08-11T12:08:09.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload-time = "2025-08-11T12:08:10.981Z" }, + { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload-time = "2025-08-11T12:08:12.439Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload-time = "2025-08-11T12:08:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload-time = "2025-08-11T12:08:15.173Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, +] + +[[package]] +name = "multiprocess" +version = "0.70.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/ae/04f39c5d0d0def03247c2893d6f2b83c136bf3320a2154d7b8858f2ba72d/multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1", size = 1772603, upload-time = "2024-01-28T18:52:34.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/76/6e712a2623d146d314f17598df5de7224c85c0060ef63fd95cc15a25b3fa/multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee", size = 134980, upload-time = "2024-01-28T18:52:15.731Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec", size = 134982, upload-time = "2024-01-28T18:52:17.783Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02", size = 134824, upload-time = "2024-01-28T18:52:26.062Z" }, + { url = "https://files.pythonhosted.org/packages/50/15/b56e50e8debaf439f44befec5b2af11db85f6e0f344c3113ae0be0593a91/multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a", size = 143519, upload-time = "2024-01-28T18:52:28.115Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7d/a988f258104dcd2ccf1ed40fdc97e26c4ac351eeaf81d76e266c52d84e2f/multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e", size = 146741, upload-time = "2024-01-28T18:52:29.395Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/38df130f2c799090c978b366cfdf5b96d08de5b29a4a293df7f7429fa50b/multiprocess-0.70.16-py38-none-any.whl", hash = "sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435", size = 132628, upload-time = "2024-01-28T18:52:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351, upload-time = "2024-01-28T18:52:31.981Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.16.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload-time = "2025-01-28T09:29:14.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload-time = "2025-01-28T09:29:12.551Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, +] + +[[package]] +name = "nltk" +version = "3.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691, upload-time = "2024-08-18T19:48:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442, upload-time = "2024-08-18T19:48:21.909Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "numba" +version = "0.65.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz", hash = "sha256:19357146c32fe9ed25059ab915e8465fb13951cf6b0aace3826b76886373ab23", size = 2765600, upload-time = "2026-04-24T02:02:56.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1b/3c5a7daf683a95465bf23504bcd1a2d5db8cd5e5e276ca87505d020dffe9/numba-0.65.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9d993ed0a257aa4116e6f553f114004bcfdee540c7276ab8ea48f650d514c452", size = 2680870, upload-time = "2026-04-24T02:02:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/1831836814018a898e7d252aebe09c0f3ce1f26d145b68264b4ae0be6822/numba-0.65.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f098109f361681e57295f7e84d8ab2426902539a141811de0703ace52826981", size = 3739780, upload-time = "2026-04-24T02:02:13.097Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1b/a813ddc81def09e257d2b1f67521982ce4b06204a87268796ffc8187271c/numba-0.65.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973fd8173f2312815e6b7aaae887c4ce8a817eeff46a4f8840b828305b75bc95", size = 3446722, upload-time = "2026-04-24T02:02:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/ee1d8b3becda384fe0552221641e05aa668a35e8a77470db4db7f6475000/numba-0.65.1-cp310-cp310-win_amd64.whl", hash = "sha256:c63aa0c4193694026452da55d0ef9d85156c1a7a333454c103bb30dec81b7bf8", size = 2747539, upload-time = "2026-04-24T02:02:16.79Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/650500c2eab4534d98e9166f4298e0f3c69c742afdf24e6eabccd1f16ad8/numba-0.65.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:7020d74b19cdb8cff16506542fdd510756e28c5e7f3bd0b7f574f0f42272fcd9", size = 2680563, upload-time = "2026-04-24T02:02:18.414Z" }, + { url = "https://files.pythonhosted.org/packages/44/0b/0615dbedb98f5b32a35a53290fbdc6e22306968109278d7e58df82d7a9f6/numba-0.65.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f80ed83774b5173abd6581cd8d2165d1d38e13d2e5c8155c0c0b421784745420", size = 3745018, upload-time = "2026-04-24T02:02:20.252Z" }, + { url = "https://files.pythonhosted.org/packages/49/aa/4361698f35bf63bff67dfe6c90493731177f48ede954f77b0588731537bc/numba-0.65.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ed425a43b0a5f9772f2f4e2dd0bbd12eabecae1af0b24efcfd4e053f012aac6", size = 3450962, upload-time = "2026-04-24T02:02:22.449Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9a/af61ec03b3116c161fd7a06b9e8a265729a8718458333e8ffbb06d9a3978/numba-0.65.1-cp311-cp311-win_amd64.whl", hash = "sha256:df40a5028a975b9ea66f6a2a3f7abbdbd541a863070e34ed367aff21141248e4", size = 2747417, upload-time = "2026-04-24T02:02:24.43Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/76f8f8c5cf9adee47fdb7bbb03be8900f76f902d451d7477cf12b845e1de/numba-0.65.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ac3f1e77c352dd0ea9712732c2d8f9ca507717435eec5b5013bf138ac33c4a08", size = 2681371, upload-time = "2026-04-24T02:02:26.105Z" }, + { url = "https://files.pythonhosted.org/packages/69/47/a415af0283e4db0398104c6d1c11c9861a98dc67a7aa442a7769ed5d6196/numba-0.65.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:52bc6f3ceb8fcaff9b2ae26b4c6b1e9fee39db8d355534c0fe4f39a901246b84", size = 3802467, upload-time = "2026-04-24T02:02:27.712Z" }, + { url = "https://files.pythonhosted.org/packages/46/36/246f73ec99cfeab2f2cb2ce7d4218766cc36a2da418901223f4f4da9c813/numba-0.65.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ca10b3463bae0bd70589726fe3c77d01d6b5fc86bee54bcdf9fb6b47c28977", size = 3502628, upload-time = "2026-04-24T02:02:29.763Z" }, + { url = "https://files.pythonhosted.org/packages/db/9e/3c679b2ee078425b9e99a91e44f8d132a6830d8ccce5227bc5e9181aeed8/numba-0.65.1-cp312-cp312-win_amd64.whl", hash = "sha256:5971c632be2a2351500431f46213821dba8d02b18a9f7d02fd36bd2743e41a6a", size = 2750611, upload-time = "2026-04-24T02:02:31.477Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/14a4579049c1eb673afd0de0cb4842982acd55b9ce2643e763db858bcea0/numba-0.65.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1735c15c1134a5108b4d6a5c77fc0947924ea066a738dc09a52008c13df9cad3", size = 2681344, upload-time = "2026-04-24T02:02:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/a0/22/b8d873f6466b20aa563fc9b33acd48dec89a07803ddaa2f1c8ca1cd33126/numba-0.65.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c09f49117ef255e1f1c6dad0c7a1ed39868243862a73be5706793241a3755f1b", size = 3810619, upload-time = "2026-04-24T02:02:36.041Z" }, + { url = "https://files.pythonhosted.org/packages/62/08/e16a8b5d9a018962ebb5c66be662317cde32b9f5dab08441f90bed5522fb/numba-0.65.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:594a8680b3fadac99e97e489b1fd89007177e5336713745c3b769528c635a464", size = 3509783, upload-time = "2026-04-24T02:02:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/03c970d57f4c1741354837353ce39fb5206952ae1dba8922d29c86f64805/numba-0.65.1-cp313-cp313-win_amd64.whl", hash = "sha256:85be74c0d036842699a30058f82fb88fc5ffdc59f7615cab5792ea92914c9b62", size = 2750534, upload-time = "2026-04-24T02:02:39.903Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2e/8aed9b726d9ba5f11ad287645fd479e88278db3060a25cb1225d730eb2b7/numba-0.65.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:33f5eb68eb1c843511615d14663ce60258525d6a4c65ab040e2c2b0c4cf17450", size = 2681554, upload-time = "2026-04-24T02:02:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/87/96/f3eb235fafa82a34e2ab5dd7dc9ffff998ebf5f0bbc23fa56a96aeb44da6/numba-0.65.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71e73029bf53a62cc6afcf96be4bd942290d8b4c55f0a454fb536158115790f7", size = 3779602, upload-time = "2026-04-24T02:02:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/09/90/b0f09b48752d23640b8284f22aa597737e8adaddc7fbfacc4708b7f73a4c/numba-0.65.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a07635e0be926b9bdbffb09137c230fb13f6ec0e564914ba937cee12ce3eb35", size = 3479532, upload-time = "2026-04-24T02:02:45.427Z" }, + { url = "https://files.pythonhosted.org/packages/56/46/3f7fc04fb853559e74b210e0b62c19974ec844cefec611f9e535f4da3761/numba-0.65.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a20fcdabdefbdacf88d85caf70c3b18c4bcb7ebb8f82e6a19486383dd26ab63", size = 2752637, upload-time = "2026-04-24T02:02:47.664Z" }, + { url = "https://files.pythonhosted.org/packages/81/7b/c1a341a9067367778f4152a5f01061cf281fb09582c92c510ec4918cabf6/numba-0.65.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:548dd4b3a4508d5062768d1514b2cd7b015f9a25ec7af651c50dee243965e652", size = 2684600, upload-time = "2026-04-24T02:02:49.653Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/98ddbcf3e4f04a6dd07e1c67249955920579ba4af6bb6868e3088f4ed282/numba-0.65.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:78abc28feff2c2ff8307fff3975b6438352759c9acb797ecd6b1fb6e7e39e31d", size = 3817198, upload-time = "2026-04-24T02:02:51.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/0dad21057ece5a835599f5d24099b091703995e23dbbf894f259e91c010b/numba-0.65.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7676cb389555805f9b9a1840cbcd1ea6c8bd5376ab6918e3a29c5ea1dbda20", size = 3533862, upload-time = "2026-04-24T02:02:52.987Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/8be7118ffd4c8440881046eac3d0982cc5ab42909508cf5d67024d62a2e4/numba-0.65.1-cp314-cp314t-win_amd64.whl", hash = "sha256:20609346e3bd75204950dcbbfe383a8d7dbf4902f442aedbf00f97fef4aa8f38", size = 2758237, upload-time = "2026-04-24T02:02:54.612Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cutlass-dsl-libs-base" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/15/575d7df4fe2f3406f1cfc68be72aeff2834f8a696daf1cd5bee8017e4507/nvidia_cutlass_dsl-4.5.2-py3-none-any.whl", hash = "sha256:68ed1b63ca74aae87955012da9dfd7fdaae471329d0028b229b841c7192ccf52", size = 10179, upload-time = "2026-05-25T03:38:56.364Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl-libs-base" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-python" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/3e/2cca8745885aaba0d835a8be29e516e56930791c01f0806da95d3017a495/nvidia_cutlass_dsl_libs_base-4.5.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b62807bc5ea13bbdef648212893fac407ed943f940cece56b880d44af243e075", size = 75635922, upload-time = "2026-05-25T03:46:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/2b/4de80442d33791322aa496e2a7f47ed08a42578bd1c7031ef0602009f8ad/nvidia_cutlass_dsl_libs_base-4.5.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:386e832427e3670479049a1560e4d8d2e565d8c0f37a6852c6d7043d046548f1", size = 74512458, upload-time = "2026-05-25T03:49:47.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/0cca1d11787128c66c0774374d1bb09313352eee11560dd00f36d6d62f36/nvidia_cutlass_dsl_libs_base-4.5.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:cbb555a95c7011e4b3ca328be407299c77d289660adbea22ed515d4406e6949c", size = 75637009, upload-time = "2026-05-25T03:48:37.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e0/78eded54b4478ec01a91c75f1b9bc6dc73a2ec205c4fa2fdc25a456f4089/nvidia_cutlass_dsl_libs_base-4.5.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:9117900cba53d3c21a8dacba6bbf3d6e5f269e427a526c320fb44707a0d57363", size = 74511501, upload-time = "2026-05-25T03:52:03.798Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ef/e827e3c67d72adbf4e8f680bdf03b1b67723d9e1ae7c3d0a1751f39f69ce/nvidia_cutlass_dsl_libs_base-4.5.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d2a3c412287e356fbe48fe9f845d6d33cd35dea5e20d7e4f628c20957967cacd", size = 75643473, upload-time = "2026-05-25T03:49:15.857Z" }, + { url = "https://files.pythonhosted.org/packages/97/68/c1247ab848f26c4ab56e562eea0e3f31fc14c9aaf0d883afaa92d8f05592/nvidia_cutlass_dsl_libs_base-4.5.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:15ef6a59193667e663934ef4873f8ccad37455e9b7c3c419c3072113b8aedf61", size = 74513226, upload-time = "2026-05-25T03:51:32.496Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f8/b192015e273ff023a35741d6d5e4a93e4819160dee3955fc5d3d53534450/nvidia_cutlass_dsl_libs_base-4.5.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:395bd77cf642aeef311313453e6582f11c9357a4b81fe620ea3daccd1fccab9b", size = 75645002, upload-time = "2026-05-25T03:48:01.887Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/bfe256ac08e5a6dfb11444809e54c76c3a2f05fff38dd173e2e71b95e4d2/nvidia_cutlass_dsl_libs_base-4.5.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e59da7d89e5e4f8514c6530843f910f9d8734d8042dcaa079c9d9c5063eb3514", size = 74514312, upload-time = "2026-05-25T03:50:56.343Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b2/7a5de500bb74915ab8b3875f4952ae07d562f33d06eef9b2569adf4c09ab/nvidia_cutlass_dsl_libs_base-4.5.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:216eee6aa8107d35569f9451b66b03a3c53167841d1af9b630b966ef8d966e19", size = 75636795, upload-time = "2026-05-25T03:47:31.081Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bc/5f9dd8c05c3e2f435228224f0b0e76e324c1bf0a6dcd3cfb917b5e94bad7/nvidia_cutlass_dsl_libs_base-4.5.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:12c29f7c1f1f82851092ba3869264dafafb035228c0d9827a8db08b884fb80ca", size = 74511193, upload-time = "2026-05-25T03:52:39.444Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/76a9d1ce5ade3f43ab6f10e361a9c1962d02177deeaf46f2c3684a7ae959/nvidia_cutlass_dsl_libs_base-4.5.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:5aca392063ffbc7da30442a267928b22d4a2d37f9ea1db32e4487aa31b0fcc33", size = 75644393, upload-time = "2026-05-25T03:47:02.706Z" }, + { url = "https://files.pythonhosted.org/packages/15/84/08d695d2e0fa95891a2e5abd978f359d50125e4d1f056e54697d465fccc3/nvidia_cutlass_dsl_libs_base-4.5.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:abab8a0d2f3f5661533c366df78f973052b86a3b52b868d997a95dce5aa8f17b", size = 74514399, upload-time = "2026-05-25T03:50:20.841Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "ollama" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/62/a36be4555e4218d6c8b35e72e0dfe0823845400097275cd81c9aec4ddf39/ollama-0.5.4.tar.gz", hash = "sha256:75857505a5d42e5e58114a1b78cc8c24596d8866863359d8a2329946a9b6d6f3", size = 45233, upload-time = "2025-09-16T00:25:25.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/af/d0a23c8fdec4c8ddb771191d9b36a57fbce6741835a78f1b18ab6d15ae7d/ollama-0.5.4-py3-none-any.whl", hash = "sha256:6374c9bb4f2a371b3583c09786112ba85b006516745689c172a7e28af4d4d1a2", size = 13548, upload-time = "2025-09-16T00:25:24.186Z" }, +] + +[[package]] +name = "openai" +version = "1.109.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/9f/d11cc7fb2d60af14a97dbef4e3c7b23917387995c257951fdc321d8efd0a/openai-1.109.0.tar.gz", hash = "sha256:701e26d13e3953524ba99f44cf5fbbda40eafd41ba15a8d85b76229a2693cfe5", size = 563971, upload-time = "2025-09-23T16:59:41.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/d3/d65e318e8d3b5845afaa5960d8da779988b4cb9f4e1ca82e588fed9c6a9d/openai-1.109.0-py3-none-any.whl", hash = "sha256:8c0910bdd4ee1274d5ff0354786bdd0bc79e68c158d5d2c19e24208b412e5792", size = 948421, upload-time = "2025-09-23T16:59:39.516Z" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827, upload-time = "2024-09-20T13:08:42.347Z" }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897, upload-time = "2024-09-20T13:08:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908, upload-time = "2024-09-20T18:37:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210, upload-time = "2024-09-20T13:08:48.325Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292, upload-time = "2024-09-20T19:01:54.443Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379, upload-time = "2024-09-20T13:08:50.882Z" }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471, upload-time = "2024-09-20T13:08:53.332Z" }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pdfminer-six" +version = "20250506" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/46/5223d613ac4963e1f7c07b2660fe0e9e770102ec6bda8c038400113fb215/pdfminer_six-20250506.tar.gz", hash = "sha256:b03cc8df09cf3c7aba8246deae52e0bca7ebb112a38895b5e1d4f5dd2b8ca2e7", size = 7387678, upload-time = "2025-05-06T16:17:00.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/16/7a432c0101fa87457e75cb12c879e1749c5870a786525e2e0f42871d6462/pdfminer_six-20250506-py3-none-any.whl", hash = "sha256:d81ad173f62e5f841b53a8ba63af1a4a355933cfc0ffabd608e568b9193909e3", size = 5620187, upload-time = "2025-05-06T16:16:58.669Z" }, +] + +[[package]] +name = "pdfplumber" +version = "0.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pdfminer-six" }, + { name = "pillow" }, + { name = "pypdfium2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/0d/4135821aa7b1a0b77a29fac881ef0890b46b0b002290d04915ed7acc0043/pdfplumber-0.11.7.tar.gz", hash = "sha256:fa67773e5e599de1624255e9b75d1409297c5e1d7493b386ce63648637c67368", size = 115518, upload-time = "2025-06-12T11:30:49.864Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/e0/52b67d4f00e09e497aec4f71bc44d395605e8ebcea52543242ed34c25ef9/pdfplumber-0.11.7-py3-none-any.whl", hash = "sha256:edd2195cca68bd770da479cf528a737e362968ec2351e62a6c0b71ff612ac25e", size = 60029, upload-time = "2025-06-12T11:30:48.89Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", size = 73178, upload-time = "2025-06-09T22:53:40.126Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", size = 43133, upload-time = "2025-06-09T22:53:41.965Z" }, + { url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", size = 43039, upload-time = "2025-06-09T22:53:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", size = 201903, upload-time = "2025-06-09T22:53:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", size = 213362, upload-time = "2025-06-09T22:53:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", size = 210525, upload-time = "2025-06-09T22:53:48.547Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", size = 198283, upload-time = "2025-06-09T22:53:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", size = 191872, upload-time = "2025-06-09T22:53:51.438Z" }, + { url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", size = 199452, upload-time = "2025-06-09T22:53:53.229Z" }, + { url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", size = 191567, upload-time = "2025-06-09T22:53:54.541Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", size = 193015, upload-time = "2025-06-09T22:53:56.44Z" }, + { url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", size = 204660, upload-time = "2025-06-09T22:53:57.839Z" }, + { url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", size = 206105, upload-time = "2025-06-09T22:53:59.638Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", size = 196980, upload-time = "2025-06-09T22:54:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", size = 37679, upload-time = "2025-06-09T22:54:03.003Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", size = 41459, upload-time = "2025-06-09T22:54:04.134Z" }, + { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288, upload-time = "2025-06-09T22:54:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456, upload-time = "2025-06-09T22:54:11.828Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429, upload-time = "2025-06-09T22:54:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472, upload-time = "2025-06-09T22:54:15.232Z" }, + { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480, upload-time = "2025-06-09T22:54:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530, upload-time = "2025-06-09T22:54:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230, upload-time = "2025-06-09T22:54:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754, upload-time = "2025-06-09T22:54:21.716Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430, upload-time = "2025-06-09T22:54:23.17Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884, upload-time = "2025-06-09T22:54:25.539Z" }, + { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480, upload-time = "2025-06-09T22:54:26.892Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757, upload-time = "2025-06-09T22:54:28.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500, upload-time = "2025-06-09T22:54:29.4Z" }, + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +] + +[[package]] +name = "protobuf" +version = "4.25.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/d8/65adb47d921ce828ba319d6587aa8758da022de509c3862a70177a958844/protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c", size = 380274, upload-time = "2024-02-15T23:44:39.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/c7/0d3bf9f6700b8ce36968899aaca4fa82d8dc6ee981f248565bec8cf3719f/protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa", size = 392414, upload-time = "2024-02-15T23:44:15.871Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/1bed3b7c904cc178cb8ee8dbaf72934964452b3de95b7a63412591edb93c/protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8", size = 413401, upload-time = "2024-02-15T23:44:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bf/26deba06a4c910a85f78245cac7698f67cedd7efe00d04f6b3e1b3506a59/protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c", size = 394162, upload-time = "2024-02-15T23:44:21.363Z" }, + { url = "https://files.pythonhosted.org/packages/d8/82/aefe901174b5a618daee511ddd00342193c1b545e3cd6a2cd6df9ba452b5/protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019", size = 293701, upload-time = "2024-02-15T23:44:23.59Z" }, + { url = "https://files.pythonhosted.org/packages/15/db/7f731524fe0e56c6b2eb57d05b55d3badd80ef7d1f1ed59db191b2fdd8ab/protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d", size = 294613, upload-time = "2024-02-15T23:44:25.132Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d5/db585a5e8d64af6b384c7b3a63da13df2ff86933e486ba78431736c67c25/protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9", size = 156466, upload-time = "2024-02-15T23:44:36.771Z" }, +] + +[[package]] +name = "psutil" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/31/4723d756b59344b643542936e37a31d1d3204bcdc42a7daa8ee9eb06fb50/psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2", size = 497660, upload-time = "2025-09-17T20:14:52.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/62/ce4051019ee20ce0ed74432dd73a5bb087a6704284a470bb8adff69a0932/psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13", size = 245242, upload-time = "2025-09-17T20:14:56.126Z" }, + { url = "https://files.pythonhosted.org/packages/38/61/f76959fba841bf5b61123fbf4b650886dc4094c6858008b5bf73d9057216/psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5", size = 246682, upload-time = "2025-09-17T20:14:58.25Z" }, + { url = "https://files.pythonhosted.org/packages/88/7a/37c99d2e77ec30d63398ffa6a660450b8a62517cabe44b3e9bae97696e8d/psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3", size = 287994, upload-time = "2025-09-17T20:14:59.901Z" }, + { url = "https://files.pythonhosted.org/packages/9d/de/04c8c61232f7244aa0a4b9a9fbd63a89d5aeaf94b2fc9d1d16e2faa5cbb0/psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3", size = 291163, upload-time = "2025-09-17T20:15:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/58/c4f976234bf6d4737bc8c02a81192f045c307b72cf39c9e5c5a2d78927f6/psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d", size = 293625, upload-time = "2025-09-17T20:15:04.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/157c8e7959ec39ced1b11cc93c730c4fb7f9d408569a6c59dbd92ceb35db/psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca", size = 244812, upload-time = "2025-09-17T20:15:07.462Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/b44c4f697276a7a95b8e94d0e320a7bf7f3318521b23de69035540b39838/psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d", size = 247965, upload-time = "2025-09-17T20:15:09.673Z" }, + { url = "https://files.pythonhosted.org/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07", size = 244971, upload-time = "2025-09-17T20:15:12.262Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pyarrow" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837, upload-time = "2025-07-18T00:54:34.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470, upload-time = "2025-07-18T00:54:38.329Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619, upload-time = "2025-07-18T00:54:42.172Z" }, + { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488, upload-time = "2025-07-18T00:54:47.132Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159, upload-time = "2025-07-18T00:54:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567, upload-time = "2025-07-18T00:54:56.679Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959, upload-time = "2025-07-18T00:55:00.482Z" }, + { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234, upload-time = "2025-07-18T00:55:03.812Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370, upload-time = "2025-07-18T00:55:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424, upload-time = "2025-07-18T00:55:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810, upload-time = "2025-07-18T00:55:16.301Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538, upload-time = "2025-07-18T00:55:23.82Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056, upload-time = "2025-07-18T00:55:28.231Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568, upload-time = "2025-07-18T00:55:32.122Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload-time = "2025-07-18T00:55:35.373Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload-time = "2025-07-18T00:55:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload-time = "2025-07-18T00:55:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload-time = "2025-07-18T00:55:47.069Z" }, + { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload-time = "2025-07-18T00:55:53.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload-time = "2025-07-18T00:55:57.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload-time = "2025-07-18T00:56:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306, upload-time = "2025-07-18T00:56:04.42Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622, upload-time = "2025-07-18T00:56:07.505Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094, upload-time = "2025-07-18T00:56:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576, upload-time = "2025-07-18T00:56:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342, upload-time = "2025-07-18T00:56:19.531Z" }, + { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218, upload-time = "2025-07-18T00:56:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551, upload-time = "2025-07-18T00:56:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064, upload-time = "2025-07-18T00:56:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837, upload-time = "2025-07-18T00:56:33.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158, upload-time = "2025-07-18T00:56:37.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885, upload-time = "2025-07-18T00:56:41.483Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625, upload-time = "2025-07-18T00:56:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890, upload-time = "2025-07-18T00:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload-time = "2025-07-18T00:56:56.379Z" }, +] + +[[package]] +name = "pybind11" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/7b/a6d8dcb83c457e24a9df1e4d8fd5fb8034d4bbc62f3c324681e8a9ba57c2/pybind11-3.0.1.tar.gz", hash = "sha256:9c0f40056a016da59bab516efb523089139fcc6f2ba7e4930854c61efb932051", size = 546914, upload-time = "2025-08-22T20:09:27.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/8a/37362fc2b949d5f733a8b0f2ff51ba423914cabefe69f1d1b6aab710f5fe/pybind11-3.0.1-py3-none-any.whl", hash = "sha256:aa8f0aa6e0a94d3b64adfc38f560f33f15e589be2175e103c0a33c6bce55ee89", size = 293611, upload-time = "2025-08-22T20:09:25.235Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymupdf" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/35/031556dfc0d332d8e9ed9b61ca105138606d3f8971b9eb02e20118629334/pymupdf-1.26.4.tar.gz", hash = "sha256:be13a066d42bfaed343a488168656637c4d9843ddc63b768dc827c9dfc6b9989", size = 83077563, upload-time = "2025-08-25T14:20:29.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/ae/3be722886cc7be2093585cd94f466db1199133ab005645a7a567b249560f/pymupdf-1.26.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cb95562a0a63ce906fd788bdad5239063b63068cf4a991684f43acb09052cb99", size = 23061974, upload-time = "2025-08-25T14:16:58.811Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b0/9a451d837e1fe18ecdbfbc34a6499f153c8a008763229cc634725383a93f/pymupdf-1.26.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:67e9e6b45832c33726651c2a031e9a20108fd9e759140b9e843f934de813a7ff", size = 22410112, upload-time = "2025-08-25T14:17:24.511Z" }, + { url = "https://files.pythonhosted.org/packages/d8/13/0916e8e02cb5453161fb9d9167c747d0a20d58633e30728645374153f815/pymupdf-1.26.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2604f687dd02b6a1b98c81bd8becfc0024899a2d2085adfe3f9e91607721fd22", size = 23454948, upload-time = "2025-08-25T21:20:07.71Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/d3cfafc75d383603884edeabe4821a549345df954a88d79e6764e2c87601/pymupdf-1.26.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:973a6dda61ebd34040e4df3753bf004b669017663fbbfdaa294d44eceba98de0", size = 24060686, upload-time = "2025-08-25T14:17:56.536Z" }, + { url = "https://files.pythonhosted.org/packages/72/08/035e9d22c801e801bba50c6745bc90ba8696a042fe2c68793e28bf0c3b07/pymupdf-1.26.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:299a49797df5b558e695647fa791329ba3911cbbb31ed65f24a6266c118ef1a7", size = 24265046, upload-time = "2025-08-25T14:18:21.238Z" }, + { url = "https://files.pythonhosted.org/packages/28/8c/c201e4846ec0fb6ae5d52aa3a5d66f9355f0c69fb94230265714df0de65e/pymupdf-1.26.4-cp39-abi3-win32.whl", hash = "sha256:51b38379aad8c71bd7a8dd24d93fbe7580c2a5d9d7e1f9cd29ebbba315aa1bd1", size = 17127332, upload-time = "2025-08-25T14:18:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c4/87d27b108c2f6d773aa5183c5ae367b2a99296ea4bc16eb79f453c679e30/pymupdf-1.26.4-cp39-abi3-win_amd64.whl", hash = "sha256:0b6345a93a9afd28de2567e433055e873205c52e6b920b129ca50e836a3aeec6", size = 18743491, upload-time = "2025-08-25T14:19:01.104Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "pypdf" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/ac/44d86f16b8ad9b42ea1da4b9aa145be71c89927566d9be87fe74bda1dfef/pypdf-6.1.0.tar.gz", hash = "sha256:0cba440d024da5a2a9304f03cd645346052827b84c5a461c6123e24ed5a3b0b9", size = 5072609, upload-time = "2025-09-21T13:38:39.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/f3/4939b609cfd374e495450b22a0385ee3f531e9aa40e8812e5c405f030c54/pypdf-6.1.0-py3-none-any.whl", hash = "sha256:6b34e4147df20978bf270af19826692e0485431a9d3944617b9533bc77efb695", size = 322468, upload-time = "2025-09-21T13:38:37.467Z" }, +] + +[[package]] +name = "pypdf2" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/bb/18dc3062d37db6c491392007dfd1a7f524bb95886eb956569ac38a23a784/PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440", size = 227419, upload-time = "2022-12-31T10:36:13.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572, upload-time = "2022-12-31T10:36:10.327Z" }, +] + +[[package]] +name = "pypdfium2" +version = "4.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/14/838b3ba247a0ba92e4df5d23f2bea9478edcfd72b78a39d6ca36ccd84ad2/pypdfium2-4.30.0.tar.gz", hash = "sha256:48b5b7e5566665bc1015b9d69c1ebabe21f6aee468b509531c3c8318eeee2e16", size = 140239, upload-time = "2024-05-09T18:33:17.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/9a/c8ff5cc352c1b60b0b97642ae734f51edbab6e28b45b4fcdfe5306ee3c83/pypdfium2-4.30.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33ceded0b6ff5b2b93bc1fe0ad4b71aa6b7e7bd5875f1ca0cdfb6ba6ac01aab", size = 2837254, upload-time = "2024-05-09T18:32:48.653Z" }, + { url = "https://files.pythonhosted.org/packages/21/8b/27d4d5409f3c76b985f4ee4afe147b606594411e15ac4dc1c3363c9a9810/pypdfium2-4.30.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e55689f4b06e2d2406203e771f78789bd4f190731b5d57383d05cf611d829de", size = 2707624, upload-time = "2024-05-09T18:32:51.458Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/28a73ca17c24b41a205d658e177d68e198d7dde65a8c99c821d231b6ee3d/pypdfium2-4.30.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6e50f5ce7f65a40a33d7c9edc39f23140c57e37144c2d6d9e9262a2a854854", size = 2793126, upload-time = "2024-05-09T18:32:53.581Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/53b3ebf0955edbd02ac6da16a818ecc65c939e98fdeb4e0958362bd385c8/pypdfium2-4.30.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d0dd3ecaffd0b6dbda3da663220e705cb563918249bda26058c6036752ba3a2", size = 2591077, upload-time = "2024-05-09T18:32:55.99Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/0394e56e7cab8b5b21f744d988400948ef71a9a892cbeb0b200d324ab2c7/pypdfium2-4.30.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc3bf29b0db8c76cdfaac1ec1cde8edf211a7de7390fbf8934ad2aa9b4d6dfad", size = 2864431, upload-time = "2024-05-09T18:32:57.911Z" }, + { url = "https://files.pythonhosted.org/packages/65/cd/3f1edf20a0ef4a212a5e20a5900e64942c5a374473671ac0780eaa08ea80/pypdfium2-4.30.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1f78d2189e0ddf9ac2b7a9b9bd4f0c66f54d1389ff6c17e9fd9dc034d06eb3f", size = 2812008, upload-time = "2024-05-09T18:32:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/c8/91/2d517db61845698f41a2a974de90762e50faeb529201c6b3574935969045/pypdfium2-4.30.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5eda3641a2da7a7a0b2f4dbd71d706401a656fea521b6b6faa0675b15d31a163", size = 6181543, upload-time = "2024-05-09T18:33:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c4/ed1315143a7a84b2c7616569dfb472473968d628f17c231c39e29ae9d780/pypdfium2-4.30.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e", size = 6175911, upload-time = "2024-05-09T18:33:05.376Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c4/9e62d03f414e0e3051c56d5943c3bf42aa9608ede4e19dc96438364e9e03/pypdfium2-4.30.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f33bd79e7a09d5f7acca3b0b69ff6c8a488869a7fab48fdf400fec6e20b9c8be", size = 6267430, upload-time = "2024-05-09T18:33:08.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/47/eda4904f715fb98561e34012826e883816945934a851745570521ec89520/pypdfium2-4.30.0-py3-none-win32.whl", hash = "sha256:ee2410f15d576d976c2ab2558c93d392a25fb9f6635e8dd0a8a3a5241b275e0e", size = 2775951, upload-time = "2024-05-09T18:33:10.567Z" }, + { url = "https://files.pythonhosted.org/packages/25/bd/56d9ec6b9f0fc4e0d95288759f3179f0fcd34b1a1526b75673d2f6d5196f/pypdfium2-4.30.0-py3-none-win_amd64.whl", hash = "sha256:90dbb2ac07be53219f56be09961eb95cf2473f834d01a42d901d13ccfad64b4c", size = 2892098, upload-time = "2024-05-09T18:33:13.107Z" }, + { url = "https://files.pythonhosted.org/packages/be/7a/097801205b991bc3115e8af1edb850d30aeaf0118520b016354cf5ccd3f6/pypdfium2-4.30.0-py3-none-win_arm64.whl", hash = "sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29", size = 2752118, upload-time = "2024-05-09T18:33:15.489Z" }, +] + +[[package]] +name = "pyrsistent" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/3a/5031723c09068e9c8c2f0bc25c3a9245f2b1d1aea8396c787a408f2b95ca/pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", size = 103642, upload-time = "2023-10-25T21:06:56.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/19/c343b14061907b629b765444b6436b160e2bd4184d17d4804bbe6381f6be/pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", size = 83416, upload-time = "2023-10-25T21:06:04.579Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/8342079ea331031ef9ed57edd312a9ad283bcc8adfaf268931ae356a09a6/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", size = 118021, upload-time = "2023-10-25T21:06:06.953Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b7/64a125c488243965b7c5118352e47c6f89df95b4ac306d31cee409153d57/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", size = 117747, upload-time = "2023-10-25T21:06:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/43c67bd5f80df9e7583042398d12113263ec57f27c0607abe9d78395d18f/pyrsistent-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", size = 114524, upload-time = "2023-10-25T21:06:10.728Z" }, + { url = "https://files.pythonhosted.org/packages/8a/98/b382a87e89ca839106d874f7bf78d226b3eedb26735eb6f751f1a3375f21/pyrsistent-0.20.0-cp310-cp310-win32.whl", hash = "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", size = 60780, upload-time = "2023-10-25T21:06:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/37/8a/23e2193f7adea6901262e3cf39c7fe18ac0c446176c0ff0e19aeb2e9681e/pyrsistent-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", size = 63310, upload-time = "2023-10-25T21:06:13.598Z" }, + { url = "https://files.pythonhosted.org/packages/df/63/7544dc7d0953294882a5c587fb1b10a26e0c23d9b92281a14c2514bac1f7/pyrsistent-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", size = 83481, upload-time = "2023-10-25T21:06:15.238Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a0/49249bc14d71b1bf2ffe89703acfa86f2017c25cfdabcaea532b8c8a5810/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", size = 120222, upload-time = "2023-10-25T21:06:17.144Z" }, + { url = "https://files.pythonhosted.org/packages/a1/94/9808e8c9271424120289b9028a657da336ad7e43da0647f62e4f6011d19b/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", size = 120002, upload-time = "2023-10-25T21:06:18.727Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f6/9ecfb78b2fc8e2540546db0fe19df1fae0f56664a5958c21ff8861b0f8da/pyrsistent-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", size = 116850, upload-time = "2023-10-25T21:06:20.424Z" }, + { url = "https://files.pythonhosted.org/packages/83/c8/e6d28bc27a0719f8eaae660357df9757d6e9ca9be2691595721de9e8adfc/pyrsistent-0.20.0-cp311-cp311-win32.whl", hash = "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", size = 60775, upload-time = "2023-10-25T21:06:21.815Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/c6ef52ff30388f357922d08de012abdd3dc61e09311d88967bdae23ab657/pyrsistent-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", size = 63306, upload-time = "2023-10-25T21:06:22.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/ff2ed52032ac1ce2e7ba19e79bd5b05d152ebfb77956cf08fcd6e8d760ea/pyrsistent-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", size = 83537, upload-time = "2023-10-25T21:06:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/338d0050b24c3132bcfc79b68c3a5f54bce3d213ecef74d37e988b971d8a/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", size = 122615, upload-time = "2023-10-25T21:06:25.815Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/e56d6431b713518094fae6ff833a04a6f49ad0fbe25fb7c0dc7408e19d20/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", size = 122335, upload-time = "2023-10-25T21:06:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/4a/bb/5f40a4d5e985a43b43f607250e766cdec28904682c3505eb0bd343a4b7db/pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", size = 118510, upload-time = "2023-10-25T21:06:30.718Z" }, + { url = "https://files.pythonhosted.org/packages/1c/13/e6a22f40f5800af116c02c28e29f15c06aa41cb2036f6a64ab124647f28b/pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", size = 60865, upload-time = "2023-10-25T21:06:32.742Z" }, + { url = "https://files.pythonhosted.org/packages/75/ef/2fa3b55023ec07c22682c957808f9a41836da4cd006b5f55ec76bf0fbfa6/pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", size = 63239, upload-time = "2023-10-25T21:06:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/23/88/0acd180010aaed4987c85700b7cc17f9505f3edb4e5873e4dc67f613e338/pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", size = 58106, upload-time = "2023-10-25T21:06:54.387Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-docx" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "regex" +version = "2025.9.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/d3/eaa0d28aba6ad1827ad1e716d9a93e1ba963ada61887498297d3da715133/regex-2025.9.18.tar.gz", hash = "sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4", size = 400917, upload-time = "2025-09-19T00:38:35.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d8/7e06171db8e55f917c5b8e89319cea2d86982e3fc46b677f40358223dece/regex-2025.9.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:12296202480c201c98a84aecc4d210592b2f55e200a1d193235c4db92b9f6788", size = 484829, upload-time = "2025-09-19T00:35:05.215Z" }, + { url = "https://files.pythonhosted.org/packages/8d/70/bf91bb39e5bedf75ce730ffbaa82ca585584d13335306d637458946b8b9f/regex-2025.9.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:220381f1464a581f2ea988f2220cf2a67927adcef107d47d6897ba5a2f6d51a4", size = 288993, upload-time = "2025-09-19T00:35:08.154Z" }, + { url = "https://files.pythonhosted.org/packages/fe/89/69f79b28365eda2c46e64c39d617d5f65a2aa451a4c94de7d9b34c2dc80f/regex-2025.9.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87f681bfca84ebd265278b5daa1dcb57f4db315da3b5d044add7c30c10442e61", size = 286624, upload-time = "2025-09-19T00:35:09.717Z" }, + { url = "https://files.pythonhosted.org/packages/44/31/81e62955726c3a14fcc1049a80bc716765af6c055706869de5e880ddc783/regex-2025.9.18-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34d674cbba70c9398074c8a1fcc1a79739d65d1105de2a3c695e2b05ea728251", size = 780473, upload-time = "2025-09-19T00:35:11.013Z" }, + { url = "https://files.pythonhosted.org/packages/fb/23/07072b7e191fbb6e213dc03b2f5b96f06d3c12d7deaded84679482926fc7/regex-2025.9.18-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:385c9b769655cb65ea40b6eea6ff763cbb6d69b3ffef0b0db8208e1833d4e746", size = 849290, upload-time = "2025-09-19T00:35:12.348Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f0/aec7f6a01f2a112210424d77c6401b9015675fb887ced7e18926df4ae51e/regex-2025.9.18-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8900b3208e022570ae34328712bef6696de0804c122933414014bae791437ab2", size = 897335, upload-time = "2025-09-19T00:35:14.058Z" }, + { url = "https://files.pythonhosted.org/packages/cc/90/2e5f9da89d260de7d0417ead91a1bc897f19f0af05f4f9323313b76c47f2/regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c204e93bf32cd7a77151d44b05eb36f469d0898e3fba141c026a26b79d9914a0", size = 789946, upload-time = "2025-09-19T00:35:15.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d5/1c712c7362f2563d389be66bae131c8bab121a3fabfa04b0b5bfc9e73c51/regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3acc471d1dd7e5ff82e6cacb3b286750decd949ecd4ae258696d04f019817ef8", size = 780787, upload-time = "2025-09-19T00:35:17.061Z" }, + { url = "https://files.pythonhosted.org/packages/4f/92/c54cdb4aa41009632e69817a5aa452673507f07e341076735a2f6c46a37c/regex-2025.9.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6479d5555122433728760e5f29edb4c2b79655a8deb681a141beb5c8a025baea", size = 773632, upload-time = "2025-09-19T00:35:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/75c996dc6a2231a8652d7ad0bfbeaf8a8c77612d335580f520f3ec40e30b/regex-2025.9.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:431bd2a8726b000eb6f12429c9b438a24062a535d06783a93d2bcbad3698f8a8", size = 844104, upload-time = "2025-09-19T00:35:20.259Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f7/25aba34cc130cb6844047dbfe9716c9b8f9629fee8b8bec331aa9241b97b/regex-2025.9.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0cc3521060162d02bd36927e20690129200e5ac9d2c6d32b70368870b122db25", size = 834794, upload-time = "2025-09-19T00:35:22.002Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/64e671beafa0ae29712268421597596d781704973551312b2425831d4037/regex-2025.9.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a021217b01be2d51632ce056d7a837d3fa37c543ede36e39d14063176a26ae29", size = 778535, upload-time = "2025-09-19T00:35:23.298Z" }, + { url = "https://files.pythonhosted.org/packages/26/33/c0ebc0b07bd0bf88f716cca240546b26235a07710ea58e271cfe390ae273/regex-2025.9.18-cp310-cp310-win32.whl", hash = "sha256:4a12a06c268a629cb67cc1d009b7bb0be43e289d00d5111f86a2efd3b1949444", size = 264115, upload-time = "2025-09-19T00:35:25.206Z" }, + { url = "https://files.pythonhosted.org/packages/59/39/aeb11a4ae68faaec2498512cadae09f2d8a91f1f65730fe62b9bffeea150/regex-2025.9.18-cp310-cp310-win_amd64.whl", hash = "sha256:47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450", size = 276143, upload-time = "2025-09-19T00:35:26.785Z" }, + { url = "https://files.pythonhosted.org/packages/29/04/37f2d3fc334a1031fc2767c9d89cec13c2e72207c7e7f6feae8a47f4e149/regex-2025.9.18-cp310-cp310-win_arm64.whl", hash = "sha256:16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442", size = 268473, upload-time = "2025-09-19T00:35:28.39Z" }, + { url = "https://files.pythonhosted.org/packages/58/61/80eda662fc4eb32bfedc331f42390974c9e89c7eac1b79cd9eea4d7c458c/regex-2025.9.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:51076980cd08cd13c88eb7365427ae27f0d94e7cebe9ceb2bb9ffdae8fc4d82a", size = 484832, upload-time = "2025-09-19T00:35:30.011Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d9/33833d9abddf3f07ad48504ddb53fe3b22f353214bbb878a72eee1e3ddbf/regex-2025.9.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:828446870bd7dee4e0cbeed767f07961aa07f0ea3129f38b3ccecebc9742e0b8", size = 288994, upload-time = "2025-09-19T00:35:31.733Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b3/526ee96b0d70ea81980cbc20c3496fa582f775a52e001e2743cc33b2fa75/regex-2025.9.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c28821d5637866479ec4cc23b8c990f5bc6dd24e5e4384ba4a11d38a526e1414", size = 286619, upload-time = "2025-09-19T00:35:33.221Z" }, + { url = "https://files.pythonhosted.org/packages/65/4f/c2c096b02a351b33442aed5895cdd8bf87d372498d2100927c5a053d7ba3/regex-2025.9.18-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726177ade8e481db669e76bf99de0b278783be8acd11cef71165327abd1f170a", size = 792454, upload-time = "2025-09-19T00:35:35.361Z" }, + { url = "https://files.pythonhosted.org/packages/24/15/b562c9d6e47c403c4b5deb744f8b4bf6e40684cf866c7b077960a925bdff/regex-2025.9.18-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5cca697da89b9f8ea44115ce3130f6c54c22f541943ac8e9900461edc2b8bd4", size = 858723, upload-time = "2025-09-19T00:35:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/f2/01/dba305409849e85b8a1a681eac4c03ed327d8de37895ddf9dc137f59c140/regex-2025.9.18-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dfbde38f38004703c35666a1e1c088b778e35d55348da2b7b278914491698d6a", size = 905899, upload-time = "2025-09-19T00:35:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d0/c51d1e6a80eab11ef96a4cbad17fc0310cf68994fb01a7283276b7e5bbd6/regex-2025.9.18-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2f422214a03fab16bfa495cfec72bee4aaa5731843b771860a471282f1bf74f", size = 798981, upload-time = "2025-09-19T00:35:40.416Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5e/72db90970887bbe02296612bd61b0fa31e6d88aa24f6a4853db3e96c575e/regex-2025.9.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a295916890f4df0902e4286bc7223ee7f9e925daa6dcdec4192364255b70561a", size = 781900, upload-time = "2025-09-19T00:35:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/50/ff/596be45eea8e9bc31677fde243fa2904d00aad1b32c31bce26c3dbba0b9e/regex-2025.9.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5db95ff632dbabc8c38c4e82bf545ab78d902e81160e6e455598014f0abe66b9", size = 852952, upload-time = "2025-09-19T00:35:43.751Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1b/2dfa348fa551e900ed3f5f63f74185b6a08e8a76bc62bc9c106f4f92668b/regex-2025.9.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb967eb441b0f15ae610b7069bdb760b929f267efbf522e814bbbfffdf125ce2", size = 844355, upload-time = "2025-09-19T00:35:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/aefb1def27fe33b8cbbb19c75c13aefccfbef1c6686f8e7f7095705969c7/regex-2025.9.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f04d2f20da4053d96c08f7fde6e1419b7ec9dbcee89c96e3d731fca77f411b95", size = 787254, upload-time = "2025-09-19T00:35:46.904Z" }, + { url = "https://files.pythonhosted.org/packages/e3/4e/8ef042e7cf0dbbb401e784e896acfc1b367b95dfbfc9ada94c2ed55a081f/regex-2025.9.18-cp311-cp311-win32.whl", hash = "sha256:895197241fccf18c0cea7550c80e75f185b8bd55b6924fcae269a1a92c614a07", size = 264129, upload-time = "2025-09-19T00:35:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7d/c4fcabf80dcdd6821c0578ad9b451f8640b9110fb3dcb74793dd077069ff/regex-2025.9.18-cp311-cp311-win_amd64.whl", hash = "sha256:7e2b414deae99166e22c005e154a5513ac31493db178d8aec92b3269c9cce8c9", size = 276160, upload-time = "2025-09-19T00:36:00.45Z" }, + { url = "https://files.pythonhosted.org/packages/64/f8/0e13c8ae4d6df9d128afaba138342d532283d53a4c1e7a8c93d6756c8f4a/regex-2025.9.18-cp311-cp311-win_arm64.whl", hash = "sha256:fb137ec7c5c54f34a25ff9b31f6b7b0c2757be80176435bf367111e3f71d72df", size = 268471, upload-time = "2025-09-19T00:36:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/05859d87a66ae7098222d65748f11ef7f2dff51bfd7482a4e2256c90d72b/regex-2025.9.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e", size = 486335, upload-time = "2025-09-19T00:36:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/97/7e/d43d4e8b978890932cf7b0957fce58c5b08c66f32698f695b0c2c24a48bf/regex-2025.9.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a", size = 289720, upload-time = "2025-09-19T00:36:05.471Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/ff80886089eb5dcf7e0d2040d9aaed539e25a94300403814bb24cc775058/regex-2025.9.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab", size = 287257, upload-time = "2025-09-19T00:36:07.072Z" }, + { url = "https://files.pythonhosted.org/packages/ee/66/243edf49dd8720cba8d5245dd4d6adcb03a1defab7238598c0c97cf549b8/regex-2025.9.18-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5", size = 797463, upload-time = "2025-09-19T00:36:08.399Z" }, + { url = "https://files.pythonhosted.org/packages/df/71/c9d25a1142c70432e68bb03211d4a82299cd1c1fbc41db9409a394374ef5/regex-2025.9.18-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742", size = 862670, upload-time = "2025-09-19T00:36:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8f/329b1efc3a64375a294e3a92d43372bf1a351aa418e83c21f2f01cf6ec41/regex-2025.9.18-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425", size = 910881, upload-time = "2025-09-19T00:36:12.223Z" }, + { url = "https://files.pythonhosted.org/packages/35/9e/a91b50332a9750519320ed30ec378b74c996f6befe282cfa6bb6cea7e9fd/regex-2025.9.18-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352", size = 802011, upload-time = "2025-09-19T00:36:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/6be3b8d7856b6e0d7ee7f942f437d0a76e0d5622983abbb6d21e21ab9a17/regex-2025.9.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d", size = 786668, upload-time = "2025-09-19T00:36:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/4a60e53df58bd157c5156a1736d3636f9910bdcc271d067b32b7fcd0c3a8/regex-2025.9.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56", size = 856578, upload-time = "2025-09-19T00:36:16.845Z" }, + { url = "https://files.pythonhosted.org/packages/86/e8/162c91bfe7217253afccde112868afb239f94703de6580fb235058d506a6/regex-2025.9.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e", size = 849017, upload-time = "2025-09-19T00:36:18.597Z" }, + { url = "https://files.pythonhosted.org/packages/35/34/42b165bc45289646ea0959a1bc7531733e90b47c56a72067adfe6b3251f6/regex-2025.9.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282", size = 788150, upload-time = "2025-09-19T00:36:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/79/5d/cdd13b1f3c53afa7191593a7ad2ee24092a5a46417725ffff7f64be8342d/regex-2025.9.18-cp312-cp312-win32.whl", hash = "sha256:e1dd06f981eb226edf87c55d523131ade7285137fbde837c34dc9d1bf309f459", size = 264536, upload-time = "2025-09-19T00:36:21.922Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f5/4a7770c9a522e7d2dc1fa3ffc83ab2ab33b0b22b447e62cffef186805302/regex-2025.9.18-cp312-cp312-win_amd64.whl", hash = "sha256:3d86b5247bf25fa3715e385aa9ff272c307e0636ce0c9595f64568b41f0a9c77", size = 275501, upload-time = "2025-09-19T00:36:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/9ce3e110e70d225ecbed455b966003a3afda5e58e8aec2964042363a18f4/regex-2025.9.18-cp312-cp312-win_arm64.whl", hash = "sha256:032720248cbeeae6444c269b78cb15664458b7bb9ed02401d3da59fe4d68c3a5", size = 268601, upload-time = "2025-09-19T00:36:25.092Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c7/5c48206a60ce33711cf7dcaeaed10dd737733a3569dc7e1dce324dd48f30/regex-2025.9.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2", size = 485955, upload-time = "2025-09-19T00:36:26.822Z" }, + { url = "https://files.pythonhosted.org/packages/e9/be/74fc6bb19a3c491ec1ace943e622b5a8539068771e8705e469b2da2306a7/regex-2025.9.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb", size = 289583, upload-time = "2025-09-19T00:36:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/25/c4/9ceaa433cb5dc515765560f22a19578b95b92ff12526e5a259321c4fc1a0/regex-2025.9.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af", size = 287000, upload-time = "2025-09-19T00:36:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e6/68bc9393cb4dc68018456568c048ac035854b042bc7c33cb9b99b0680afa/regex-2025.9.18-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29", size = 797535, upload-time = "2025-09-19T00:36:31.876Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/ebae9032d34b78ecfe9bd4b5e6575b55351dc8513485bb92326613732b8c/regex-2025.9.18-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f", size = 862603, upload-time = "2025-09-19T00:36:33.344Z" }, + { url = "https://files.pythonhosted.org/packages/3b/74/12332c54b3882557a4bcd2b99f8be581f5c6a43cf1660a85b460dd8ff468/regex-2025.9.18-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68", size = 910829, upload-time = "2025-09-19T00:36:34.826Z" }, + { url = "https://files.pythonhosted.org/packages/86/70/ba42d5ed606ee275f2465bfc0e2208755b06cdabd0f4c7c4b614d51b57ab/regex-2025.9.18-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783", size = 802059, upload-time = "2025-09-19T00:36:36.664Z" }, + { url = "https://files.pythonhosted.org/packages/da/c5/fcb017e56396a7f2f8357412638d7e2963440b131a3ca549be25774b3641/regex-2025.9.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac", size = 786781, upload-time = "2025-09-19T00:36:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ee/21c4278b973f630adfb3bcb23d09d83625f3ab1ca6e40ebdffe69901c7a1/regex-2025.9.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e", size = 856578, upload-time = "2025-09-19T00:36:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/de51550dc7274324435c8f1539373ac63019b0525ad720132866fff4a16a/regex-2025.9.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23", size = 849119, upload-time = "2025-09-19T00:36:41.651Z" }, + { url = "https://files.pythonhosted.org/packages/60/52/383d3044fc5154d9ffe4321696ee5b2ee4833a28c29b137c22c33f41885b/regex-2025.9.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f", size = 788219, upload-time = "2025-09-19T00:36:43.575Z" }, + { url = "https://files.pythonhosted.org/packages/20/bd/2614fc302671b7359972ea212f0e3a92df4414aaeacab054a8ce80a86073/regex-2025.9.18-cp313-cp313-win32.whl", hash = "sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d", size = 264517, upload-time = "2025-09-19T00:36:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/ab5c1581e6563a7bffdc1974fb2d25f05689b88e2d416525271f232b1946/regex-2025.9.18-cp313-cp313-win_amd64.whl", hash = "sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d", size = 275481, upload-time = "2025-09-19T00:36:46.965Z" }, + { url = "https://files.pythonhosted.org/packages/49/22/ee47672bc7958f8c5667a587c2600a4fba8b6bab6e86bd6d3e2b5f7cac42/regex-2025.9.18-cp313-cp313-win_arm64.whl", hash = "sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb", size = 268598, upload-time = "2025-09-19T00:36:48.314Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/6887e16a187c6226cb85d8301e47d3b73ecc4505a3a13d8da2096b44fd76/regex-2025.9.18-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2", size = 489765, upload-time = "2025-09-19T00:36:49.996Z" }, + { url = "https://files.pythonhosted.org/packages/51/c5/e2f7325301ea2916ff301c8d963ba66b1b2c1b06694191df80a9c4fea5d0/regex-2025.9.18-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3", size = 291228, upload-time = "2025-09-19T00:36:51.654Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/7d229d2bc6961289e864a3a3cfebf7d0d250e2e65323a8952cbb7e22d824/regex-2025.9.18-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12", size = 289270, upload-time = "2025-09-19T00:36:53.118Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/b4f06868ee2958ff6430df89857fbf3d43014bbf35538b6ec96c2704e15d/regex-2025.9.18-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0", size = 806326, upload-time = "2025-09-19T00:36:54.631Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e4/bca99034a8f1b9b62ccf337402a8e5b959dd5ba0e5e5b2ead70273df3277/regex-2025.9.18-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6", size = 871556, upload-time = "2025-09-19T00:36:56.208Z" }, + { url = "https://files.pythonhosted.org/packages/6d/df/e06ffaf078a162f6dd6b101a5ea9b44696dca860a48136b3ae4a9caf25e2/regex-2025.9.18-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef", size = 913817, upload-time = "2025-09-19T00:36:57.807Z" }, + { url = "https://files.pythonhosted.org/packages/9e/05/25b05480b63292fd8e84800b1648e160ca778127b8d2367a0a258fa2e225/regex-2025.9.18-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a", size = 811055, upload-time = "2025-09-19T00:36:59.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/97/7bc7574655eb651ba3a916ed4b1be6798ae97af30104f655d8efd0cab24b/regex-2025.9.18-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d", size = 794534, upload-time = "2025-09-19T00:37:01.405Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c2/d5da49166a52dda879855ecdba0117f073583db2b39bb47ce9a3378a8e9e/regex-2025.9.18-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368", size = 866684, upload-time = "2025-09-19T00:37:03.441Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2d/0a5c4e6ec417de56b89ff4418ecc72f7e3feca806824c75ad0bbdae0516b/regex-2025.9.18-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90", size = 853282, upload-time = "2025-09-19T00:37:04.985Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/d656af63e31a86572ec829665d6fa06eae7e144771e0330650a8bb865635/regex-2025.9.18-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7", size = 797830, upload-time = "2025-09-19T00:37:06.697Z" }, + { url = "https://files.pythonhosted.org/packages/db/ce/06edc89df8f7b83ffd321b6071be4c54dc7332c0f77860edc40ce57d757b/regex-2025.9.18-cp313-cp313t-win32.whl", hash = "sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e", size = 267281, upload-time = "2025-09-19T00:37:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/83/9a/2b5d9c8b307a451fd17068719d971d3634ca29864b89ed5c18e499446d4a/regex-2025.9.18-cp313-cp313t-win_amd64.whl", hash = "sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730", size = 278724, upload-time = "2025-09-19T00:37:10.023Z" }, + { url = "https://files.pythonhosted.org/packages/3d/70/177d31e8089a278a764f8ec9a3faac8d14a312d622a47385d4b43905806f/regex-2025.9.18-cp313-cp313t-win_arm64.whl", hash = "sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a", size = 269771, upload-time = "2025-09-19T00:37:13.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/b7/3b4663aa3b4af16819f2ab6a78c4111c7e9b066725d8107753c2257448a5/regex-2025.9.18-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129", size = 486130, upload-time = "2025-09-19T00:37:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/4533f5d7ac9c6a02a4725fe8883de2aebc713e67e842c04cf02626afb747/regex-2025.9.18-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea", size = 289539, upload-time = "2025-09-19T00:37:16.356Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8d/5ab6797c2750985f79e9995fad3254caa4520846580f266ae3b56d1cae58/regex-2025.9.18-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1", size = 287233, upload-time = "2025-09-19T00:37:18.025Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/95afcb02ba8d3a64e6ffeb801718ce73471ad6440c55d993f65a4a5e7a92/regex-2025.9.18-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47", size = 797876, upload-time = "2025-09-19T00:37:19.609Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fb/720b1f49cec1f3b5a9fea5b34cd22b88b5ebccc8c1b5de9cc6f65eed165a/regex-2025.9.18-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379", size = 863385, upload-time = "2025-09-19T00:37:21.65Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ca/e0d07ecf701e1616f015a720dc13b84c582024cbfbb3fc5394ae204adbd7/regex-2025.9.18-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203", size = 910220, upload-time = "2025-09-19T00:37:23.723Z" }, + { url = "https://files.pythonhosted.org/packages/b6/45/bba86413b910b708eca705a5af62163d5d396d5f647ed9485580c7025209/regex-2025.9.18-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164", size = 801827, upload-time = "2025-09-19T00:37:25.684Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/740fbd9fcac31a1305a8eed30b44bf0f7f1e042342be0a4722c0365ecfca/regex-2025.9.18-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb", size = 786843, upload-time = "2025-09-19T00:37:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/80/a7/0579e8560682645906da640c9055506465d809cb0f5415d9976f417209a6/regex-2025.9.18-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743", size = 857430, upload-time = "2025-09-19T00:37:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9b/4dc96b6c17b38900cc9fee254fc9271d0dde044e82c78c0811b58754fde5/regex-2025.9.18-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282", size = 848612, upload-time = "2025-09-19T00:37:31.42Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6a/6f659f99bebb1775e5ac81a3fb837b85897c1a4ef5acffd0ff8ffe7e67fb/regex-2025.9.18-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773", size = 787967, upload-time = "2025-09-19T00:37:34.019Z" }, + { url = "https://files.pythonhosted.org/packages/61/35/9e35665f097c07cf384a6b90a1ac11b0b1693084a0b7a675b06f760496c6/regex-2025.9.18-cp314-cp314-win32.whl", hash = "sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788", size = 269847, upload-time = "2025-09-19T00:37:35.759Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/27594dbe0f1590b82de2821ebfe9a359b44dcb9b65524876cd12fabc447b/regex-2025.9.18-cp314-cp314-win_amd64.whl", hash = "sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3", size = 278755, upload-time = "2025-09-19T00:37:37.367Z" }, + { url = "https://files.pythonhosted.org/packages/30/a3/0cd8d0d342886bd7d7f252d701b20ae1a3c72dc7f34ef4b2d17790280a09/regex-2025.9.18-cp314-cp314-win_arm64.whl", hash = "sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d", size = 271873, upload-time = "2025-09-19T00:37:39.125Z" }, + { url = "https://files.pythonhosted.org/packages/99/cb/8a1ab05ecf404e18b54348e293d9b7a60ec2bd7aa59e637020c5eea852e8/regex-2025.9.18-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306", size = 489773, upload-time = "2025-09-19T00:37:40.968Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/6543c9b7f7e734d2404fa2863d0d710c907bef99d4598760ed4563d634c3/regex-2025.9.18-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946", size = 291221, upload-time = "2025-09-19T00:37:42.901Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/e9fdee6ad6bf708d98c5d17fded423dcb0661795a49cba1b4ffb8358377a/regex-2025.9.18-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f", size = 289268, upload-time = "2025-09-19T00:37:44.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/a6/bc3e8a918abe4741dadeaeb6c508e3a4ea847ff36030d820d89858f96a6c/regex-2025.9.18-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95", size = 806659, upload-time = "2025-09-19T00:37:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/2b/71/ea62dbeb55d9e6905c7b5a49f75615ea1373afcad95830047e4e310db979/regex-2025.9.18-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b", size = 871701, upload-time = "2025-09-19T00:37:48.882Z" }, + { url = "https://files.pythonhosted.org/packages/6a/90/fbe9dedb7dad24a3a4399c0bae64bfa932ec8922a0a9acf7bc88db30b161/regex-2025.9.18-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3", size = 913742, upload-time = "2025-09-19T00:37:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/47e4a8c0e73d41eb9eb9fdeba3b1b810110a5139a2526e82fd29c2d9f867/regex-2025.9.18-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571", size = 811117, upload-time = "2025-09-19T00:37:52.686Z" }, + { url = "https://files.pythonhosted.org/packages/2a/da/435f29fddfd015111523671e36d30af3342e8136a889159b05c1d9110480/regex-2025.9.18-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad", size = 794647, upload-time = "2025-09-19T00:37:54.626Z" }, + { url = "https://files.pythonhosted.org/packages/23/66/df5e6dcca25c8bc57ce404eebc7342310a0d218db739d7882c9a2b5974a3/regex-2025.9.18-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494", size = 866747, upload-time = "2025-09-19T00:37:56.367Z" }, + { url = "https://files.pythonhosted.org/packages/82/42/94392b39b531f2e469b2daa40acf454863733b674481fda17462a5ffadac/regex-2025.9.18-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b", size = 853434, upload-time = "2025-09-19T00:37:58.39Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f8/dcc64c7f7bbe58842a8f89622b50c58c3598fbbf4aad0a488d6df2c699f1/regex-2025.9.18-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41", size = 798024, upload-time = "2025-09-19T00:38:00.397Z" }, + { url = "https://files.pythonhosted.org/packages/20/8d/edf1c5d5aa98f99a692313db813ec487732946784f8f93145e0153d910e5/regex-2025.9.18-cp314-cp314t-win32.whl", hash = "sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096", size = 273029, upload-time = "2025-09-19T00:38:02.383Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/02d4e4f88466f17b145f7ea2b2c11af3a942db6222429c2c146accf16054/regex-2025.9.18-cp314-cp314t-win_amd64.whl", hash = "sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a", size = 282680, upload-time = "2025-09-19T00:38:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a3/c64894858aaaa454caa7cc47e2f225b04d3ed08ad649eacf58d45817fad2/regex-2025.9.18-cp314-cp314t-win_arm64.whl", hash = "sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01", size = 273034, upload-time = "2025-09-19T00:38:05.807Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" }, + { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" }, + { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" }, + { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" }, + { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, + { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, + { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, + { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" }, + { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" }, + { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" }, + { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" }, + { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, + { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" }, + { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" }, + { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, +] + +[[package]] +name = "safetensors" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/cc/738f3011628920e027a11754d9cae9abec1aed00f7ae860abbf843755233/safetensors-0.6.2.tar.gz", hash = "sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9", size = 197968, upload-time = "2025-08-08T13:13:58.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b1/3f5fd73c039fc87dba3ff8b5d528bfc5a32b597fea8e7a6a4800343a17c7/safetensors-0.6.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba", size = 454797, upload-time = "2025-08-08T13:13:52.066Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c9/bb114c158540ee17907ec470d01980957fdaf87b4aa07914c24eba87b9c6/safetensors-0.6.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b", size = 432206, upload-time = "2025-08-08T13:13:50.931Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/f70c34e47df3110e8e0bb268d90db8d4be8958a54ab0336c9be4fe86dac8/safetensors-0.6.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd", size = 473261, upload-time = "2025-08-08T13:13:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f5/be9c6a7c7ef773e1996dc214e73485286df1836dbd063e8085ee1976f9cb/safetensors-0.6.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a", size = 485117, upload-time = "2025-08-08T13:13:43.506Z" }, + { url = "https://files.pythonhosted.org/packages/c9/55/23f2d0a2c96ed8665bf17a30ab4ce5270413f4d74b6d87dd663258b9af31/safetensors-0.6.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1", size = 616154, upload-time = "2025-08-08T13:13:45.096Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/affb0bd9ce02aa46e7acddbe087912a04d953d7a4d74b708c91b5806ef3f/safetensors-0.6.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda", size = 520713, upload-time = "2025-08-08T13:13:46.25Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5d/5a514d7b88e310c8b146e2404e0dc161282e78634d9358975fd56dfd14be/safetensors-0.6.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f", size = 485835, upload-time = "2025-08-08T13:13:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7b/4fc3b2ba62c352b2071bea9cfbad330fadda70579f617506ae1a2f129cab/safetensors-0.6.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19", size = 521503, upload-time = "2025-08-08T13:13:47.651Z" }, + { url = "https://files.pythonhosted.org/packages/5a/50/0057e11fe1f3cead9254315a6c106a16dd4b1a19cd247f7cc6414f6b7866/safetensors-0.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce", size = 652256, upload-time = "2025-08-08T13:13:53.167Z" }, + { url = "https://files.pythonhosted.org/packages/e9/29/473f789e4ac242593ac1656fbece6e1ecd860bb289e635e963667807afe3/safetensors-0.6.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7", size = 747281, upload-time = "2025-08-08T13:13:54.656Z" }, + { url = "https://files.pythonhosted.org/packages/68/52/f7324aad7f2df99e05525c84d352dc217e0fa637a4f603e9f2eedfbe2c67/safetensors-0.6.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5", size = 692286, upload-time = "2025-08-08T13:13:55.884Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/cad1d9762868c7c5dc70c8620074df28ebb1a8e4c17d4c0cb031889c457e/safetensors-0.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac", size = 655957, upload-time = "2025-08-08T13:13:57.029Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/e2158e17bbe57d104f0abbd95dff60dda916cf277c9f9663b4bf9bad8b6e/safetensors-0.6.2-cp38-abi3-win32.whl", hash = "sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1", size = 308926, upload-time = "2025-08-08T13:14:01.095Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c3/c0be1135726618dc1e28d181b8c442403d8dbb9e273fd791de2d4384bcdd/safetensors-0.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c", size = 320192, upload-time = "2025-08-08T13:13:59.467Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/3b/546a6f0bfe791bbb7f8d591613454d15097e53f906308ec6f7c1ce588e8e/scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b", size = 30580599, upload-time = "2025-09-11T17:48:08.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/ef/37ed4b213d64b48422df92560af7300e10fe30b5d665dd79932baebee0c6/scipy-1.16.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6ab88ea43a57da1af33292ebd04b417e8e2eaf9d5aa05700be8d6e1b6501cd92", size = 36619956, upload-time = "2025-09-11T17:39:20.5Z" }, + { url = "https://files.pythonhosted.org/packages/85/ab/5c2eba89b9416961a982346a4d6a647d78c91ec96ab94ed522b3b6baf444/scipy-1.16.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c95e96c7305c96ede73a7389f46ccd6c659c4da5ef1b2789466baeaed3622b6e", size = 28931117, upload-time = "2025-09-11T17:39:29.06Z" }, + { url = "https://files.pythonhosted.org/packages/80/d1/eed51ab64d227fe60229a2d57fb60ca5898cfa50ba27d4f573e9e5f0b430/scipy-1.16.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:87eb178db04ece7c698220d523c170125dbffebb7af0345e66c3554f6f60c173", size = 20921997, upload-time = "2025-09-11T17:39:34.892Z" }, + { url = "https://files.pythonhosted.org/packages/be/7c/33ea3e23bbadde96726edba6bf9111fb1969d14d9d477ffa202c67bec9da/scipy-1.16.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:4e409eac067dcee96a57fbcf424c13f428037827ec7ee3cb671ff525ca4fc34d", size = 23523374, upload-time = "2025-09-11T17:39:40.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/7399dc96e1e3f9a05e258c98d716196a34f528eef2ec55aad651ed136d03/scipy-1.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e574be127bb760f0dad24ff6e217c80213d153058372362ccb9555a10fc5e8d2", size = 33583702, upload-time = "2025-09-11T17:39:49.011Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bc/a5c75095089b96ea72c1bd37a4497c24b581ec73db4ef58ebee142ad2d14/scipy-1.16.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5db5ba6188d698ba7abab982ad6973265b74bb40a1efe1821b58c87f73892b9", size = 35883427, upload-time = "2025-09-11T17:39:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/ab/66/e25705ca3d2b87b97fe0a278a24b7f477b4023a926847935a1a71488a6a6/scipy-1.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec6e74c4e884104ae006d34110677bfe0098203a3fec2f3faf349f4cb05165e3", size = 36212940, upload-time = "2025-09-11T17:40:06.013Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fd/0bb911585e12f3abdd603d721d83fc1c7492835e1401a0e6d498d7822b4b/scipy-1.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:912f46667d2d3834bc3d57361f854226475f695eb08c08a904aadb1c936b6a88", size = 38865092, upload-time = "2025-09-11T17:40:15.143Z" }, + { url = "https://files.pythonhosted.org/packages/d6/73/c449a7d56ba6e6f874183759f8483cde21f900a8be117d67ffbb670c2958/scipy-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:91e9e8a37befa5a69e9cacbe0bcb79ae5afb4a0b130fd6db6ee6cc0d491695fa", size = 38687626, upload-time = "2025-09-11T17:40:24.041Z" }, + { url = "https://files.pythonhosted.org/packages/68/72/02f37316adf95307f5d9e579023c6899f89ff3a051fa079dbd6faafc48e5/scipy-1.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:f3bf75a6dcecab62afde4d1f973f1692be013110cad5338007927db8da73249c", size = 25503506, upload-time = "2025-09-11T17:40:30.703Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8d/6396e00db1282279a4ddd507c5f5e11f606812b608ee58517ce8abbf883f/scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d", size = 36646259, upload-time = "2025-09-11T17:40:39.329Z" }, + { url = "https://files.pythonhosted.org/packages/3b/93/ea9edd7e193fceb8eef149804491890bde73fb169c896b61aa3e2d1e4e77/scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371", size = 28888976, upload-time = "2025-09-11T17:40:46.82Z" }, + { url = "https://files.pythonhosted.org/packages/91/4d/281fddc3d80fd738ba86fd3aed9202331180b01e2c78eaae0642f22f7e83/scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0", size = 20879905, upload-time = "2025-09-11T17:40:52.545Z" }, + { url = "https://files.pythonhosted.org/packages/69/40/b33b74c84606fd301b2915f0062e45733c6ff5708d121dd0deaa8871e2d0/scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232", size = 23553066, upload-time = "2025-09-11T17:40:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/55/a7/22c739e2f21a42cc8f16bc76b47cff4ed54fbe0962832c589591c2abec34/scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1", size = 33336407, upload-time = "2025-09-11T17:41:06.796Z" }, + { url = "https://files.pythonhosted.org/packages/53/11/a0160990b82999b45874dc60c0c183d3a3a969a563fffc476d5a9995c407/scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f", size = 35673281, upload-time = "2025-09-11T17:41:15.055Z" }, + { url = "https://files.pythonhosted.org/packages/96/53/7ef48a4cfcf243c3d0f1643f5887c81f29fdf76911c4e49331828e19fc0a/scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef", size = 36004222, upload-time = "2025-09-11T17:41:23.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7f/71a69e0afd460049d41c65c630c919c537815277dfea214031005f474d78/scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1", size = 38664586, upload-time = "2025-09-11T17:41:31.021Z" }, + { url = "https://files.pythonhosted.org/packages/34/95/20e02ca66fb495a95fba0642fd48e0c390d0ece9b9b14c6e931a60a12dea/scipy-1.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e", size = 38550641, upload-time = "2025-09-11T17:41:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/92/ad/13646b9beb0a95528ca46d52b7babafbe115017814a611f2065ee4e61d20/scipy-1.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851", size = 25456070, upload-time = "2025-09-11T17:41:41.3Z" }, + { url = "https://files.pythonhosted.org/packages/c1/27/c5b52f1ee81727a9fc457f5ac1e9bf3d6eab311805ea615c83c27ba06400/scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70", size = 36604856, upload-time = "2025-09-11T17:41:47.695Z" }, + { url = "https://files.pythonhosted.org/packages/32/a9/15c20d08e950b540184caa8ced675ba1128accb0e09c653780ba023a4110/scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9", size = 28864626, upload-time = "2025-09-11T17:41:52.642Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5", size = 20855689, upload-time = "2025-09-11T17:41:57.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925", size = 23512151, upload-time = "2025-09-11T17:42:02.303Z" }, + { url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9", size = 33329824, upload-time = "2025-09-11T17:42:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7", size = 35681881, upload-time = "2025-09-11T17:42:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb", size = 36006219, upload-time = "2025-09-11T17:42:18.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e", size = 38682147, upload-time = "2025-09-11T17:42:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/0f38e396ad19e41b4c5db66130167eef8ee620a49bc7d0512e3bb67e0cab/scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c", size = 38520766, upload-time = "2025-09-11T17:43:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/85d3e867b6822d331e26c862a91375bb7746a0b458db5effa093d34cdb89/scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104", size = 25451169, upload-time = "2025-09-11T17:43:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1", size = 37012682, upload-time = "2025-09-11T17:42:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a", size = 29389926, upload-time = "2025-09-11T17:42:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f", size = 21381152, upload-time = "2025-09-11T17:42:40.07Z" }, + { url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4", size = 23914410, upload-time = "2025-09-11T17:42:44.313Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21", size = 33481880, upload-time = "2025-09-11T17:42:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7", size = 35791425, upload-time = "2025-09-11T17:42:54.711Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8", size = 36178622, upload-time = "2025-09-11T17:43:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472", size = 38783985, upload-time = "2025-09-11T17:43:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/9f/bc81c1d1e033951eb5912cd3750cc005943afa3e65a725d2443a3b3c4347/scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351", size = 38631367, upload-time = "2025-09-11T17:43:14.44Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5e/2cc7555fd81d01814271412a1d59a289d25f8b63208a0a16c21069d55d3e/scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d", size = 25787992, upload-time = "2025-09-11T17:43:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ac/ad8951250516db71619f0bd3b2eb2448db04b720a003dd98619b78b692c0/scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77", size = 36595109, upload-time = "2025-09-11T17:43:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f6/5779049ed119c5b503b0f3dc6d6f3f68eefc3a9190d4ad4c276f854f051b/scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70", size = 28859110, upload-time = "2025-09-11T17:43:40.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/09/9986e410ae38bf0a0c737ff8189ac81a93b8e42349aac009891c054403d7/scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88", size = 20850110, upload-time = "2025-09-11T17:43:44.981Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ad/485cdef2d9215e2a7df6d61b81d2ac073dfacf6ae24b9ae87274c4e936ae/scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f", size = 23497014, upload-time = "2025-09-11T17:43:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/f6a852e5d581122b8f0f831f1d1e32fb8987776ed3658e95c377d308ed86/scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb", size = 33401155, upload-time = "2025-09-11T17:43:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f5/61d243bbc7c6e5e4e13dde9887e84a5cbe9e0f75fd09843044af1590844e/scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7", size = 35691174, upload-time = "2025-09-11T17:44:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/59933956331f8cc57e406cdb7a483906c74706b156998f322913e789c7e1/scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548", size = 36070752, upload-time = "2025-09-11T17:44:05.619Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7d/00f825cfb47ee19ef74ecf01244b43e95eae74e7e0ff796026ea7cd98456/scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936", size = 38701010, upload-time = "2025-09-11T17:44:11.322Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9f/b62587029980378304ba5a8563d376c96f40b1e133daacee76efdcae32de/scipy-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff", size = 39360061, upload-time = "2025-09-11T17:45:09.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/7a2f1609921352c7fbee0815811b5050582f67f19983096c4769867ca45f/scipy-1.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d", size = 26126914, upload-time = "2025-09-11T17:45:14.73Z" }, + { url = "https://files.pythonhosted.org/packages/51/b9/60929ce350c16b221928725d2d1d7f86cf96b8bc07415547057d1196dc92/scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8", size = 37013193, upload-time = "2025-09-11T17:44:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/2a/41/ed80e67782d4bc5fc85a966bc356c601afddd175856ba7c7bb6d9490607e/scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4", size = 29390172, upload-time = "2025-09-11T17:44:21.783Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a3/2f673ace4090452696ccded5f5f8efffb353b8f3628f823a110e0170b605/scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831", size = 21381326, upload-time = "2025-09-11T17:44:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/42/bf/59df61c5d51395066c35836b78136accf506197617c8662e60ea209881e1/scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3", size = 23915036, upload-time = "2025-09-11T17:44:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/91/c3/edc7b300dc16847ad3672f1a6f3f7c5d13522b21b84b81c265f4f2760d4a/scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac", size = 33484341, upload-time = "2025-09-11T17:44:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/26/c7/24d1524e72f06ff141e8d04b833c20db3021020563272ccb1b83860082a9/scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374", size = 35790840, upload-time = "2025-09-11T17:44:41.76Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b7/5aaad984eeedd56858dc33d75efa59e8ce798d918e1033ef62d2708f2c3d/scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6", size = 36174716, upload-time = "2025-09-11T17:44:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c2/e276a237acb09824822b0ada11b028ed4067fdc367a946730979feacb870/scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c", size = 38790088, upload-time = "2025-09-11T17:44:53.011Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b4/5c18a766e8353015439f3780f5fc473f36f9762edc1a2e45da3ff5a31b21/scipy-1.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9", size = 39457455, upload-time = "2025-09-11T17:44:58.899Z" }, + { url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pillow" }, + { name = "scikit-learn" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/47/7d61a19ba7e6b5f36f0ffff5bbf032a1c1913612caac611e12383069eda0/sentence_transformers-5.1.1.tar.gz", hash = "sha256:8af3f844b2ecf9a6c2dfeafc2c02938a87f61202b54329d70dfd7dfd7d17a84e", size = 374434, upload-time = "2025-09-22T11:28:27.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/21/4670d03ab8587b0ab6f7d5fa02a95c3dd6b1f39d0e40e508870201f3d76c/sentence_transformers-5.1.1-py3-none-any.whl", hash = "sha256:5ed544629eafe89ca668a8910ebff96cf0a9c5254ec14b05c66c086226c892fd", size = 486574, upload-time = "2025-09-22T11:28:26.311Z" }, +] + +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/48/fb401ec8c4953d519d05c87feca816ad668b8258448ff60579ac7a1c1386/setproctitle-1.3.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf555b6299f10a6eb44e4f96d2f5a3884c70ce25dc5c8796aaa2f7b40e72cb1b", size = 18079, upload-time = "2025-09-05T12:49:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/c2b0333c2716fb3b4c9a973dd113366ac51b4f8d56b500f4f8f704b4817a/setproctitle-1.3.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:690b4776f9c15aaf1023bb07d7c5b797681a17af98a4a69e76a1d504e41108b7", size = 13099, upload-time = "2025-09-05T12:49:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f8/17bda581c517678260e6541b600eeb67745f53596dc077174141ba2f6702/setproctitle-1.3.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00afa6fc507967d8c9d592a887cdc6c1f5742ceac6a4354d111ca0214847732c", size = 31793, upload-time = "2025-09-05T12:49:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/27/d1/76a33ae80d4e788ecab9eb9b53db03e81cfc95367ec7e3fbf4989962fedd/setproctitle-1.3.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e02667f6b9fc1238ba753c0f4b0a37ae184ce8f3bbbc38e115d99646b3f4cd3", size = 32779, upload-time = "2025-09-05T12:49:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/59/27/1a07c38121967061564f5e0884414a5ab11a783260450172d4fc68c15621/setproctitle-1.3.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83fcd271567d133eb9532d3b067c8a75be175b2b3b271e2812921a05303a693f", size = 34578, upload-time = "2025-09-05T12:49:13.393Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d4/725e6353935962d8bb12cbf7e7abba1d0d738c7f6935f90239d8e1ccf913/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13fe37951dda1a45c35d77d06e3da5d90e4f875c4918a7312b3b4556cfa7ff64", size = 32030, upload-time = "2025-09-05T12:49:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/e4677ae8e1cb0d549ab558b12db10c175a889be0974c589c428fece5433e/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a05509cfb2059e5d2ddff701d38e474169e9ce2a298cf1b6fd5f3a213a553fe5", size = 33363, upload-time = "2025-09-05T12:49:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/55/d4/69ce66e4373a48fdbb37489f3ded476bb393e27f514968c3a69a67343ae0/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6da835e76ae18574859224a75db6e15c4c2aaa66d300a57efeaa4c97ca4c7381", size = 31508, upload-time = "2025-09-05T12:49:18.032Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5a/42c1ed0e9665d068146a68326529b5686a1881c8b9197c2664db4baf6aeb/setproctitle-1.3.7-cp310-cp310-win32.whl", hash = "sha256:9e803d1b1e20240a93bac0bc1025363f7f80cb7eab67dfe21efc0686cc59ad7c", size = 12558, upload-time = "2025-09-05T12:49:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fe/dd206cc19a25561921456f6cb12b405635319299b6f366e0bebe872abc18/setproctitle-1.3.7-cp310-cp310-win_amd64.whl", hash = "sha256:a97200acc6b64ec4cada52c2ecaf1fba1ef9429ce9c542f8a7db5bcaa9dcbd95", size = 13245, upload-time = "2025-09-05T12:49:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/cd/1b7ba5cad635510720ce19d7122154df96a2387d2a74217be552887c93e5/setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0", size = 18085, upload-time = "2025-09-05T12:49:22.183Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/b2da0a620490aae355f9d72072ac13e901a9fec809a6a24fc6493a8f3c35/setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4", size = 13097, upload-time = "2025-09-05T12:49:23.322Z" }, + { url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e1277f9ba302f1a250bbd3eedbbee747a244b3cc682eb58fb9733968f6d8/setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d", size = 12556, upload-time = "2025-09-05T12:49:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/822a23f17e9003dfdee92cd72758441ca2a3680388da813a371b716fb07f/setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4", size = 13243, upload-time = "2025-09-05T12:49:34.553Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047, upload-time = "2025-09-05T12:49:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073, upload-time = "2025-09-05T12:49:51.46Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284, upload-time = "2025-09-05T12:49:52.741Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/adf3c4c0a2173cb7920dc9df710bcc67e9bcdbf377e243b7a962dc31a51a/setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0", size = 34104, upload-time = "2025-09-05T12:49:54.416Z" }, + { url = "https://files.pythonhosted.org/packages/52/4f/6daf66394152756664257180439d37047aa9a1cfaa5e4f5ed35e93d1dc06/setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929", size = 35982, upload-time = "2025-09-05T12:49:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/1b/62/f2c0595403cf915db031f346b0e3b2c0096050e90e0be658a64f44f4278a/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f", size = 33150, upload-time = "2025-09-05T12:49:58.025Z" }, + { url = "https://files.pythonhosted.org/packages/a0/29/10dd41cde849fb2f9b626c846b7ea30c99c81a18a5037a45cc4ba33c19a7/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698", size = 34463, upload-time = "2025-09-05T12:49:59.424Z" }, + { url = "https://files.pythonhosted.org/packages/71/3c/cedd8eccfaf15fb73a2c20525b68c9477518917c9437737fa0fda91e378f/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c", size = 32848, upload-time = "2025-09-05T12:50:01.107Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3e/0a0e27d1c9926fecccfd1f91796c244416c70bf6bca448d988638faea81d/setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd", size = 12544, upload-time = "2025-09-05T12:50:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/6bf4cb7acbbd5c846ede1c3f4d6b4ee52744d402e43546826da065ff2ab7/setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f", size = 13235, upload-time = "2025-09-05T12:50:16.89Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a4/d588d3497d4714750e3eaf269e9e8985449203d82b16b933c39bd3fc52a1/setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9", size = 18058, upload-time = "2025-09-05T12:50:02.501Z" }, + { url = "https://files.pythonhosted.org/packages/05/77/7637f7682322a7244e07c373881c7e982567e2cb1dd2f31bd31481e45500/setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5", size = 13072, upload-time = "2025-09-05T12:50:03.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/09/f366eca0973cfbac1470068d1313fa3fe3de4a594683385204ec7f1c4101/setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29", size = 34490, upload-time = "2025-09-05T12:50:04.948Z" }, + { url = "https://files.pythonhosted.org/packages/71/36/611fc2ed149fdea17c3677e1d0df30d8186eef9562acc248682b91312706/setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152", size = 35267, upload-time = "2025-09-05T12:50:06.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/a4/64e77d0671446bd5a5554387b69e1efd915274686844bea733714c828813/setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c", size = 37376, upload-time = "2025-09-05T12:50:07.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/ad9c664fe524fb4a4b2d3663661a5c63453ce851736171e454fa2cdec35c/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b", size = 33963, upload-time = "2025-09-05T12:50:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a36de7caf2d90c4c28678da1466b47495cbbad43badb4e982d8db8167ed4/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18", size = 35550, upload-time = "2025-09-05T12:50:10.791Z" }, + { url = "https://files.pythonhosted.org/packages/dd/68/17e8aea0ed5ebc17fbf03ed2562bfab277c280e3625850c38d92a7b5fcd9/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c", size = 33727, upload-time = "2025-09-05T12:50:12.032Z" }, + { url = "https://files.pythonhosted.org/packages/b2/33/90a3bf43fe3a2242b4618aa799c672270250b5780667898f30663fd94993/setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29", size = 12549, upload-time = "2025-09-05T12:50:13.074Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/50d1f07f3032e1f23d814ad6462bc0a138f369967c72494286b8a5228e40/setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9", size = 13243, upload-time = "2025-09-05T12:50:14.146Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" }, + { url = "https://files.pythonhosted.org/packages/34/8a/aff5506ce89bc3168cb492b18ba45573158d528184e8a9759a05a09088a9/setproctitle-1.3.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:eb440c5644a448e6203935ed60466ec8d0df7278cd22dc6cf782d07911bcbea6", size = 12654, upload-time = "2025-09-05T12:51:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/5b6f2faedd6ced3d3c085a5efbd91380fb1f61f4c12bc42acad37932f4e9/setproctitle-1.3.7-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:502b902a0e4c69031b87870ff4986c290ebbb12d6038a70639f09c331b18efb2", size = 14284, upload-time = "2025-09-05T12:51:18.393Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c0/4312fed3ca393a29589603fd48f17937b4ed0638b923bac75a728382e730/setproctitle-1.3.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6f268caeabb37ccd824d749e7ce0ec6337c4ed954adba33ec0d90cc46b0ab78", size = 13282, upload-time = "2025-09-05T12:51:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5b/5e1c117ac84e3cefcf8d7a7f6b2461795a87e20869da065a5c087149060b/setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1", size = 12587, upload-time = "2025-09-05T12:51:21.195Z" }, + { url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/1be1d2a53c2a91ec48fa2ff4a409b395f836798adf194d99de9c059419ea/setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a", size = 13282, upload-time = "2025-09-05T12:51:24.094Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "sglang" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "requests" }, + { name = "setproctitle" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/f0/954c401fe1bc80135c245f477cb117d7bb301f7b2eebcf38dcf211c03ac1/sglang-0.5.2.tar.gz", hash = "sha256:0c8a9ad02278d12eba2f30928e0464a646d03b2e2f32efcf6c681bbd795df793", size = 1627791, upload-time = "2025-09-11T23:09:48.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2b/44c336e0be9a9a23e56b6fcfed3b6f03dfc8a4181ef2cc82129aa9811fa8/sglang-0.5.2-py3-none-any.whl", hash = "sha256:83aae146f3913ed0802bb1ea356facff47efe0e7d18041a3f143de9ef6e44b2c", size = 2184239, upload-time = "2025-09-11T23:09:46.458Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/4e/985f7da36f09592c5ade99321c72c15101d23c0bb7eecfd1daaca5714422/sqlalchemy-2.0.43-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069", size = 2133162, upload-time = "2025-08-11T15:52:17.854Z" }, + { url = "https://files.pythonhosted.org/packages/37/34/798af8db3cae069461e3bc0898a1610dc469386a97048471d364dc8aae1c/sqlalchemy-2.0.43-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154", size = 2123082, upload-time = "2025-08-11T15:52:19.181Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0f/79cf4d9dad42f61ec5af1e022c92f66c2d110b93bb1dc9b033892971abfa/sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612", size = 3208871, upload-time = "2025-08-11T15:50:30.656Z" }, + { url = "https://files.pythonhosted.org/packages/56/b3/59befa58fb0e1a9802c87df02344548e6d007e77e87e6084e2131c29e033/sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019", size = 3209583, upload-time = "2025-08-11T15:57:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/29/d2/124b50c0eb8146e8f0fe16d01026c1a073844f0b454436d8544fe9b33bd7/sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20", size = 3148177, upload-time = "2025-08-11T15:50:32.078Z" }, + { url = "https://files.pythonhosted.org/packages/83/f5/e369cd46aa84278107624617034a5825fedfc5c958b2836310ced4d2eadf/sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18", size = 3172276, upload-time = "2025-08-11T15:57:49.477Z" }, + { url = "https://files.pythonhosted.org/packages/de/2b/4602bf4c3477fa4c837c9774e6dd22e0389fc52310c4c4dfb7e7ba05e90d/sqlalchemy-2.0.43-cp310-cp310-win32.whl", hash = "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00", size = 2101491, upload-time = "2025-08-11T15:54:59.191Z" }, + { url = "https://files.pythonhosted.org/packages/38/2d/bfc6b6143adef553a08295490ddc52607ee435b9c751c714620c1b3dd44d/sqlalchemy-2.0.43-cp310-cp310-win_amd64.whl", hash = "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b", size = 2125148, upload-time = "2025-08-11T15:55:00.593Z" }, + { url = "https://files.pythonhosted.org/packages/9d/77/fa7189fe44114658002566c6fe443d3ed0ec1fa782feb72af6ef7fbe98e7/sqlalchemy-2.0.43-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29", size = 2136472, upload-time = "2025-08-11T15:52:21.789Z" }, + { url = "https://files.pythonhosted.org/packages/99/ea/92ac27f2fbc2e6c1766bb807084ca455265707e041ba027c09c17d697867/sqlalchemy-2.0.43-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631", size = 2126535, upload-time = "2025-08-11T15:52:23.109Z" }, + { url = "https://files.pythonhosted.org/packages/94/12/536ede80163e295dc57fff69724caf68f91bb40578b6ac6583a293534849/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685", size = 3297521, upload-time = "2025-08-11T15:50:33.536Z" }, + { url = "https://files.pythonhosted.org/packages/03/b5/cacf432e6f1fc9d156eca0560ac61d4355d2181e751ba8c0cd9cb232c8c1/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca", size = 3297343, upload-time = "2025-08-11T15:57:51.186Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/d4c9b526f18457667de4c024ffbc3a0920c34237b9e9dd298e44c7c00ee5/sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d", size = 3232113, upload-time = "2025-08-11T15:50:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/aa/79/c0121b12b1b114e2c8a10ea297a8a6d5367bc59081b2be896815154b1163/sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3", size = 3258240, upload-time = "2025-08-11T15:57:52.983Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/a2f9be96fb382f3ba027ad42f00dbe30fdb6ba28cda5f11412eee346bec5/sqlalchemy-2.0.43-cp311-cp311-win32.whl", hash = "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921", size = 2101248, upload-time = "2025-08-11T15:55:01.855Z" }, + { url = "https://files.pythonhosted.org/packages/ee/13/744a32ebe3b4a7a9c7ea4e57babae7aa22070d47acf330d8e5a1359607f1/sqlalchemy-2.0.43-cp311-cp311-win_amd64.whl", hash = "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8", size = 2126109, upload-time = "2025-08-11T15:55:04.092Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/20c78f1081446095450bdc6ee6cc10045fce67a8e003a5876b6eaafc5cc4/sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", size = 2134891, upload-time = "2025-08-11T15:51:13.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/0a/3d89034ae62b200b4396f0f95319f7d86e9945ee64d2343dcad857150fa2/sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", size = 2123061, upload-time = "2025-08-11T15:51:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/cb/10/2711f7ff1805919221ad5bee205971254845c069ee2e7036847103ca1e4c/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", size = 3320384, upload-time = "2025-08-11T15:52:35.088Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0e/3d155e264d2ed2778484006ef04647bc63f55b3e2d12e6a4f787747b5900/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", size = 3329648, upload-time = "2025-08-11T15:56:34.153Z" }, + { url = "https://files.pythonhosted.org/packages/5b/81/635100fb19725c931622c673900da5efb1595c96ff5b441e07e3dd61f2be/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", size = 3258030, upload-time = "2025-08-11T15:52:36.933Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/a99302716d62b4965fded12520c1cbb189f99b17a6d8cf77611d21442e47/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", size = 3294469, upload-time = "2025-08-11T15:56:35.553Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a2/3a11b06715149bf3310b55a98b5c1e84a42cfb949a7b800bc75cb4e33abc/sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", size = 2098906, upload-time = "2025-08-11T15:55:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/bc/09/405c915a974814b90aa591280623adc6ad6b322f61fd5cff80aeaef216c9/sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", size = 2126260, upload-time = "2025-08-11T15:55:02.965Z" }, + { url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" }, + { url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "striprtf" +version = "0.0.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/20/3d419008265346452d09e5dadfd5d045b64b40d8fc31af40588e6c76997a/striprtf-0.0.26.tar.gz", hash = "sha256:fdb2bba7ac440072d1c41eab50d8d74ae88f60a8b6575c6e2c7805dc462093aa", size = 6258, upload-time = "2023-07-20T14:30:36.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/cf/0fea4f4ba3fc2772ac2419278aa9f6964124d4302117d61bc055758e000c/striprtf-0.0.26-py3-none-any.whl", hash = "sha256:8c8f9d32083cdc2e8bfb149455aa1cc5a4e0a035893bedc75db8b73becb3a1bb", size = 6914, upload-time = "2023-07-20T14:30:35.338Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/86/ad0155a37c4f310935d5ac0b1ccf9bdb635dcb906e0a9a26b616dd55825a/tiktoken-0.11.0.tar.gz", hash = "sha256:3c518641aee1c52247c2b97e74d8d07d780092af79d5911a6ab5e79359d9b06a", size = 37648, upload-time = "2025-08-08T23:58:08.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/4d/c6a2e7dca2b4f2e9e0bfd62b3fe4f114322e2c028cfba905a72bc76ce479/tiktoken-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8a9b517d6331d7103f8bef29ef93b3cca95fa766e293147fe7bacddf310d5917", size = 1059937, upload-time = "2025-08-08T23:57:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/41/54/3739d35b9f94cb8dc7b0db2edca7192d5571606aa2369a664fa27e811804/tiktoken-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b4ddb1849e6bf0afa6cc1c5d809fb980ca240a5fffe585a04e119519758788c0", size = 999230, upload-time = "2025-08-08T23:57:30.241Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/ec8d43338d28d53513004ebf4cd83732a135d11011433c58bf045890cc10/tiktoken-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10331d08b5ecf7a780b4fe4d0281328b23ab22cdb4ff65e68d56caeda9940ecc", size = 1130076, upload-time = "2025-08-08T23:57:31.706Z" }, + { url = "https://files.pythonhosted.org/packages/94/80/fb0ada0a882cb453caf519a4bf0d117c2a3ee2e852c88775abff5413c176/tiktoken-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b062c82300341dc87e0258c69f79bed725f87e753c21887aea90d272816be882", size = 1183942, upload-time = "2025-08-08T23:57:33.142Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e9/6c104355b463601719582823f3ea658bc3aa7c73d1b3b7553ebdc48468ce/tiktoken-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:195d84bec46169af3b1349a1495c151d37a0ff4cba73fd08282736be7f92cc6c", size = 1244705, upload-time = "2025-08-08T23:57:34.594Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/eaa6068f47e8b3f0aab9e05177cce2cf5aa2cc0ca93981792e620d4d4117/tiktoken-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe91581b0ecdd8783ce8cb6e3178f2260a3912e8724d2f2d49552b98714641a1", size = 884152, upload-time = "2025-08-08T23:57:36.18Z" }, + { url = "https://files.pythonhosted.org/packages/8a/91/912b459799a025d2842566fe1e902f7f50d54a1ce8a0f236ab36b5bd5846/tiktoken-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ae374c46afadad0f501046db3da1b36cd4dfbfa52af23c998773682446097cf", size = 1059743, upload-time = "2025-08-08T23:57:37.516Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/6faa6870489ce64f5f75dcf91512bf35af5864583aee8fcb0dcb593121f5/tiktoken-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25a512ff25dc6c85b58f5dd4f3d8c674dc05f96b02d66cdacf628d26a4e4866b", size = 999334, upload-time = "2025-08-08T23:57:38.595Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3e/a05d1547cf7db9dc75d1461cfa7b556a3b48e0516ec29dfc81d984a145f6/tiktoken-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2130127471e293d385179c1f3f9cd445070c0772be73cdafb7cec9a3684c0458", size = 1129402, upload-time = "2025-08-08T23:57:39.627Z" }, + { url = "https://files.pythonhosted.org/packages/34/9a/db7a86b829e05a01fd4daa492086f708e0a8b53952e1dbc9d380d2b03677/tiktoken-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e43022bf2c33f733ea9b54f6a3f6b4354b909f5a73388fb1b9347ca54a069c", size = 1184046, upload-time = "2025-08-08T23:57:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/bb/52edc8e078cf062ed749248f1454e9e5cfd09979baadb830b3940e522015/tiktoken-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:adb4e308eb64380dc70fa30493e21c93475eaa11669dea313b6bbf8210bfd013", size = 1244691, upload-time = "2025-08-08T23:57:42.251Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/884b6cd7ae2570ecdcaffa02b528522b18fef1cbbfdbcaa73799807d0d3b/tiktoken-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:ece6b76bfeeb61a125c44bbefdfccc279b5288e6007fbedc0d32bfec602df2f2", size = 884392, upload-time = "2025-08-08T23:57:43.628Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9e/eceddeffc169fc75fe0fd4f38471309f11cb1906f9b8aa39be4f5817df65/tiktoken-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fd9e6b23e860973cf9526544e220b223c60badf5b62e80a33509d6d40e6c8f5d", size = 1055199, upload-time = "2025-08-08T23:57:45.076Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/5f02bfefffdc6b54e5094d2897bc80efd43050e5b09b576fd85936ee54bf/tiktoken-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76d53cee2da71ee2731c9caa747398762bda19d7f92665e882fef229cb0b5b", size = 996655, upload-time = "2025-08-08T23:57:46.304Z" }, + { url = "https://files.pythonhosted.org/packages/65/8e/c769b45ef379bc360c9978c4f6914c79fd432400a6733a8afc7ed7b0726a/tiktoken-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef72aab3ea240646e642413cb363b73869fed4e604dcfd69eec63dc54d603e8", size = 1128867, upload-time = "2025-08-08T23:57:47.438Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2d/4d77f6feb9292bfdd23d5813e442b3bba883f42d0ac78ef5fdc56873f756/tiktoken-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f929255c705efec7a28bf515e29dc74220b2f07544a8c81b8d69e8efc4578bd", size = 1183308, upload-time = "2025-08-08T23:57:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/7a/65/7ff0a65d3bb0fc5a1fb6cc71b03e0f6e71a68c5eea230d1ff1ba3fd6df49/tiktoken-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61f1d15822e4404953d499fd1dcc62817a12ae9fb1e4898033ec8fe3915fdf8e", size = 1244301, upload-time = "2025-08-08T23:57:49.642Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6e/5b71578799b72e5bdcef206a214c3ce860d999d579a3b56e74a6c8989ee2/tiktoken-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:45927a71ab6643dfd3ef57d515a5db3d199137adf551f66453be098502838b0f", size = 884282, upload-time = "2025-08-08T23:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/a9034bcee638716d9310443818d73c6387a6a96db93cbcb0819b77f5b206/tiktoken-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a5f3f25ffb152ee7fec78e90a5e5ea5b03b4ea240beed03305615847f7a6ace2", size = 1055339, upload-time = "2025-08-08T23:57:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/f1/91/9922b345f611b4e92581f234e64e9661e1c524875c8eadd513c4b2088472/tiktoken-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dc6e9ad16a2a75b4c4be7208055a1f707c9510541d94d9cc31f7fbdc8db41d8", size = 997080, upload-time = "2025-08-08T23:57:53.442Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9d/49cd047c71336bc4b4af460ac213ec1c457da67712bde59b892e84f1859f/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a0517634d67a8a48fd4a4ad73930c3022629a85a217d256a6e9b8b47439d1e4", size = 1128501, upload-time = "2025-08-08T23:57:54.808Z" }, + { url = "https://files.pythonhosted.org/packages/52/d5/a0dcdb40dd2ea357e83cb36258967f0ae96f5dd40c722d6e382ceee6bba9/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fb4effe60574675118b73c6fbfd3b5868e5d7a1f570d6cc0d18724b09ecf318", size = 1182743, upload-time = "2025-08-08T23:57:56.307Z" }, + { url = "https://files.pythonhosted.org/packages/3b/17/a0fc51aefb66b7b5261ca1314afa83df0106b033f783f9a7bcbe8e741494/tiktoken-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94f984c9831fd32688aef4348803b0905d4ae9c432303087bae370dc1381a2b8", size = 1244057, upload-time = "2025-08-08T23:57:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/50/79/bcf350609f3a10f09fe4fc207f132085e497fdd3612f3925ab24d86a0ca0/tiktoken-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2177ffda31dec4023356a441793fed82f7af5291120751dee4d696414f54db0c", size = 883901, upload-time = "2025-08-08T23:57:59.359Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "torch" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/b7/53fe0436586716ab7aecff41e26b9302d57c85ded481fd83a2cd741e6b4e/torch-2.12.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1834bd984f8a2f4f16bdfbeecca9146184b220aa46276bf5756735b5dae12812", size = 87981887, upload-time = "2026-05-13T14:55:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/34/60/d930eac44c30de06ed16f6d1ba4e785e1632532b50d8f0bf9bf699a4d0c7/torch-2.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d4d029801cb7b6df858804a2a21b00cc2aa0bf0ee5d2ab18d343c9e9e5681f35", size = 426355000, upload-time = "2026-05-13T14:54:31.944Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0c/c76b6a087820bab55705b94dfc074e520de9ae91f5ef90da2ecbf2a3ef12/torch-2.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d47e7dee68ac4cd7a068b26bcd6b989935427709fae1c8f7bd0019978f829e15", size = 532144998, upload-time = "2026-05-13T14:56:05.523Z" }, + { url = "https://files.pythonhosted.org/packages/4a/64/8a0d036e166a6aa85ee09bef072f3655d1ba5d5486a68d1b03b6813c01b3/torch-2.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:cf9839790285dd472e7a16aafcb4a4e6bf58ec1b494045044b0eefb0eb4bd1f2", size = 122949877, upload-time = "2026-05-13T14:55:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/131124fb95df03811b8260d1d43dcc5ee85ea1a344b964613d7efe77fb08/torch-2.12.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:10802fd383bbfed646212e765a72c37d2185205d4f26eb197a254e8ac7ddcb25", size = 87990344, upload-time = "2026-05-13T14:55:42.154Z" }, + { url = "https://files.pythonhosted.org/packages/12/9c/dda0dbd547dc549839824135f223792fd0e725f28ed0715dda366b7acaa2/torch-2.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c12592630aef72feaf18bd3f197ef587bbfa21131b31c38b23ab2e55fce92e36", size = 426362932, upload-time = "2026-05-13T14:54:15.295Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d2/a7dd5a3f9bdaa7842124e8e2359202b317c48d47d2fc5816fafdf2049adb/torch-2.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:415c1b8d0412f67551c8e89a2daca0fb3e56694af0281ba155eaa9da481f58b4", size = 532170085, upload-time = "2026-05-13T14:55:20.788Z" }, + { url = "https://files.pythonhosted.org/packages/12/1b/a61ce2004f9ab0ea8964a6e6168133a127795667639e2ff4f8f2bdb16a65/torch-2.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd37188ea325042cb1f6cafa56822b11ada2520c04791a52629b0af25bdfbfd9", size = 122953128, upload-time = "2026-05-13T14:54:52.744Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/285d643f254731294c9b595a007eac39db4600a98682d7bca688f42ca164/torch-2.12.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b41339df93d491435e790ff8bcbae1c0ce777175889bfd1281d119862793e6a2", size = 88010197, upload-time = "2026-05-13T14:55:35.414Z" }, + { url = "https://files.pythonhosted.org/packages/79/81/76debf1db1343bd929bbb5d74c89fb437c2ed88eb144712557e7bd3eea45/torch-2.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8fbef9f108a863e7722a73740998967e3b074742a834fc5be3a535a2befa7057", size = 426376751, upload-time = "2026-05-13T14:55:03.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/80026028b603c4650ff270fc3785bdef4bd6738765a9cc5a0f5a637d65a2/torch-2.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4f64c2c2b11f7510d93dd6412b87025ff6eddd6bb61c3b5a3d892ea20c4756", size = 532261691, upload-time = "2026-05-13T14:52:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c2/64b06cbb7830fb3cd9be13e1158b31a3f36b68e6a209105ee3c9d9480be0/torch-2.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b958caff4a14d3a3b0b2dfc6a378f64dda9728a9dad28c08a0db9ce4dafb549", size = 122988114, upload-time = "2026-05-13T14:54:42.153Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/01896c80ba921676aa45886b2c5b8d774912de2a1f719de48169c6f755cd/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", size = 88009511, upload-time = "2026-05-13T14:54:47.411Z" }, + { url = "https://files.pythonhosted.org/packages/a5/04/52bdaf4787eab6ac7d7f5851dff934e4def0bc8ead9c8fd2b69b3e529699/torch-2.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:864392c73b7654f4d2b3ae712f607937d0dbb1101c4555fbb41848106b297f39", size = 426383231, upload-time = "2026-05-13T14:53:32.129Z" }, + { url = "https://files.pythonhosted.org/packages/49/8a/94bdecd13f5aaa90d45920b89789d9fe7c6f4af8c3cdd7ce01fcb59908fc/torch-2.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5d6b560dfa7d56291c07d615c3bb73e8d9943d9b6d87f76cd0d9d570c4797fa6", size = 532269288, upload-time = "2026-05-13T14:53:49.423Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2f/bdbaaa267de519ef1b73054bf590d8c93c37a266c9a4e24a01bd38b6918f/torch-2.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:3fee918902090ade827643e758e98363278815de583c75d111fdd665ebffde9f", size = 122987706, upload-time = "2026-05-13T14:54:00.335Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ad/e95e822f3538171e22640a7fbe839a1fdb666600bf6487025de2ff03b11a/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", size = 88319556, upload-time = "2026-05-13T14:54:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/055d06d985b445d67422d25b033c11cf55bbb81785d4c4e68e28bca5820e/torch-2.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af68dbf403439cae9ceaeaaf92f8352b460787dcd27b92aa05c40dd4a19c0f1e", size = 426397656, upload-time = "2026-05-13T14:52:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/43/94/b0b4fdc3014122e0a7302fb90086d352aa48f2576f0b252561ebb38c01a8/torch-2.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a6a2eebb237d3b1d9ad3b378e86d9b9e0782afdea8b1e0eba6a13646b9b49c07", size = 532183124, upload-time = "2026-05-13T14:53:16.178Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c8/052405e6ad05d3237bfe5a4df78f917773956f8e17813a2d44c059068b74/torch-2.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2140e373e9a51a3e22ef62e8d14366d0b470d18f0adf19fdc757368077133a34", size = 123232462, upload-time = "2026-05-13T14:52:27.26Z" }, + { url = "https://files.pythonhosted.org/packages/67/dc/ac069f8d6e8be701535921141055293b0d4819d3d7f224a4612cf157c7f9/torch-2.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7dfae4a519197dfa050e98d8e36378a0fb5899625a875c2b54445005a2e404e", size = 88027282, upload-time = "2026-05-13T14:53:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/1c1eb00e34555b536dddf792676026a988d710ed36981aa00499b36b0620/torch-2.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:891c769072637c74e9a5a77a3bc782894696d8ffec83b938df8536dee7f0ba78", size = 426386961, upload-time = "2026-05-13T14:51:28.406Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d4/7e730dba0c7032a4154dc9056b76cf9625515e030e269cfbf8098fcfee7d/torch-2.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e2ad3eb85d39c3cab62dfa93ed5a73516e6a53c6713cb97d004004fe089f0f1f", size = 532272265, upload-time = "2026-05-13T14:51:59.308Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b4/92c80d1bbfee1c0036c06d1d2155a3065bd2423134c83bf8a47e65cd6b9b/torch-2.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:c66696857e987efb8bc1777a37357ec4f60ab5e8af6250b83d6034437fa2d8f3", size = 122987138, upload-time = "2026-05-13T14:51:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/2e12b37ce50a19a037d7bc62d652a5a8f27385a7b05859d6bc9204f20cfe/torch-2.12.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b4556715c8572758625d62b6e0ae3b1f76c440221913a6fb5e100f321fb4fb02", size = 88320100, upload-time = "2026-05-13T14:51:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/83c450ec7b0bb40a7b74611c1b5440f9260e33c54c90d556fd4a1f0fd955/torch-2.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a43ac605a5e13116c72b64c359644cce0229f213dde48d2ae0ae5eb5becf7feb", size = 426391871, upload-time = "2026-05-13T14:52:14.989Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e9/1a0b575d98d0afedd8f157d23fa3d2759421483660448e60d0a4b10b6daa/torch-2.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a7512adfdd7f6732e40de1c620831e3c75b39b98cef60b11d0c5f0a76473ec5", size = 532192241, upload-time = "2026-05-13T14:51:07.795Z" }, + { url = "https://files.pythonhosted.org/packages/88/21/afadd25ecd81b3cea1e11c73cf1ab41a983a50271548c3ec7ec3b9efc3e9/torch-2.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f96b63f8287f66a005dd1b5a6abba2920f11156c5e5c4d815f3e2050fd1aa16", size = 123231092, upload-time = "2026-05-13T14:51:18.854Z" }, +] + +[[package]] +name = "torchvision" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/15/2df874db140bbfe42f377e05e2dd38f2b9dc88414a6607eecc42073b2baa/torchvision-0.27.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0822b58d2c5d325cd0c7152b744acbd15f898c07572e2cfb70b075a865a4f6f9", size = 1758817, upload-time = "2026-05-13T14:57:20.113Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/10b1ff4087d35b7af7bd85ccb85fbc2573c6f1c2008cf8abfcaf605a10fc/torchvision-0.27.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c9f44e35e6ec01caedacce9e941a5bf21fe424403321efac2507a201273653c5", size = 7830083, upload-time = "2026-05-13T14:57:18.336Z" }, + { url = "https://files.pythonhosted.org/packages/57/20/97dca91770235028ba7e9c598ca1fc48c297f1843af8102430f2adcd4335/torchvision-0.27.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:419c98a9275b27660cdce6d09080fd5974d1ec1d4a225f71439ebacb3b0c4e64", size = 7573816, upload-time = "2026-05-13T14:57:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/37/a5/66fbf7f21f292d095a153ee142806646813e2055a69efe5854c28e7c3fb9/torchvision-0.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:2664d06acd64d328aa7689b0d0c81ee31e240e9977d8768816b4be7c66c03211", size = 3435489, upload-time = "2026-05-13T14:57:13.716Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d6/a7e71e981042d5c573e2e61891b9023b190c88adb75b18bed8594371250c/torchvision-0.27.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:df0c166b6bdf7c47f88e81e8b43bc085451d5c50d0c5d1691bc474c1227d6fed", size = 1758812, upload-time = "2026-05-13T14:57:16.662Z" }, + { url = "https://files.pythonhosted.org/packages/93/f9/f542fb7e4476603fb237ebdc64369a7d11f18eb5a129aa2559cbdb710aee/torchvision-0.27.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9bb9251f64b854124efed95d02953a89f7e2726c3ca662d7ea0151129157297f", size = 7831148, upload-time = "2026-05-13T14:57:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/f6/61/7aa7cc2c9e8750027f6fb9ae3a7393ef43860bcdfe3966e2f71fee800e31/torchvision-0.27.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f44453f107c296d5446a79f7ac59733ad8bf5ddfa04c53805dfbae298a42a798", size = 7575519, upload-time = "2026-05-13T14:56:50.552Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/929b358b1a643849b81ec95569938044cc37dc65ab10c84eb6d82fe1bfbb/torchvision-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:b4aacff70ea4b7377f996f9048989c850d221fef33658ddbcae42aa5bd4ca11a", size = 3749475, upload-time = "2026-05-13T14:57:11.007Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c8/5cd91932f7f3671b0743dc4ae1a4c16b1d0b45bf4087976277d325bda718/torchvision-0.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1a6dd742a150645126df9e0b2e449874c1d635897c773b322c2e067e98382dfe", size = 1758824, upload-time = "2026-05-13T14:57:15.227Z" }, + { url = "https://files.pythonhosted.org/packages/d9/36/7fb7d19477b3d93283b52fea11fa8ee30ab9064a08c97b4a6b91445e26cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65772ff3ec4f4f5d680e30019835555dd239e7fefee4b0a846375fe1cb1592ef", size = 7831034, upload-time = "2026-05-13T14:57:06.483Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/dfd894c3f8b01b5b33fde990f0159c1926ebc7b6e2c4193e2efb7da3c4cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7a9966a088d06b4cf6c610e03be62de469efa6f2cd2e7c7eed8e925ed6af59ac", size = 7579774, upload-time = "2026-05-13T14:56:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0c/722e989f9cf026e97ef7cb24a9bb1859e099f72d247ae35388fb89729f73/torchvision-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c037709072ca9b19750c0cbe9e8bb6f91c9a1be1befa26df33e281deccbd8c7", size = 4021073, upload-time = "2026-05-13T14:57:00.848Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/36547812e6e047c1d80bcacd1b17a340612b08a6e876e0aabf3d0b9228b0/torchvision-0.27.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:41d6dae73e1af09fa82ded597ae57f2a2314285acde54b25890a8f8e51b999d7", size = 1758826, upload-time = "2026-05-13T14:57:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/32c4ea842738728a14e3df8c576c62dedcf5ae5cb6a5c984c6429ebe7524/torchvision-0.27.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:70f071c6f74b60d5fe8851636d8d4cd5f4fa29d57fd9348a87a6f17b990b95ba", size = 7789501, upload-time = "2026-05-13T14:56:57.786Z" }, + { url = "https://files.pythonhosted.org/packages/f6/24/4d0d48684251bd0673f87d633d5d88ab00227983b00591156eed2f86c8d5/torchvision-0.27.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aaafa6962c9d91f42503de1957d6fa349907d028c06f335bd95da7a5bc57147d", size = 7579868, upload-time = "2026-05-13T14:56:41.618Z" }, + { url = "https://files.pythonhosted.org/packages/ba/da/e6edd051d2ba25adf23b120fa97f458dff888d098c51e84724f17d2d1470/torchvision-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:aee384a2782c89517c4ab9061d2720ba59fd2ffe5ef89d0a149cc2d43abdf521", size = 4092700, upload-time = "2026-05-13T14:57:09.729Z" }, + { url = "https://files.pythonhosted.org/packages/fa/23/95dfa40431360f42ca949bf861434bed51164adfa8fb9801e05bf3194f50/torchvision-0.27.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:c5121f1b9ab09a7f73e837871deb8321551f7eaeb19d87aa00de9191968eae44", size = 1845008, upload-time = "2026-05-13T14:57:03.768Z" }, + { url = "https://files.pythonhosted.org/packages/23/b9/9dbdf76b2b49a75ba8088df6f7c755bdb520afb6c6dbac0102b46cde5e99/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:1c01f0d1091ae22b9dfc082b0a0fe5faaf053686a29b4fb082ba7691375c73cf", size = 7791430, upload-time = "2026-05-13T14:56:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6a/e4a16cf2f3310c2ea7760dc5d9054496844391e0f4c1fae87fefac2f3d9e/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dadea3c5ecfd05bbb2a3312ab0374f213c58bf6459cb059122e2f4dfe13d10ed", size = 7668441, upload-time = "2026-05-13T14:57:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/01b6461117a6a94b5af3f8ee166bb0f045056f3cf187750c110dabfdfffa/torchvision-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a49e55055a39a8506fe7e59850522cab004efb2c3839f6057658889c1d69c815", size = 4141602, upload-time = "2026-05-13T14:56:53.449Z" }, + { url = "https://files.pythonhosted.org/packages/92/22/c0633677b3b3f3e69554a21ac087bf705f829c40cd5e3783507b8c006681/torchvision-0.27.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:c1fac0fc2a7adf29481fc1938a0e7845c57ba1147a986784109c4d98f434ea8c", size = 1758818, upload-time = "2026-05-13T14:56:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/48/e8/55f9d9667b56dae470e69e31beac9b00d458ea393feec1aae95cc4f3f1c9/torchvision-0.27.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:cbf89764fc76f3f17fbf80c12d5a89c691e91cb9d82c38412aaf0568655ffb19", size = 7789667, upload-time = "2026-05-13T14:56:48.858Z" }, + { url = "https://files.pythonhosted.org/packages/00/bc/6f8681daf3bbc4c315bb0005110f99d28e3ecd675bf9c8f2c0d393fbac7a/torchvision-0.27.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:91f61b9865423037c327eb56afa207cc72de874e458c361840db9dcf5ce0c0eb", size = 7579848, upload-time = "2026-05-13T14:56:38.209Z" }, + { url = "https://files.pythonhosted.org/packages/19/6c/8d8020e6bd1e46c53e487c9c4e9457a07f2ee28931028fb5d71e2da40adc/torchvision-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:5bb82fc3c55daf1788621e504310b0a286f1069627a8742f692aebb075ef25a7", size = 4119284, upload-time = "2026-05-13T14:56:46.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/7e/e78c48662a8d551606efdbe11c6b9c1d6d2391b92cd0e4591b9e6a2412b8/torchvision-0.27.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2c4099a15150143b9b034730b404a56d572efe0b79489b4c765d929cb4eac7f3", size = 1758828, upload-time = "2026-05-13T14:56:52.293Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/d03ee9f9ee7bf11a8c7c776fb8e7fd6102f59c013791a2a4e5175bd6cba7/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b4c6bb0a670dcba017b3643e21902c9b8a1cc1c127d602f1488fa29ec3c6e865", size = 7790618, upload-time = "2026-05-13T14:56:44.721Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/4002336a74742be70728603ec1769feb2b55e0d19c532c9ec9f92008de76/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1c2db4bde82bc48ebff73436a6adf34d4f809448268a70d9a1285f5c8f92313d", size = 7580217, upload-time = "2026-05-13T14:56:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cb/4dd4783eb3565f526ba6e64b6f6ca26c00eacc924cdfe60455db9d91b84b/torchvision-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:72bf547e58ddb948689734eed6f4b6a2031f979dba4fb08e3690688b392e929f", size = 4226392, upload-time = "2026-05-13T14:56:40.235Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, + { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "transformers" +version = "4.57.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, +] + +[[package]] +name = "tree-sitter" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/2b/02a642e67605b9dd59986b00d13a076044dede04025a243f0592ac79d68c/tree-sitter-0.25.1.tar.gz", hash = "sha256:cd761ad0e4d1fc88a4b1b8083bae06d4f973acf6f5f29bbf13ea9609c1dec9c1", size = 177874, upload-time = "2025-08-05T17:14:34.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/6c/6160ca15926d11a6957d8bee887f477f3c1d9bc5272c863affc0b50b9cff/tree_sitter-0.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a15d62ffdb095d509bda8c140c1ddd0cc80f0c67f92b87fcc96cd242dc0c71ea", size = 146692, upload-time = "2025-08-05T17:13:54.559Z" }, + { url = "https://files.pythonhosted.org/packages/81/4a/e5eb39fe73a514a13bf94acee97925de296d673dace00557763cbbdc938f/tree_sitter-0.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d938f0a1ffad1206a1a569b0501345eeca81cae0a4487bb485e53768b02f24e", size = 141015, upload-time = "2025-08-05T17:13:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/63/22/c8e3ba245e5cdb8c951482028a7ee99d141302047b708dc9d670f0fafd85/tree_sitter-0.25.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba8cea296de5dcb384b9a15cf526985ac8339c81da51c7e29a251d82071f5ee9", size = 599462, upload-time = "2025-08-05T17:13:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/c2/91/c866c3d278ee86354fd81fd055b5d835c510b0e9af07e1cf7e48e2f946b0/tree_sitter-0.25.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:387fd2bd8657d69e877618dc199c18e2d6fe073b8f5c59e23435f3baee4ee10a", size = 627062, upload-time = "2025-08-05T17:13:58.363Z" }, + { url = "https://files.pythonhosted.org/packages/90/96/ac010f72778dae60381ab5fcca9651ac72647d582db0b027ca6c56116920/tree_sitter-0.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:afa49e51f82b58ae2c1291d6b79ca31e0fb36c04bd9a20d89007472edfb70136", size = 623788, upload-time = "2025-08-05T17:13:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/0e/29/190bdfd54a564a2e43a702884ad5679f4578c481a46161f9f335dd390a70/tree_sitter-0.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:77be45f666adf284914510794b41100decccd71dba88010c03dc2bb0d653acec", size = 127253, upload-time = "2025-08-05T17:14:00.446Z" }, + { url = "https://files.pythonhosted.org/packages/da/60/7daca5ccf65fb204c9f2cc2907db6aeaf1cb42aa605427580c17a38a53b3/tree_sitter-0.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:72badac2de4e81ae0df5efe14ec5003bd4df3e48e7cf84dbd9df3a54599ba371", size = 113930, upload-time = "2025-08-05T17:14:01.623Z" }, + { url = "https://files.pythonhosted.org/packages/17/dc/0dabb75d249108fb9062d6e9e791e4ad8e9ae5c095e06dd8af770bc07902/tree_sitter-0.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33a8fbaeb2b5049cf5318306ab8b16ab365828b2b21ee13678c29e0726a1d27a", size = 146696, upload-time = "2025-08-05T17:14:02.408Z" }, + { url = "https://files.pythonhosted.org/packages/da/d0/b7305a05d65dbcfce7a97a93252bf7384f09800866e9de55a625c76e0257/tree_sitter-0.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:797bbbc686d8d3722d25ee0108ad979bda6ad3e1025859ce2ee290e517816bd4", size = 141014, upload-time = "2025-08-05T17:14:03.58Z" }, + { url = "https://files.pythonhosted.org/packages/84/d0/d0d8bd13c44ef6379499712a3f5e3930e7db11e5c8eb2af8655e288597a3/tree_sitter-0.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629fc2ae3f5954b0f6a7b42ee3fcd8f34b68ea161e9f02fa5bf709cbbac996d3", size = 604339, upload-time = "2025-08-05T17:14:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/13/22869a6da25ffe2dfff922712605e72a9c3481109a93f4218bea1bc65f35/tree_sitter-0.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4257018c42a33a7935a5150d678aac05c6594347d6a6e6dbdf7e2ef4ae985213", size = 631593, upload-time = "2025-08-05T17:14:06.043Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/f4590fc08422768fc57456a85c932888a02e7a13540574859308611be1cf/tree_sitter-0.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4027854c9feee2a3bb99642145ba04ce95d75bd17e292911c93a488cb28d0a04", size = 629265, upload-time = "2025-08-05T17:14:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a8/ee9305ce9a7417715cbf038fdcc4fdb6042e30065c9837bdcf36be440388/tree_sitter-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:183faaedcee5f0a3ba39257fa81749709d5eb7cf92c2c050b36ff38468d1774c", size = 127210, upload-time = "2025-08-05T17:14:08.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/6a39882f534373873ef3dba8a1a8f47dc3bfb39ee63784eac2e789b404c4/tree_sitter-0.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:6a3800235535a2532ce392ed0d8e6f698ee010e73805bdeac2f249da8246bab6", size = 113928, upload-time = "2025-08-05T17:14:09.376Z" }, + { url = "https://files.pythonhosted.org/packages/45/79/6dea0c098879d99f41ba919da1ea46e614fb4bf9c4d591450061aeec6fcb/tree_sitter-0.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9362a202144075b54f7c9f07e0b0e44a61eed7ee19e140c506b9e64c1d21ed58", size = 146928, upload-time = "2025-08-05T17:14:10.522Z" }, + { url = "https://files.pythonhosted.org/packages/15/30/8002f4e76c7834a6101895ff7524ea29ab4f1f1da1270260ef52e2319372/tree_sitter-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:593f22529f34dd04de02f56ea6d7c2c8ec99dfab25b58be893247c1090dedd60", size = 140802, upload-time = "2025-08-05T17:14:11.38Z" }, + { url = "https://files.pythonhosted.org/packages/38/ec/d297ad9d4a4b26f551a5ca49afe48fdbcb20f058c2eff8d8463ad6c0eed1/tree_sitter-0.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb6849f76e1cbfa223303fa680da533d452e378d5fe372598e4752838ca7929", size = 606762, upload-time = "2025-08-05T17:14:12.264Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1c/05a623cfb420b10d5f782d4ec064cf00fbfa9c21b8526ca4fd042f80acff/tree_sitter-0.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:034d4544bb0f82e449033d76dd083b131c3f9ecb5e37d3475f80ae55e8f382bd", size = 634632, upload-time = "2025-08-05T17:14:13.21Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/f05fd5a2331c16d428efb8eef32dfb80dc6565438146e34e9a235ecd7925/tree_sitter-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46a9b721560070f2f980105266e28a17d3149485582cdba14d66dca14692e932", size = 630756, upload-time = "2025-08-05T17:14:14.673Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/79f3c5d53d1721b95ab6cda0368192a4f1d367e3a5ff7ac21d77e9841782/tree_sitter-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:9a5c522b1350a626dc1cbc5dc203133caeaa114d3f65e400445e8b02f18b343b", size = 127157, upload-time = "2025-08-05T17:14:15.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/b7/07c4e3f71af0096db6c2ecd83e7d61584e3891c79cb39b208082312d1d60/tree_sitter-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:43e7b8e83f9fc29ca62e7d2aa8c38e3fa806ff3fc65e0d501d18588dc1509888", size = 113910, upload-time = "2025-08-05T17:14:16.385Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d3/bfb08aab9c7daed2715f303cc017329e3512bb77678cc28829681decadd2/tree_sitter-0.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae1eebc175e6a50b38b0e0385cdc26e92ac0bff9b32ee1c0619bbbf6829d57ea", size = 146920, upload-time = "2025-08-05T17:14:17.483Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/7f897c50489c38665255579646fca8191e1b9e5a29ac9cf11022e42e1e2b/tree_sitter-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e0ae03c4f132f1bffb2bc40b1bb28742785507da693ab04da8531fe534ada9c", size = 140782, upload-time = "2025-08-05T17:14:18.594Z" }, + { url = "https://files.pythonhosted.org/packages/16/e6/85012113899296b8e0789ae94f562d3971d7d3df989e8bec6128749394e1/tree_sitter-0.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acf571758be0a71046a61a0936cb815f15b13e0ae7ec6d08398e4aa1560b371d", size = 607590, upload-time = "2025-08-05T17:14:19.782Z" }, + { url = "https://files.pythonhosted.org/packages/49/93/605b08dc4cf76d08cfacebc30a88467c6526ea5c94592c25240518e38b71/tree_sitter-0.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:632910847e3f8ae35841f92cba88a9a1b8bc56ecc1514a5affebf7951fa0fc0a", size = 635553, upload-time = "2025-08-05T17:14:21.107Z" }, + { url = "https://files.pythonhosted.org/packages/ce/27/123667f756bb32168507c940db9040104c606fbb0214397d3c20cf985073/tree_sitter-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a99ecef7771afb118b2a8435c8ba67ea7a085c60d5d33dc0a4794ed882e5f7df", size = 630844, upload-time = "2025-08-05T17:14:22.078Z" }, + { url = "https://files.pythonhosted.org/packages/2f/53/180b0ed74153a3c9a23967f54774d5930c2e0b67671ae4ca0d4d35ba18ac/tree_sitter-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:c1d6393454d1f9d4195c74e40a487640cd4390cd4aee90837485f932a1a0f40c", size = 127159, upload-time = "2025-08-05T17:14:23.061Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/b8b7b5122ac4a80cd689a5023f2416910e10f9534ace1cdf0020a315d40d/tree_sitter-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:c1d2dbf7d12426b71ff49739f599c355f4de338a5c0ab994de2a1d290f6e0b20", size = 113920, upload-time = "2025-08-05T17:14:23.879Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/cb851da552baf4215baf96443e5e9e39095083a95bc05c4444e640fe0fe8/tree_sitter-0.25.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:32cee52264d9ecf98885fcac0185ac63e16251b31dd8b4a3b8d8071173405f8f", size = 146775, upload-time = "2025-08-05T17:14:25.064Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/002c89df1e8f1664b82023e5d0c06de97fff5c2a2e33dce1a241c8909758/tree_sitter-0.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae024d8ccfef51e61c44a81af7a48670601430701c24f450bea10f4b4effd8d1", size = 140787, upload-time = "2025-08-05T17:14:25.914Z" }, + { url = "https://files.pythonhosted.org/packages/39/48/c9e6deb88f3c7f16963ef205e5b8e3ea7f5effd048b4515d09738c7b032b/tree_sitter-0.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d025c56c393cea660df9ef33ca60329952a1f8ee6212d21b2b390dfec08a3874", size = 609173, upload-time = "2025-08-05T17:14:26.817Z" }, + { url = "https://files.pythonhosted.org/packages/53/a8/b782576d7ea081a87285d974005155da03b6d0c66283fe1e3a5e0dd4bd98/tree_sitter-0.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:044aa23ea14f337809821bea7467f33f4c6d351739dca76ba0cbe4d0154d8662", size = 635994, upload-time = "2025-08-05T17:14:28.343Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/c5b6c9cdb7bd4bf0c3d2bd494fcf356acc53f8e63007dc2a836d95bbe964/tree_sitter-0.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1863d96704eb002df4ad3b738294ae8bd5dcf8cefb715da18bff6cb2d33d978e", size = 630944, upload-time = "2025-08-05T17:14:31.123Z" }, + { url = "https://files.pythonhosted.org/packages/12/2a/d0b097157c2d487f5e6293dae2c106ec9ede792a6bb780249e81432e754d/tree_sitter-0.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:a40a481e28e1afdbc455932d61e49ffd4163aafa83f4a3deb717524a7786197e", size = 130831, upload-time = "2025-08-05T17:14:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ce/33/3591e7b22dd49f46ae4fdee1db316ecefd0486cae880c5b497a55f0ccb24/tree_sitter-0.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:f7b68f584336b39b2deab9896b629dddc3c784170733d3409f01fe825e9c04eb", size = 117376, upload-time = "2025-08-05T17:14:33.283Z" }, +] + +[[package]] +name = "tree-sitter-c-sharp" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/85/a61c782afbb706a47d990eaee6977e7c2bd013771c5bf5c81c617684f286/tree_sitter_c_sharp-0.23.1.tar.gz", hash = "sha256:322e2cfd3a547a840375276b2aea3335fa6458aeac082f6c60fec3f745c967eb", size = 1317728, upload-time = "2024-11-11T05:25:32.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/dc/d4a0ad9e466263728f80f9dac399609473af01c1aba2ea3ea8879ce56276/tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e87be7572991552606a3155d2f6c2045ded8bce94bfd9f74bf521d949c219a1c", size = 333661, upload-time = "2026-04-14T15:11:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/61/7a/5c862770460a2e27079e725585ad2718100373c09448c14e36934ef44414/tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:86c2fdf178c66474a1be2965602818d30780e4e3ed890e3c206931f65d9a154c", size = 376295, upload-time = "2026-04-14T15:11:15.346Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/0571a3a34c0feda60a9c37cf6dd5edfdbc24f8fcb1e48b6b6eb0f324ad2a/tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:035d259e64c41d02cc45afc3b8b46388b232e7d16d84734d851cca7334761da5", size = 358331, upload-time = "2026-04-14T15:11:16.418Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/0f7e1f50f6365338eb700f01710da0adc49a49fa9a8443e5a90ea4f29491/tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa472cb9de7e14fee9408e144f29f68384cd8e9c677dff0002da19f361a59bdf", size = 359444, upload-time = "2026-04-14T15:11:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/129bd56d5ef22b4ae254940a09b6d3ed873093218868a3f9635d571d514e/tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1a0ea86eccff74e85ab4a2cf77c813fad7c84162962ce242dff0c51601028832", size = 358143, upload-time = "2026-04-14T15:11:18.755Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cd/e12cdca47e0c56151cb4b156d48091b7bc1d968e072c1656cf6b73fe7218/tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8ab26dc998bbd4b4287b129f67c10ca715deb402ed77d0645674490ea509097e", size = 357524, upload-time = "2026-04-14T15:11:19.717Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2c/f742d60f818cba83760f4975c7158d1c96c36b5807e95a843db7fb8c64b7/tree_sitter_c_sharp-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:d4486653feaff3314ef45534dcb6f9ea8ab3aa160896287c6473788f88eb38be", size = 338755, upload-time = "2026-04-14T15:11:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e4/8a8642b9bba86248ac2facc81ffb187c06c6768efa56c79d61fab70d736b/tree_sitter_c_sharp-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:e7a14b76ec23cc8386cf662d5ea602d81331376c93ca6299a97b174047790345", size = 337261, upload-time = "2026-04-14T15:11:22.111Z" }, + { url = "https://files.pythonhosted.org/packages/58/04/f6c2df4c53a588ccd88d50851155945cff8cd887bd70c175e00aaade7edf/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2b612a6e5bd17bb7fa2aab4bb6fc1fba45c94f09cb034ab332e45603b86e32fd", size = 372235, upload-time = "2024-11-11T05:25:19.424Z" }, + { url = "https://files.pythonhosted.org/packages/99/10/1aa9486f1e28fc22810fa92cbdc54e1051e7f5536a5e5b5e9695f609b31e/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a8b98f62bc53efcd4d971151950c9b9cd5cbe3bacdb0cd69fdccac63350d83e", size = 419046, upload-time = "2024-11-11T05:25:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/0f/21/13df29f8fcb9ba9f209b7b413a4764b673dfd58989a0dd67e9c7e19e9c2e/tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:986e93d845a438ec3c4416401aa98e6a6f6631d644bbbc2e43fcb915c51d255d", size = 415999, upload-time = "2024-11-11T05:25:22.359Z" }, + { url = "https://files.pythonhosted.org/packages/ca/72/fc6846795bcdae2f8aa94cc8b1d1af33d634e08be63e294ff0d6794b1efc/tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8024e466b2f5611c6dc90321f232d8584893c7fb88b75e4a831992f877616d2", size = 402830, upload-time = "2024-11-11T05:25:24.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3a/b6028c5890ce6653807d5fa88c72232c027c6ceb480dbeb3b186d60e5971/tree_sitter_c_sharp-0.23.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7f9bf876866835492281d336b9e1f9626ab668737f74e914c31d285261507da7", size = 397880, upload-time = "2024-11-11T05:25:25.937Z" }, + { url = "https://files.pythonhosted.org/packages/47/d2/4facaa34b40f8104d8751746d0e1cd2ddf0beb9f1404b736b97f372bd1f3/tree_sitter_c_sharp-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:ae9a9e859e8f44e2b07578d44f9a220d3fa25b688966708af6aa55d42abeebb3", size = 377562, upload-time = "2024-11-11T05:25:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/d8/88/3cf6bd9959d94d1fec1e6a9c530c5f08ff4115a474f62aedb5fedb0f7241/tree_sitter_c_sharp-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:c81548347a93347be4f48cb63ec7d60ef4b0efa91313330e69641e49aa5a08c5", size = 375157, upload-time = "2024-11-11T05:25:30.839Z" }, +] + +[[package]] +name = "tree-sitter-java" +version = "0.23.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/dc/eb9c8f96304e5d8ae1663126d89967a622a80937ad2909903569ccb7ec8f/tree_sitter_java-0.23.5.tar.gz", hash = "sha256:f5cd57b8f1270a7f0438878750d02ccc79421d45cca65ff284f1527e9ef02e38", size = 138121, upload-time = "2024-12-21T18:24:26.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/21/b3399780b440e1567a11d384d0ebb1aea9b642d0d98becf30fa55c0e3a3b/tree_sitter_java-0.23.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:355ce0308672d6f7013ec913dee4a0613666f4cda9044a7824240d17f38209df", size = 58926, upload-time = "2024-12-21T18:24:12.53Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/6406b444e2a93bc72a04e802f4107e9ecf04b8de4a5528830726d210599c/tree_sitter_java-0.23.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:24acd59c4720dedad80d548fe4237e43ef2b7a4e94c8549b0ca6e4c4d7bf6e69", size = 62288, upload-time = "2024-12-21T18:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/74b1c150d4f69c291ab0b78d5dd1b59712559bbe7e7daf6d8466d483463f/tree_sitter_java-0.23.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9401e7271f0b333df39fc8a8336a0caf1b891d9a2b89ddee99fae66b794fc5b7", size = 85533, upload-time = "2024-12-21T18:24:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/29/09/e0d08f5c212062fd046db35c1015a2621c2631bc8b4aae5740d7adb276ad/tree_sitter_java-0.23.5-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370b204b9500b847f6d0c5ad584045831cee69e9a3e4d878535d39e4a7e4c4f1", size = 84033, upload-time = "2024-12-21T18:24:18.758Z" }, + { url = "https://files.pythonhosted.org/packages/43/56/7d06b23ddd09bde816a131aa504ee11a1bbe87c6b62ab9b2ed23849a3382/tree_sitter_java-0.23.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aae84449e330363b55b14a2af0585e4e0dae75eb64ea509b7e5b0e1de536846a", size = 82564, upload-time = "2024-12-21T18:24:20.493Z" }, + { url = "https://files.pythonhosted.org/packages/da/d6/0528c7e1e88a18221dbd8ccee3825bf274b1fa300f745fd74eb343878043/tree_sitter_java-0.23.5-cp39-abi3-win_amd64.whl", hash = "sha256:1ee45e790f8d31d416bc84a09dac2e2c6bc343e89b8a2e1d550513498eedfde7", size = 60650, upload-time = "2024-12-21T18:24:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/72/57/5bab54d23179350356515526fff3cc0f3ac23bfbc1a1d518a15978d4880e/tree_sitter_java-0.23.5-cp39-abi3-win_arm64.whl", hash = "sha256:402efe136104c5603b429dc26c7e75ae14faaca54cfd319ecc41c8f2534750f4", size = 59059, upload-time = "2024-12-21T18:24:24.934Z" }, +] + +[[package]] +name = "tree-sitter-python" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/8b/c992ff0e768cb6768d5c96234579bf8842b3a633db641455d86dd30d5dac/tree_sitter_python-0.25.0.tar.gz", hash = "sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac", size = 159845, upload-time = "2025-09-11T06:47:58.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/64/a4e503c78a4eb3ac46d8e72a29c1b1237fa85238d8e972b063e0751f5a94/tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361", size = 73790, upload-time = "2025-09-11T06:47:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/60d8c2a0cc63d6ec4ba4e99ce61b802d2e39ef9db799bdf2a8f932a6cd4b/tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762", size = 76691, upload-time = "2025-09-11T06:47:49.038Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5", size = 108133, upload-time = "2025-09-11T06:47:50.499Z" }, + { url = "https://files.pythonhosted.org/packages/40/bd/bf4787f57e6b2860f3f1c8c62f045b39fb32d6bac4b53d7a9e66de968440/tree_sitter_python-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b", size = 110603, upload-time = "2025-09-11T06:47:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/25/feff09f5c2f32484fbce15db8b49455c7572346ce61a699a41972dea7318/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d", size = 108998, upload-time = "2025-09-11T06:47:53.046Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/4946da3d6c0df316ccb938316ce007fb565d08f89d02d854f2d308f0309f/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683", size = 107268, upload-time = "2025-09-11T06:47:54.388Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/996fc2dfa1076dc460d3e2f3c75974ea4b8f02f6bc925383aaae519920e8/tree_sitter_python-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76", size = 76073, upload-time = "2025-09-11T06:47:55.773Z" }, + { url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" }, +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/fc/bb52958f7e399250aee093751e9373a6311cadbe76b6e0d109b853757f35/tree_sitter_typescript-0.23.2.tar.gz", hash = "sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d", size = 773053, upload-time = "2024-11-11T02:36:11.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/95/4c00680866280e008e81dd621fd4d3f54aa3dad1b76b857a19da1b2cc426/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478", size = 286677, upload-time = "2024-11-11T02:35:58.839Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2f/1f36fda564518d84593f2740d5905ac127d590baf5c5753cef2a88a89c15/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8", size = 302008, upload-time = "2024-11-11T02:36:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31", size = 351987, upload-time = "2024-11-11T02:36:02.669Z" }, + { url = "https://files.pythonhosted.org/packages/49/d1/a71c36da6e2b8a4ed5e2970819b86ef13ba77ac40d9e333cb17df6a2c5db/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c", size = 344960, upload-time = "2024-11-11T02:36:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/7f/cb/f57b149d7beed1a85b8266d0c60ebe4c46e79c9ba56bc17b898e17daf88e/tree_sitter_typescript-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0", size = 340245, upload-time = "2024-11-11T02:36:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9", size = 278015, upload-time = "2024-11-11T02:36:07.631Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" }, +] + +[[package]] +name = "triton" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/97/dcd1f2a0f8336691bff74abc59b2ed9c69a0c0f8f65cd77109c49e05f068/triton-3.7.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223ac302091491436c248a34ee1e6c47a1026486579103c906ffd805be50cb89", size = 188367104, upload-time = "2026-05-07T19:04:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c0/c2ac4fd2d8809b7579d4a820a0f9e5de62a9bc8a757ed4b3abf4f7ee964a/triton-3.7.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c631b65668d4951213b948a413c0564184305b77bb45cc9d686d3e1ecc4701a3", size = 201313191, upload-time = "2026-05-07T18:45:58.444Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/5d842314bb6c78442cc60437928781701c6050b8d479bc2a1aed691d37ca/triton-3.7.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9e71fc392675fac364e0ecf4ef3f76f85b7f5433a16f4c3c5fe5f05a52c85fe", size = 188480277, upload-time = "2026-05-07T19:05:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/13/31/8315ea5f8dd18e60970b3022e3a8b93fd37e0b784fbbef86e10c8e6e5ca1/triton-3.7.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22bacffce443f54593dd20f05294d5a40622e0ea9ab632816f87154504356221", size = 201415942, upload-time = "2026-05-07T18:46:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104, upload-time = "2026-05-07T19:05:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/468a576e35beef1426e0828e28e9ba9e65f5474d496f16ee126c15646324/triton-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f111161d49bf903c0eaedde3962353a3d841c08a836839b7cc1025b8426efcf", size = 201457567, upload-time = "2026-05-07T18:46:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/a59a583de59b8f62c495d67c80ee3ea97d09e91ac80c4c6e76456ed8d8ac/triton-3.7.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abdf6beaa89b1bcfb9a43cd990536ce66091a997841a4814b260b7bee4c88c3c", size = 188503209, upload-time = "2026-05-07T19:05:17.935Z" }, + { url = "https://files.pythonhosted.org/packages/30/b1/b7507bb9815d403927c8dd51d4158ed2e11751a92dbc118a044f247b6848/triton-3.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a35d7afe3f3f058e7ec49fcce09794049e0ffc5c59019ac25ec3413741b8c4e7", size = 201453566, upload-time = "2026-05-07T18:46:20.427Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/0bea7a6a0c989315c9135a1d7fb37e41905cfb3a17cbc1f10044ebd4cc3a/triton-3.7.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc1d61c172d257db80ddf42595131fb196ad2e9bdd751e90fe2ef13531734e8b", size = 188612899, upload-time = "2026-05-07T19:05:24.955Z" }, + { url = "https://files.pythonhosted.org/packages/e1/02/d96f57828d0912aec733b9bc7e0e7dbfd2c6f079a8fa433ac25cb93d1a30/triton-3.7.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70fb9bbdc9f400afc54bbf6eb2670af28829a6ae3996863317964783141daf56", size = 201553816, upload-time = "2026-05-07T18:46:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/fb/82a802dac4689f2a2fb2e69302e6a138eecc3e175bbe976ba3cfc717683a/triton-3.7.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a44a8476d0d3571eac4e4d1048e1ff75aad81a09ff4602ccfc56c6dea1672e", size = 188507879, upload-time = "2026-05-07T19:05:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/8f/af/9904ec6d3c93d9b24e5ec360445bbdf758b7f00bfbeedb89cb0eb64eb8bb/triton-3.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9b85e72968a9d8bba5ddb24e9b64aaabaf48affb042f2755cb7cfa92b7531ce", size = 201460637, upload-time = "2026-05-07T18:46:34.749Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/4835a8ea746b88727d8899f4e3ccce4f9cacb38abfc3bb0a638266c53111/triton-3.7.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a160de426fd99f92b0baf509045360afbd3bfaa0b4a5171dde800ec9f09684", size = 188608706, upload-time = "2026-05-07T19:05:39.218Z" }, + { url = "https://files.pythonhosted.org/packages/c1/68/fa86e5a39608000f645535b2c124920126327ab731f8c4fafd5b07ff8d4b/triton-3.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce061073102714b725f3660ec6939d94a1da7984b3aa99c921417cae273672f5", size = 201546766, upload-time = "2026-05-07T18:46:42.088Z" }, +] + +[[package]] +name = "typer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "xxhash" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241, upload-time = "2024-08-17T09:20:38.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8a/0e9feca390d512d293afd844d31670e25608c4a901e10202aa98785eab09/xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ece616532c499ee9afbb83078b1b952beffef121d989841f7f4b3dc5ac0fd212", size = 31970, upload-time = "2024-08-17T09:17:35.675Z" }, + { url = "https://files.pythonhosted.org/packages/16/e6/be5aa49580cd064a18200ab78e29b88b1127e1a8c7955eb8ecf81f2626eb/xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3171f693dbc2cef6477054a665dc255d996646b4023fe56cb4db80e26f4cc520", size = 30801, upload-time = "2024-08-17T09:17:37.353Z" }, + { url = "https://files.pythonhosted.org/packages/20/ee/b8a99ebbc6d1113b3a3f09e747fa318c3cde5b04bd9c197688fadf0eeae8/xxhash-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5d3e570ef46adaf93fc81b44aca6002b5a4d8ca11bd0580c07eac537f36680", size = 220927, upload-time = "2024-08-17T09:17:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/58/62/15d10582ef159283a5c2b47f6d799fc3303fe3911d5bb0bcc820e1ef7ff4/xxhash-3.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb29a034301e2982df8b1fe6328a84f4b676106a13e9135a0d7e0c3e9f806da", size = 200360, upload-time = "2024-08-17T09:17:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/23/41/61202663ea9b1bd8e53673b8ec9e2619989353dba8cfb68e59a9cbd9ffe3/xxhash-3.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0d307d27099bb0cbeea7260eb39ed4fdb99c5542e21e94bb6fd29e49c57a23", size = 428528, upload-time = "2024-08-17T09:17:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/f2/07/d9a3059f702dec5b3b703737afb6dda32f304f6e9da181a229dafd052c29/xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0342aafd421795d740e514bc9858ebddfc705a75a8c5046ac56d85fe97bf196", size = 194149, upload-time = "2024-08-17T09:17:44.361Z" }, + { url = "https://files.pythonhosted.org/packages/eb/58/27caadf78226ecf1d62dbd0c01d152ed381c14c1ee4ad01f0d460fc40eac/xxhash-3.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dbbd9892c5ebffeca1ed620cf0ade13eb55a0d8c84e0751a6653adc6ac40d0c", size = 207703, upload-time = "2024-08-17T09:17:46.656Z" }, + { url = "https://files.pythonhosted.org/packages/b1/08/32d558ce23e1e068453c39aed7b3c1cdc690c177873ec0ca3a90d5808765/xxhash-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4cc2d67fdb4d057730c75a64c5923abfa17775ae234a71b0200346bfb0a7f482", size = 216255, upload-time = "2024-08-17T09:17:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d4/2b971e2d2b0a61045f842b622ef11e94096cf1f12cd448b6fd426e80e0e2/xxhash-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ec28adb204b759306a3d64358a5e5c07d7b1dd0ccbce04aa76cb9377b7b70296", size = 202744, upload-time = "2024-08-17T09:17:50.045Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/6a6438864a8c4c39915d7b65effd85392ebe22710412902487e51769146d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1328f6d8cca2b86acb14104e381225a3d7b42c92c4b86ceae814e5c400dbb415", size = 210115, upload-time = "2024-08-17T09:17:51.834Z" }, + { url = "https://files.pythonhosted.org/packages/48/7d/b3c27c27d1fc868094d02fe4498ccce8cec9fcc591825c01d6bcb0b4fc49/xxhash-3.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8d47ebd9f5d9607fd039c1fbf4994e3b071ea23eff42f4ecef246ab2b7334198", size = 414247, upload-time = "2024-08-17T09:17:53.094Z" }, + { url = "https://files.pythonhosted.org/packages/a1/05/918f9e7d2fbbd334b829997045d341d6239b563c44e683b9a7ef8fe50f5d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b96d559e0fcddd3343c510a0fe2b127fbff16bf346dd76280b82292567523442", size = 191419, upload-time = "2024-08-17T09:17:54.906Z" }, + { url = "https://files.pythonhosted.org/packages/08/29/dfe393805b2f86bfc47c290b275f0b7c189dc2f4e136fd4754f32eb18a8d/xxhash-3.5.0-cp310-cp310-win32.whl", hash = "sha256:61c722ed8d49ac9bc26c7071eeaa1f6ff24053d553146d5df031802deffd03da", size = 30114, upload-time = "2024-08-17T09:17:56.566Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d7/aa0b22c4ebb7c3ccb993d4c565132abc641cd11164f8952d89eb6a501909/xxhash-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bed5144c6923cc902cd14bb8963f2d5e034def4486ab0bbe1f58f03f042f9a9", size = 30003, upload-time = "2024-08-17T09:17:57.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/12/f969b81541ee91b55f1ce469d7ab55079593c80d04fd01691b550e535000/xxhash-3.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:893074d651cf25c1cc14e3bea4fceefd67f2921b1bb8e40fcfeba56820de80c6", size = 26773, upload-time = "2024-08-17T09:17:59.169Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/afed0f131fbda960ff15eee7f304fa0eeb2d58770fade99897984852ef23/xxhash-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02c2e816896dc6f85922ced60097bcf6f008dedfc5073dcba32f9c8dd786f3c1", size = 31969, upload-time = "2024-08-17T09:18:00.852Z" }, + { url = "https://files.pythonhosted.org/packages/8c/0c/7c3bc6d87e5235672fcc2fb42fd5ad79fe1033925f71bf549ee068c7d1ca/xxhash-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6027dcd885e21581e46d3c7f682cfb2b870942feeed58a21c29583512c3f09f8", size = 30800, upload-time = "2024-08-17T09:18:01.863Z" }, + { url = "https://files.pythonhosted.org/packages/04/9e/01067981d98069eec1c20201f8c145367698e9056f8bc295346e4ea32dd1/xxhash-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1308fa542bbdbf2fa85e9e66b1077eea3a88bef38ee8a06270b4298a7a62a166", size = 221566, upload-time = "2024-08-17T09:18:03.461Z" }, + { url = "https://files.pythonhosted.org/packages/d4/09/d4996de4059c3ce5342b6e1e6a77c9d6c91acce31f6ed979891872dd162b/xxhash-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28b2fdcee797e1c1961cd3bcd3d545cab22ad202c846235197935e1df2f8ef7", size = 201214, upload-time = "2024-08-17T09:18:05.616Z" }, + { url = "https://files.pythonhosted.org/packages/62/f5/6d2dc9f8d55a7ce0f5e7bfef916e67536f01b85d32a9fbf137d4cadbee38/xxhash-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:924361811732ddad75ff23e90efd9ccfda4f664132feecb90895bade6a1b4623", size = 429433, upload-time = "2024-08-17T09:18:06.957Z" }, + { url = "https://files.pythonhosted.org/packages/d9/72/9256303f10e41ab004799a4aa74b80b3c5977d6383ae4550548b24bd1971/xxhash-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89997aa1c4b6a5b1e5b588979d1da048a3c6f15e55c11d117a56b75c84531f5a", size = 194822, upload-time = "2024-08-17T09:18:08.331Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/1a3a29acd08248a34b0e6a94f4e0ed9b8379a4ff471f1668e4dce7bdbaa8/xxhash-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c4f4e8c59837de103344eb1c8a3851f670309eb5c361f746805c5471b8c88", size = 208538, upload-time = "2024-08-17T09:18:10.332Z" }, + { url = "https://files.pythonhosted.org/packages/53/ad/7fa1a109663366de42f724a1cdb8e796a260dbac45047bce153bc1e18abf/xxhash-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbd2ecfbfee70bc1a4acb7461fa6af7748ec2ab08ac0fa298f281c51518f982c", size = 216953, upload-time = "2024-08-17T09:18:11.707Z" }, + { url = "https://files.pythonhosted.org/packages/35/02/137300e24203bf2b2a49b48ce898ecce6fd01789c0fcd9c686c0a002d129/xxhash-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25b5a51dc3dfb20a10833c8eee25903fd2e14059e9afcd329c9da20609a307b2", size = 203594, upload-time = "2024-08-17T09:18:13.799Z" }, + { url = "https://files.pythonhosted.org/packages/23/03/aeceb273933d7eee248c4322b98b8e971f06cc3880e5f7602c94e5578af5/xxhash-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a8fb786fb754ef6ff8c120cb96629fb518f8eb5a61a16aac3a979a9dbd40a084", size = 210971, upload-time = "2024-08-17T09:18:15.824Z" }, + { url = "https://files.pythonhosted.org/packages/e3/64/ed82ec09489474cbb35c716b189ddc1521d8b3de12b1b5ab41ce7f70253c/xxhash-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a905ad00ad1e1c34fe4e9d7c1d949ab09c6fa90c919860c1534ff479f40fd12d", size = 415050, upload-time = "2024-08-17T09:18:17.142Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/6db4c02dcb488ad4e03bc86d70506c3d40a384ee73c9b5c93338eb1f3c23/xxhash-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:963be41bcd49f53af6d795f65c0da9b4cc518c0dd9c47145c98f61cb464f4839", size = 192216, upload-time = "2024-08-17T09:18:18.779Z" }, + { url = "https://files.pythonhosted.org/packages/22/6d/db4abec29e7a567455344433d095fdb39c97db6955bb4a2c432e486b4d28/xxhash-3.5.0-cp311-cp311-win32.whl", hash = "sha256:109b436096d0a2dd039c355fa3414160ec4d843dfecc64a14077332a00aeb7da", size = 30120, upload-time = "2024-08-17T09:18:20.009Z" }, + { url = "https://files.pythonhosted.org/packages/52/1c/fa3b61c0cf03e1da4767213672efe186b1dfa4fc901a4a694fb184a513d1/xxhash-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b702f806693201ad6c0a05ddbbe4c8f359626d0b3305f766077d51388a6bac58", size = 30003, upload-time = "2024-08-17T09:18:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/9e6fc572acf6e1cc7ccb01973c213f895cb8668a9d4c2b58a99350da14b7/xxhash-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:c4dcb4120d0cc3cc448624147dba64e9021b278c63e34a38789b688fd0da9bf3", size = 26777, upload-time = "2024-08-17T09:18:22.809Z" }, + { url = "https://files.pythonhosted.org/packages/07/0e/1bfce2502c57d7e2e787600b31c83535af83746885aa1a5f153d8c8059d6/xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00", size = 31969, upload-time = "2024-08-17T09:18:24.025Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d6/8ca450d6fe5b71ce521b4e5db69622383d039e2b253e9b2f24f93265b52c/xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9", size = 30787, upload-time = "2024-08-17T09:18:25.318Z" }, + { url = "https://files.pythonhosted.org/packages/5b/84/de7c89bc6ef63d750159086a6ada6416cc4349eab23f76ab870407178b93/xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84", size = 220959, upload-time = "2024-08-17T09:18:26.518Z" }, + { url = "https://files.pythonhosted.org/packages/fe/86/51258d3e8a8545ff26468c977101964c14d56a8a37f5835bc0082426c672/xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793", size = 200006, upload-time = "2024-08-17T09:18:27.905Z" }, + { url = "https://files.pythonhosted.org/packages/02/0a/96973bd325412feccf23cf3680fd2246aebf4b789122f938d5557c54a6b2/xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be", size = 428326, upload-time = "2024-08-17T09:18:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/11/a7/81dba5010f7e733de88af9555725146fc133be97ce36533867f4c7e75066/xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6", size = 194380, upload-time = "2024-08-17T09:18:30.706Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7d/f29006ab398a173f4501c0e4977ba288f1c621d878ec217b4ff516810c04/xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90", size = 207934, upload-time = "2024-08-17T09:18:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6e/6e88b8f24612510e73d4d70d9b0c7dff62a2e78451b9f0d042a5462c8d03/xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27", size = 216301, upload-time = "2024-08-17T09:18:33.474Z" }, + { url = "https://files.pythonhosted.org/packages/af/51/7862f4fa4b75a25c3b4163c8a873f070532fe5f2d3f9b3fc869c8337a398/xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2", size = 203351, upload-time = "2024-08-17T09:18:34.889Z" }, + { url = "https://files.pythonhosted.org/packages/22/61/8d6a40f288f791cf79ed5bb113159abf0c81d6efb86e734334f698eb4c59/xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d", size = 210294, upload-time = "2024-08-17T09:18:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/17/02/215c4698955762d45a8158117190261b2dbefe9ae7e5b906768c09d8bc74/xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab", size = 414674, upload-time = "2024-08-17T09:18:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/b7a8db8a3237cff3d535261325d95de509f6a8ae439a5a7a4ffcff478189/xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e", size = 192022, upload-time = "2024-08-17T09:18:40.138Z" }, + { url = "https://files.pythonhosted.org/packages/78/e3/dd76659b2811b3fd06892a8beb850e1996b63e9235af5a86ea348f053e9e/xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8", size = 30170, upload-time = "2024-08-17T09:18:42.163Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6b/1c443fe6cfeb4ad1dcf231cdec96eb94fb43d6498b4469ed8b51f8b59a37/xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e", size = 30040, upload-time = "2024-08-17T09:18:43.699Z" }, + { url = "https://files.pythonhosted.org/packages/0f/eb/04405305f290173acc0350eba6d2f1a794b57925df0398861a20fbafa415/xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2", size = 26796, upload-time = "2024-08-17T09:18:45.29Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/e4b3ad92d249be5c83fa72916c9091b0965cb0faeff05d9a0a3870ae6bff/xxhash-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37889a0d13b0b7d739cfc128b1c902f04e32de17b33d74b637ad42f1c55101f6", size = 31795, upload-time = "2024-08-17T09:18:46.813Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d8/b3627a0aebfbfa4c12a41e22af3742cf08c8ea84f5cc3367b5de2d039cce/xxhash-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97a662338797c660178e682f3bc180277b9569a59abfb5925e8620fba00b9fc5", size = 30792, upload-time = "2024-08-17T09:18:47.862Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cc/762312960691da989c7cd0545cb120ba2a4148741c6ba458aa723c00a3f8/xxhash-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f85e0108d51092bdda90672476c7d909c04ada6923c14ff9d913c4f7dc8a3bc", size = 220950, upload-time = "2024-08-17T09:18:49.06Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e9/cc266f1042c3c13750e86a535496b58beb12bf8c50a915c336136f6168dc/xxhash-3.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2fd827b0ba763ac919440042302315c564fdb797294d86e8cdd4578e3bc7f3", size = 199980, upload-time = "2024-08-17T09:18:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/bf/85/a836cd0dc5cc20376de26b346858d0ac9656f8f730998ca4324921a010b9/xxhash-3.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82085c2abec437abebf457c1d12fccb30cc8b3774a0814872511f0f0562c768c", size = 428324, upload-time = "2024-08-17T09:18:51.988Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0e/15c243775342ce840b9ba34aceace06a1148fa1630cd8ca269e3223987f5/xxhash-3.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fda5de378626e502b42b311b049848c2ef38784d0d67b6f30bb5008642f8eb", size = 194370, upload-time = "2024-08-17T09:18:54.164Z" }, + { url = "https://files.pythonhosted.org/packages/87/a1/b028bb02636dfdc190da01951d0703b3d904301ed0ef6094d948983bef0e/xxhash-3.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c279f0d2b34ef15f922b77966640ade58b4ccdfef1c4d94b20f2a364617a493f", size = 207911, upload-time = "2024-08-17T09:18:55.509Z" }, + { url = "https://files.pythonhosted.org/packages/80/d5/73c73b03fc0ac73dacf069fdf6036c9abad82de0a47549e9912c955ab449/xxhash-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89e66ceed67b213dec5a773e2f7a9e8c58f64daeb38c7859d8815d2c89f39ad7", size = 216352, upload-time = "2024-08-17T09:18:57.073Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2a/5043dba5ddbe35b4fe6ea0a111280ad9c3d4ba477dd0f2d1fe1129bda9d0/xxhash-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bcd51708a633410737111e998ceb3b45d3dbc98c0931f743d9bb0a209033a326", size = 203410, upload-time = "2024-08-17T09:18:58.54Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b2/9a8ded888b7b190aed75b484eb5c853ddd48aa2896e7b59bbfbce442f0a1/xxhash-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ff2c0a34eae7df88c868be53a8dd56fbdf592109e21d4bfa092a27b0bf4a7bf", size = 210322, upload-time = "2024-08-17T09:18:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/98/62/440083fafbc917bf3e4b67c2ade621920dd905517e85631c10aac955c1d2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e28503dccc7d32e0b9817aa0cbfc1f45f563b2c995b7a66c4c8a0d232e840c7", size = 414725, upload-time = "2024-08-17T09:19:01.332Z" }, + { url = "https://files.pythonhosted.org/packages/75/db/009206f7076ad60a517e016bb0058381d96a007ce3f79fa91d3010f49cc2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6c50017518329ed65a9e4829154626f008916d36295b6a3ba336e2458824c8c", size = 192070, upload-time = "2024-08-17T09:19:03.007Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6d/c61e0668943a034abc3a569cdc5aeae37d686d9da7e39cf2ed621d533e36/xxhash-3.5.0-cp313-cp313-win32.whl", hash = "sha256:53a068fe70301ec30d868ece566ac90d873e3bb059cf83c32e76012c889b8637", size = 30172, upload-time = "2024-08-17T09:19:04.355Z" }, + { url = "https://files.pythonhosted.org/packages/96/14/8416dce965f35e3d24722cdf79361ae154fa23e2ab730e5323aa98d7919e/xxhash-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:80babcc30e7a1a484eab952d76a4f4673ff601f54d5142c26826502740e70b43", size = 30041, upload-time = "2024-08-17T09:19:05.435Z" }, + { url = "https://files.pythonhosted.org/packages/27/ee/518b72faa2073f5aa8e3262408d284892cb79cf2754ba0c3a5870645ef73/xxhash-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:4811336f1ce11cac89dcbd18f3a25c527c16311709a89313c3acaf771def2d4b", size = 26801, upload-time = "2024-08-17T09:19:06.547Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/233606bada5bd6f50b2b72c45de3d9868ad551e83893d2ac86dc7bb8553a/xxhash-3.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2014c5b3ff15e64feecb6b713af12093f75b7926049e26a580e94dcad3c73d8c", size = 29732, upload-time = "2024-08-17T09:20:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/f75276ca39e2c6604e3bee6c84e9db8a56a4973fde9bf35989787cf6e8aa/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fab81ef75003eda96239a23eda4e4543cedc22e34c373edcaf744e721a163986", size = 36214, upload-time = "2024-08-17T09:20:12.335Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f8/f6c61fd794229cc3848d144f73754a0c107854372d7261419dcbbd286299/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2febf914ace002132aa09169cc572e0d8959d0f305f93d5828c4836f9bc5a6", size = 32020, upload-time = "2024-08-17T09:20:13.537Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/c029c99801526f859e6b38d34ab87c08993bf3dcea34b11275775001638a/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d3a10609c51da2a1c0ea0293fc3968ca0a18bd73838455b5bca3069d7f8e32b", size = 40515, upload-time = "2024-08-17T09:20:14.669Z" }, + { url = "https://files.pythonhosted.org/packages/62/e3/bef7b82c1997579c94de9ac5ea7626d01ae5858aa22bf4fcb38bf220cb3e/xxhash-3.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a74f23335b9689b66eb6dbe2a931a88fcd7a4c2cc4b1cb0edba8ce381c7a1da", size = 30064, upload-time = "2024-08-17T09:20:15.925Z" }, +] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910, upload-time = "2025-06-10T00:42:31.108Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644, upload-time = "2025-06-10T00:42:33.851Z" }, + { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322, upload-time = "2025-06-10T00:42:35.688Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786, upload-time = "2025-06-10T00:42:37.817Z" }, + { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627, upload-time = "2025-06-10T00:42:39.937Z" }, + { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149, upload-time = "2025-06-10T00:42:42.627Z" }, + { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327, upload-time = "2025-06-10T00:42:44.842Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054, upload-time = "2025-06-10T00:42:47.149Z" }, + { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035, upload-time = "2025-06-10T00:42:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962, upload-time = "2025-06-10T00:42:51.024Z" }, + { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399, upload-time = "2025-06-10T00:42:53.007Z" }, + { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649, upload-time = "2025-06-10T00:42:54.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563, upload-time = "2025-06-10T00:42:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609, upload-time = "2025-06-10T00:42:59.055Z" }, + { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224, upload-time = "2025-06-10T00:43:01.248Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753, upload-time = "2025-06-10T00:43:03.486Z" }, + { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817, upload-time = "2025-06-10T00:43:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" }, + { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003, upload-time = "2025-06-10T00:43:14.088Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537, upload-time = "2025-06-10T00:43:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358, upload-time = "2025-06-10T00:43:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362, upload-time = "2025-06-10T00:43:20.888Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979, upload-time = "2025-06-10T00:43:23.169Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274, upload-time = "2025-06-10T00:43:27.111Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294, upload-time = "2025-06-10T00:43:28.96Z" }, + { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169, upload-time = "2025-06-10T00:43:30.701Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776, upload-time = "2025-06-10T00:43:32.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341, upload-time = "2025-06-10T00:43:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988, upload-time = "2025-06-10T00:43:36.489Z" }, + { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113, upload-time = "2025-06-10T00:43:38.592Z" }, + { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485, upload-time = "2025-06-10T00:43:41.038Z" }, + { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686, upload-time = "2025-06-10T00:43:42.692Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +] diff --git a/videos/google_clear.gif b/videos/google_clear.gif new file mode 100644 index 0000000..c348b45 Binary files /dev/null and b/videos/google_clear.gif differ diff --git a/videos/mail_clear.gif b/videos/mail_clear.gif new file mode 100644 index 0000000..22452dc Binary files /dev/null and b/videos/mail_clear.gif differ diff --git a/videos/paper_clear.gif b/videos/paper_clear.gif new file mode 100644 index 0000000..d27b4de Binary files /dev/null and b/videos/paper_clear.gif differ diff --git a/videos/wechat_clear.gif b/videos/wechat_clear.gif new file mode 100644 index 0000000..3f976e6 Binary files /dev/null and b/videos/wechat_clear.gif differ